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 36e137bfd..b377a086c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,43 +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: cimg/go:1.18 - - auth: - username: $DOCKER_USER - password: $DOCKER_PASS - - environment: - GOCACHE: "/tmp/go/cache" - GOLANGCI_LINT_VERSION: "1.46.2" 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 - - run: - name: test build - command: make - - 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 c886d46ce..000000000 --- a/.circleci/deploy.sh +++ /dev/null @@ -1,53 +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/ jammy 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" -if [[ "${VERSION}" == "canary" ]]; then - az storage blob upload-batch -s _dist/ -d "$AZURE_STORAGE_CONTAINER_NAME" --pattern 'helm-*' --connection-string "$AZURE_STORAGE_CONNECTION_STRING" --overwrite -else - az storage blob upload-batch -s _dist/ -d "$AZURE_STORAGE_CONTAINER_NAME" --pattern 'helm-*' --connection-string "$AZURE_STORAGE_CONNECTION_STRING" -fi 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-pr.yml b/.github/workflows/build-test.yml similarity index 65% rename from .github/workflows/build-pr.yml rename to .github/workflows/build-test.yml index a8ad9b8c7..7696b4e21 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-test.yml @@ -1,18 +1,23 @@ -name: build-pr +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@v2 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3.6.0 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # pin@4.1.0 with: - go-version: '1.18' + 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 @@ -21,9 +26,11 @@ jobs: 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.46.2' - GOLANGCI_LINT_SHA256: '242cd4f2d6ac0556e315192e8555784d13da5d1874e51304711570769c4f2b9b' + 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..b219b2c5d 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@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # pinv2.21.8 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@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # pinv2.21.8 # ℹ️ 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@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # pinv2.21.8 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/OWNERS b/OWNERS index ed06be851..cc18ea522 100644 --- a/OWNERS +++ b/OWNERS @@ -1,19 +1,20 @@ maintainers: - - adamreese - - bacongobbler - hickeyma + - joejulian - jdolitsky - marckhouzam - mattfarina - sabre1041 - scottrigby - - SlickNik - technosophos triage: - - joejulian - yxxhero - zonggen + - gjenkins8 + - z4ce emeritus: + - adamreese + - bacongobbler - fibonacci1729 - jascott1 - michelleN @@ -22,6 +23,7 @@ emeritus: - 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/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_update_test.go b/cmd/helm/dependency_update_test.go index 491f6a856..967786b9a 100644 --- a/cmd/helm/dependency_update_test.go +++ b/cmd/helm/dependency_update_test.go @@ -149,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 { @@ -206,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/flags.go b/cmd/helm/flags.go index 76d6e0476..a8f25cb35 100644 --- a/cmd/helm/flags.go +++ b/cmd/helm/flags.go @@ -48,6 +48,7 @@ func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { 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) { @@ -60,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") } 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 2dbef97cf..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, 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 15b0d5c76..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" @@ -106,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 1adcf016f..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" @@ -92,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/install.go b/cmd/helm/install.go index 1ad959777..7e4bff9c2 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -136,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 errors.Wrap(err, "INSTALLATION FAILED") } - return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false}) + return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false}) }, } @@ -154,7 +167,13 @@ 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") @@ -171,6 +190,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal 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.BoolVar(&client.SkipSchemaValidation, "skip-schema-validation", false, "if set, disables JSON schema validation") + 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) @@ -247,6 +267,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 @@ -263,6 +284,11 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options client.Namespace = settings.Namespace() + // 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) @@ -303,3 +329,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/lint_test.go b/cmd/helm/lint_test.go index ebea09bf0..314b54c35 100644 --- a/cmd/helm/lint_test.go +++ b/cmd/helm/lint_test.go @@ -53,6 +53,11 @@ func TestLintCmdWithQuietFlag(t *testing.T) { 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) diff --git a/cmd/helm/load_plugins.go b/cmd/helm/load_plugins.go index 6b67ac28f..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 @@ -311,7 +311,7 @@ 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, fmt.Errorf("file (%s) not provided by plugin. No plugin auto-completion possible", path) } diff --git a/cmd/helm/package.go b/cmd/helm/package.go index ce965b729..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" @@ -87,7 +86,7 @@ func newPackageCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if client.DependencyUpdate { downloadManager := &downloader.Manager{ - Out: ioutil.Discard, + Out: io.Discard, ChartPath: path, Keyring: client.Keyring, Getters: p, diff --git a/cmd/helm/package_test.go b/cmd/helm/package_test.go index d7e01fb75..9093b510c 100644 --- a/cmd/helm/package_test.go +++ b/cmd/helm/package_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/chart/loader" ) @@ -111,7 +110,7 @@ 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 { @@ -170,7 +169,7 @@ func TestSetAppVersion(t *testing.T) { var ch *chart.Chart expectedAppVersion := "app-version-foo" chartToPackage := "testdata/testcharts/alpine" - dir := ensure.TempDir(t) + dir := t.TempDir() cmd := fmt.Sprintf("package %s --destination=%s --app-version=%s", chartToPackage, dir, expectedAppVersion) _, output, err := executeActionCommand(cmd) if err != nil { diff --git a/cmd/helm/plugin_test.go b/cmd/helm/plugin_test.go index 33de33522..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 diff --git a/cmd/helm/pull.go b/cmd/helm/pull.go index 378301196..af3092aff 100644 --- a/cmd/helm/pull.go +++ b/cmd/helm/pull.go @@ -64,6 +64,13 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.Version = ">0.0.0-0" } + 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]) if err != nil { diff --git a/cmd/helm/push.go b/cmd/helm/push.go index d2cf2693e..3375155ed 100644 --- a/cmd/helm/push.go +++ b/cmd/helm/push.go @@ -34,8 +34,16 @@ 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 { - client := action.NewPushWithOpts(action.WithPushConfig(cfg)) + o := ®istryPushOptions{} cmd := &cobra.Command{ Use: "push [chart] [remote]", @@ -60,8 +68,18 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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 { @@ -72,5 +90,12 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { }, } + 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/cmd/helm/registry_login.go b/cmd/helm/registry_login.go index 6b1fed589..112e06a95 100644 --- a/cmd/helm/registry_login.go +++ b/cmd/helm/registry_login.go @@ -21,7 +21,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "strings" @@ -36,9 +35,18 @@ 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]", @@ -49,20 +57,27 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman 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 } @@ -74,7 +89,7 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd password := passwordOpt if passwordFromStdinOpt { - passwordFromStdin, err := ioutil.ReadAll(os.Stdin) + passwordFromStdin, err := io.ReadAll(os.Stdin) if err != nil { return "", "", err } diff --git a/cmd/helm/release_testing.go b/cmd/helm/release_testing.go index d9b8fa8c9..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, 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 e13df7ad8..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" @@ -134,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 } @@ -213,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 f9b0cab00..2386bb01f 100644 --- a/cmd/helm/repo_add_test.go +++ b/cmd/helm/repo_add_test.go @@ -18,7 +18,7 @@ package main import ( "fmt" - "io/ioutil" + "io" "os" "path/filepath" "strings" @@ -27,7 +27,6 @@ import ( "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" @@ -48,7 +47,7 @@ func TestRepoAddCmd(t *testing.T) { } defer srv2.Stop() - tmpdir := filepath.Join(ensure.TempDir(t), "path-component.yaml/data") + tmpdir := filepath.Join(t.TempDir(), "path-component.yaml/data") err = os.MkdirAll(tmpdir, 0777) if err != nil { t.Fatal(err) @@ -88,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" @@ -102,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) } @@ -126,11 +125,11 @@ 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") } } @@ -145,8 +144,8 @@ func TestRepoAddCheckLegalName(t *testing.T) { const testRepoName = "test-hub/test-name" - rootDir := ensure.TempDir(t) - repoFile := filepath.Join(ensure.TempDir(t), "repositories.yaml") + rootDir := t.TempDir() + repoFile := filepath.Join(t.TempDir(), "repositories.yaml") o := &repoAddOptions{ name: testRepoName, @@ -159,7 +158,7 @@ func TestRepoAddCheckLegalName(t *testing.T) { wantErrorMsg := fmt.Sprintf("repository name (%s) contains '/', please specify a different name without '/'", testRepoName) - if err := o.run(ioutil.Discard); err != nil { + 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) } @@ -170,25 +169,25 @@ func TestRepoAddCheckLegalName(t *testing.T) { 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) } @@ -211,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) } @@ -254,7 +253,7 @@ func TestRepoAddWithPasswordFromStdin(t *testing.T) { t.Errorf("unexpected error, got '%v'", err) } - tmpdir := ensure.TempDir(t) + tmpdir := t.TempDir() repoFile := filepath.Join(tmpdir, "repositories.yaml") store := storageFixture() 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_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 768295655..e2795e738 100644 --- a/cmd/helm/repo_remove_test.go +++ b/cmd/helm/repo_remove_test.go @@ -24,7 +24,6 @@ import ( "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" @@ -37,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" @@ -169,7 +168,7 @@ func TestRepoRemoveCompletion(t *testing.T) { } defer ts.Stop() - rootDir := ensure.TempDir(t) + rootDir := t.TempDir() repoFile := filepath.Join(rootDir, "repositories.yaml") repoCache := filepath.Join(rootDir, "cache/") diff --git a/cmd/helm/repo_update_test.go b/cmd/helm/repo_update_test.go index cf2136ff0..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" @@ -103,10 +102,9 @@ func TestUpdateCmdInvalid(t *testing.T) { } 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 { @@ -119,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) } @@ -130,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 { @@ -165,7 +163,7 @@ func TestRepoUpdateFileCompletion(t *testing.T) { func TestUpdateChartsFail(t *testing.T) { defer resetEnv()() - defer ensure.HelmHome(t)() + ensure.HelmHome(t) ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") if err != nil { @@ -198,7 +196,7 @@ func TestUpdateChartsFail(t *testing.T) { func TestUpdateChartsFailWithError(t *testing.T) { defer resetEnv()() - defer ensure.HelmHome(t)() + ensure.HelmHome(t) ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") if err != nil { 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 7da57c6aa..dd95b1df2 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -152,12 +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.ClientOptEnableCache(true), - registry.ClientOptWriter(os.Stderr), - registry.ClientOptCredentialsFile(settings.RegistryConfig), - ) + registryClient, err := newDefaultRegistryClient(false) if err != nil { return nil, err } @@ -261,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/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 b8887efd5..1618a4c9f 100644 --- a/cmd/helm/search_hub.go +++ b/cmd/helm/search_hub.go @@ -54,6 +54,7 @@ type searchHubOptions struct { maxColWidth uint outputFormat output.Format listRepoURL bool + failOnNoResult bool } func newSearchHubCmd(out io.Writer) *cobra.Command { @@ -72,6 +73,7 @@ func newSearchHubCmd(out io.Writer) *cobra.Command { 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) @@ -91,7 +93,7 @@ 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, o.listRepoURL)) + return o.outputFormat.Write(out, newHubSearchWriter(results, o.searchEndpoint, o.maxColWidth, o.listRepoURL, o.failOnNoResult)) } type hubChartRepo struct { @@ -108,12 +110,13 @@ type hubChartElement struct { } type hubSearchWriter struct { - elements []hubChartElement - columnWidth uint - listRepoURL bool + elements []hubChartElement + columnWidth uint + listRepoURL bool + failOnNoResult bool } -func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint, listRepoURL bool) *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 @@ -126,11 +129,16 @@ func newHubSearchWriter(results []monocular.SearchResult, endpoint string, colum 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, listRepoURL} + 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) @@ -165,6 +173,11 @@ 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)) diff --git a/cmd/helm/search_hub_test.go b/cmd/helm/search_hub_test.go index 7df54ea8f..89ce2b3e5 100644 --- a/cmd/helm/search_hub_test.go +++ b/cmd/helm/search_hub_test.go @@ -90,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 f794f6bca..2c6f17094 100644 --- a/cmd/helm/search_repo.go +++ b/cmd/helm/search_repo.go @@ -21,7 +21,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -64,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 { @@ -94,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 @@ -124,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() { @@ -205,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) @@ -235,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)) @@ -259,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() { 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 718d716a0..28eb9756d 100644 --- a/cmd/helm/show.go +++ b/cmd/helm/show.go @@ -84,6 +84,10 @@ func newShowCmd(cfg *action.Configuration, 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 @@ -101,6 +105,10 @@ func newShowCmd(cfg *action.Configuration, 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 @@ -118,6 +126,10 @@ func newShowCmd(cfg *action.Configuration, 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 @@ -135,6 +147,10 @@ func newShowCmd(cfg *action.Configuration, 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 @@ -152,6 +168,10 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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 @@ -204,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/status.go b/cmd/helm/status.go index aa22aa02a..850862cd5 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -80,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, client.ShowResources}) + return outfmt.Write(out, &statusPrinter{rel, false, client.ShowDescription, client.ShowResources, false}) }, } @@ -112,6 +112,7 @@ type statusPrinter struct { debug bool showDescription bool showResources bool + showMetadata bool } func (s statusPrinter) WriteJSON(out io.Writer) error { @@ -126,15 +127,20 @@ 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 { @@ -149,31 +155,31 @@ func (s statusPrinter) WriteTable(out io.Writer) error { } for _, t := range keys { - fmt.Fprintf(buf, "==> %s\n", t) + _, _ = 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) + _, _ = 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()) + _, _ = 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)), @@ -183,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 6d34d6db7..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, }} } diff --git a/cmd/helm/template.go b/cmd/helm/template.go index 0ddd0c551..a16cbc76e 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -73,6 +73,19 @@ 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.Replace = true // Skip the name check diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go index d1f17fe98..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", @@ -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/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/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 index a4e8575f8..e3d29a5a3 100644 --- a/cmd/helm/testdata/output/lint-quiet-with-error.txt +++ b/cmd/helm/testdata/output/lint-quiet-with-error.txt @@ -1,7 +1,7 @@ ==> 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 -[WARNING] templates/: directory not found +[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 diff --git a/cmd/helm/testdata/output/lint-quiet-with-warning.txt b/cmd/helm/testdata/output/lint-quiet-with-warning.txt index 02c6fa592..e69de29bb 100644 --- a/cmd/helm/testdata/output/lint-quiet-with-warning.txt +++ b/cmd/helm/testdata/output/lint-quiet-with-warning.txt @@ -1,4 +0,0 @@ -==> Linting testdata/testcharts/chart-with-only-crds -[WARNING] templates/: directory not found - -1 chart(s) linted, 0 chart(s) failed 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/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/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-with-crds.txt b/cmd/helm/testdata/output/template-with-crds.txt index dd58480c9..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: diff --git a/cmd/helm/testdata/output/version-client-shorthand.txt b/cmd/helm/testdata/output/version-client-shorthand.txt index a623a516f..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.11", 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 a623a516f..9a42dcba7 100644 --- a/cmd/helm/testdata/output/version-client.txt +++ b/cmd/helm/testdata/output/version-client.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v3.11", 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 7784d9743..588b5b7c5 100644 --- a/cmd/helm/testdata/output/version-short.txt +++ b/cmd/helm/testdata/output/version-short.txt @@ -1 +1 @@ -v3.11 +v3.13 diff --git a/cmd/helm/testdata/output/version-template.txt b/cmd/helm/testdata/output/version-template.txt index 9c5308b73..b6f541d94 100644 --- a/cmd/helm/testdata/output/version-template.txt +++ b/cmd/helm/testdata/output/version-template.txt @@ -1 +1 @@ -Version: v3.11 \ 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 a623a516f..9a42dcba7 100644 --- a/cmd/helm/testdata/output/version.txt +++ b/cmd/helm/testdata/output/version.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v3.11", GitCommit:"", GitTreeState:"", GoVersion:""} +version.BuildInfo{Version:"v3.13", GitCommit:"", GitTreeState:"", GoVersion:""} 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/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 67f778f15..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,10 +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/upgrade.go b/cmd/helm/upgrade.go index fbf5e4771..a44ab5270 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -65,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 { @@ -90,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 { @@ -106,6 +126,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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 @@ -119,13 +140,14 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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, false}) + return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false}) } else if err != nil { return err } @@ -140,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) @@ -207,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, false}) + return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false}) }, } @@ -215,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") @@ -231,6 +258,7 @@ 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.SkipSchemaValidation, "skip-schema-validation", false, "if set, disables JSON schema validation") diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index 8afcb139b..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) } @@ -431,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 96541ff9a..c169a6283 100644 --- a/go.mod +++ b/go.mod @@ -1,113 +1,120 @@ module helm.sh/helm/v3 -go 1.17 +go 1.19 require ( - github.com/BurntSushi/toml v1.2.1 + github.com/BurntSushi/toml v1.3.2 github.com/DATA-DOG/go-sqlmock v1.5.0 - github.com/Masterminds/semver/v3 v3.2.0 + github.com/Masterminds/semver/v3 v3.2.1 github.com/Masterminds/sprig/v3 v3.2.3 - github.com/Masterminds/squirrel v1.5.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.6.15 - github.com/cyphar/filepath-securejoin v0.2.3 + 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.1 github.com/gosuri/uitable v0.0.4 + github.com/hashicorp/go-multierror v1.1.1 github.com/jmoiron/sqlx v1.3.5 - github.com/lib/pq v1.10.7 + 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.0.0-20221205130635-1aeaba878587 - github.com/opencontainers/image-spec v1.1.0-rc2 + 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 v1.2.0 - github.com/sirupsen/logrus v1.9.0 - github.com/spf13/cobra v1.6.1 + 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.8.1 + github.com/stretchr/testify v1.8.4 github.com/xeipuuv/gojsonschema v1.2.0 - golang.org/x/crypto v0.5.0 - golang.org/x/term v0.4.0 - golang.org/x/text v0.6.0 - k8s.io/api v0.26.0 - k8s.io/apiextensions-apiserver v0.26.0 - k8s.io/apimachinery v0.26.0 - k8s.io/apiserver v0.26.0 - k8s.io/cli-runtime v0.26.0 - k8s.io/client-go v0.26.0 - k8s.io/klog/v2 v2.80.1 - k8s.io/kubectl v0.26.0 - oras.land/oras-go v1.2.2 + 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 ) 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.1.2 // 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 v20.10.21+incompatible // indirect - github.com/docker/distribution v2.8.1+incompatible // indirect - github.com/docker/docker v20.10.21+incompatible // 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.4.0 // 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.9.0 // 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.7.0 // indirect + github.com/fatih/color v1.13.0 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect - github.com/fvbommel/sortorder v1.0.1 // indirect - github.com/go-errors/errors v1.0.1 // indirect - github.com/go-gorp/gorp/v3 v3.0.2 // indirect - github.com/go-logr/logr v1.2.3 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/swag v0.19.14 // 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.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 v0.5.7-v3refs // 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.2.0 // 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.3.3 // indirect - github.com/imdario/mergo v0.3.12 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // 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.11.13 // 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.6 // indirect - github.com/mattn/go-colorable v0.0.9 // indirect - github.com/mattn/go-isatty v0.0.3 // 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/mitchellh/go-wordwrap v1.0.0 // 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 @@ -119,37 +126,39 @@ require ( 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.14.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.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.2.0 // indirect - github.com/spf13/cast v1.3.1 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // 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.1.0 // 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.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect - golang.org/x/net v0.5.0 // indirect - golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.4.0 // indirect - golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // 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 v0.0.0-20220502173005-c8bf987b8c21 // indirect - google.golang.org/grpc v1.49.0 // indirect - google.golang.org/protobuf v1.28.1 // 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.26.0 // indirect - k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect - k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect - sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect - sigs.k8s.io/kustomize/api v0.12.1 // indirect - sigs.k8s.io/kustomize/kyaml v0.13.9 // 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 b855fc842..974a2c68d 100644 --- a/go.sum +++ b/go.sum @@ -1,78 +1,33 @@ 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/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -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/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -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/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -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= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +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/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -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/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.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 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/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/sprig/v3 v3.2.0/go.mod h1:tWhwTbUTndesPNeF0C900vKoq283u6zp4APT9vaF3SI= +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.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= -github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +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.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= -github.com/Microsoft/hcsshim v0.9.6 h1:VwnDOgLeoi2du6dAznfmspNqTiwczvjv4K7NxuY9jsY= +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/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/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -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/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= @@ -80,9 +35,7 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 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/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= 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= @@ -92,46 +45,35 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= -github.com/containerd/containerd v1.6.15 h1:4wWexxzLNHNE46aIETc6ge4TofO550v+BlLoANrbses= -github.com/containerd/containerd v1.6.15/go.mod h1:U2NnBPIhzJDm59xF7xB2MMHnKtggpZ+phKg8o2TKj2c= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +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.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= -github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +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/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 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 v20.10.21+incompatible h1:qVkgyYUnOLQ98LtXBrwd/duVqPT2X4SHndOuGsfwyhU= -github.com/docker/cli v20.10.21+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= -github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog= -github.com/docker/docker v20.10.21+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +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= @@ -140,162 +82,100 @@ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ 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.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +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/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= -github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= -github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +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/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 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/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +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/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE= -github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -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-gorp/gorp/v3 v3.0.2 h1:ULqJXIekoqMx29FI5ekXXFoH1dT2Vc8UhnRzBg+Emz4= -github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY= +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/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 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-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +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/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/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= -github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= -github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= 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.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= 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.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-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/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/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 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.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 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/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +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 v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 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 v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= -github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +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.4.1/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.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/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.6/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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -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/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +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.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.0/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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +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= @@ -305,72 +185,39 @@ 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/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/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.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/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +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/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/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +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/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 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.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= -github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= 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.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4= -github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +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.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 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/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -380,53 +227,39 @@ 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/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/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/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -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.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= -github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -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 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +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.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw= -github.com/mattn/go-sqlite3 v1.14.14/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.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.4/go.mod h1:vTLESy5mRhKOs9KDp0/RATawxP1UqBmdrpVRMnpcvKQ= +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.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-homedir v1.0.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/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +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/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -435,9 +268,9 @@ 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/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +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= @@ -452,94 +285,60 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P 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/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= -github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys= +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.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= -github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +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-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 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/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1 h1:oL4IBbcqwhhNWh31bjOX8C/OCy0zs9906d/VUru+bqg= -github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= +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 v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +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-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.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +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.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +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.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rubenv/sql-migrate v1.2.0 h1:fOXMPLMd41sK7Tg75SXDec15k3zg5WNV6SjuDRiNfcU= -github.com/rubenv/sql-migrate v1.2.0/go.mod h1:Z5uVnq7vrIrPmHbVFfR4YLHRZquxeHpckCnRq0P/K9Y= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -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/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.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -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 v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +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.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -547,29 +346,24 @@ 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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +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/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= -github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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= @@ -577,473 +371,186 @@ github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMzt 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/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -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/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -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.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/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-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +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/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/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.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/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-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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/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-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-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/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-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 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.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/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.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-20181026203630-95b1ffbd15a5/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/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-20190801041406-cbf593c0f2f3/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-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-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-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-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-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/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-20210104204734-6f8348627aad/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-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/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.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.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-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.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -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-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/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-20191012152004-8de300cfc20a/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-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-20200227222343-706bc42d1f0d/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-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/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-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 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.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -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.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 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/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 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-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-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 h1:hrbNEivu7Zn1pxvHk6MBrq9iE22woVILTHqexqBxe6I= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +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.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -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/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +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/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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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-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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 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.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +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= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.26.0 h1:IpPlZnxBpV1xl7TGk/X6lFtpgjgntCg8PJ+qrPHAC7I= -k8s.io/api v0.26.0/go.mod h1:k6HDTaIFC8yn1i6pSClSqIwLABIcLV9l5Q4EcngKnQg= -k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo= -k8s.io/apiextensions-apiserver v0.26.0/go.mod h1:7ez0LTiyW5nq3vADtK6C3kMESxadD51Bh6uz3JOlqWQ= -k8s.io/apimachinery v0.26.0 h1:1feANjElT7MvPqp0JT6F3Ss6TWDwmcjLypwoPpEf7zg= -k8s.io/apimachinery v0.26.0/go.mod h1:tnPmbONNJ7ByJNz9+n9kMjNP8ON+1qoAIIC70lztu74= -k8s.io/apiserver v0.26.0 h1:q+LqIK5EZwdznGZb8bq0+a+vCqdeEEe4Ux3zsOjbc4o= -k8s.io/apiserver v0.26.0/go.mod h1:aWhlLD+mU+xRo+zhkvP/gFNbShI4wBDHS33o0+JGI84= -k8s.io/cli-runtime v0.26.0 h1:aQHa1SyUhpqxAw1fY21x2z2OS5RLtMJOCj7tN4oq8mw= -k8s.io/cli-runtime v0.26.0/go.mod h1:o+4KmwHzO/UK0wepE1qpRk6l3o60/txUZ1fEXWGIKTY= -k8s.io/client-go v0.26.0 h1:lT1D3OfO+wIi9UFolCrifbjUUgu7CpLca0AD8ghRLI8= -k8s.io/client-go v0.26.0/go.mod h1:I2Sh57A79EQsDmn7F7ASpmru1cceh3ocVT9KlX2jEZg= -k8s.io/component-base v0.26.0 h1:0IkChOCohtDHttmKuz+EP3j3+qKmV55rM9gIFTXA7Vs= -k8s.io/component-base v0.26.0/go.mod h1:lqHwlfV1/haa14F/Z5Zizk5QmzaVf23nQzCwVOQpfC8= -k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= -k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= -k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= -k8s.io/kubectl v0.26.0 h1:xmrzoKR9CyNdzxBmXV7jW9Ln8WMrwRK6hGbbf69o4T0= -k8s.io/kubectl v0.26.0/go.mod h1:eInP0b+U9XUJWSYeU9XZnTA+cVYuWyl3iYPGtru0qhQ= -k8s.io/utils v0.0.0-20221107191617-1a15be271d1d h1:0Smp/HP1OH4Rvhe+4B8nWGERtlqAGSftbSbbmm45oFs= -k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -oras.land/oras-go v1.2.2 h1:0E9tOHUfrNH7TCDk5KU0jVBEzCqbfdyuVfGmJ7ZeRPE= -oras.land/oras-go v1.2.2/go.mod h1:Apa81sKoZPpP7CDciE006tSZ0x3Q3+dOoBcMZ/aNxvw= -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/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM= -sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s= -sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= -sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4= +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= 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 76cd8f074..92920d3c4 100644 --- a/internal/fileutil/fileutil_test.go +++ b/internal/fileutil/fileutil_test.go @@ -18,7 +18,6 @@ package fileutil import ( "bytes" - "io/ioutil" "os" "path/filepath" "testing" @@ -37,7 +36,7 @@ func TestAtomicWriteFile(t *testing.T) { 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/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 4d5862c70..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" @@ -53,7 +53,7 @@ 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) } @@ -73,7 +73,7 @@ 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) } @@ -88,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_test.go b/internal/third_party/dep/fs/fs_test.go index c9e6ce65b..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" @@ -137,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) } @@ -337,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) } @@ -396,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) } 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 3cfcfef92..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.11" + version = "v3.13" // metadata is extra build time data metadata = "" diff --git a/pkg/action/action.go b/pkg/action/action.go index 016aec3f6..5693f4838 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -103,7 +103,7 @@ type Configuration struct { // 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, enableDNS bool) ([]*release.Hook, *bytes.Buffer, string, error) { +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) @@ -121,12 +121,10 @@ 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 @@ -189,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 } } } diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index c816c84af..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, @@ -17,7 +17,7 @@ package action import ( "flag" - "io/ioutil" + "io" "testing" fakeclientset "k8s.io/client-go/kubernetes/fake" @@ -44,7 +44,7 @@ func actionConfigFixture(t *testing.T) *Configuration { 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/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 483d8e84c..d16f52a82 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -20,7 +20,7 @@ import ( "bytes" "context" "fmt" - "io/ioutil" + "io" "net/url" "os" "path" @@ -34,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" @@ -72,6 +73,7 @@ type Install struct { Force bool CreateNamespace bool DryRun bool + DryRunOption string DisableHooks bool Replace bool Wait bool @@ -91,6 +93,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 @@ -114,6 +117,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 @@ -137,6 +141,16 @@ func NewInstall(cfg *Configuration) *Install { 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 { // We do these one file at a time in the order they were read. totalItems := []*resource.Info{} @@ -160,22 +174,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 } @@ -190,6 +220,9 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. } // 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 { @@ -202,15 +235,20 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma return nil, err } - if err := chartutil.ProcessDependencies(chrt, vals); err != nil { + 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 @@ -225,7 +263,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma 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) @@ -244,7 +282,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma } // 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, @@ -266,10 +304,14 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma } } - 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, i.EnableDNS) + 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() @@ -305,12 +347,12 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma 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 } @@ -356,23 +398,48 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma // not working. return rel, err } - rChan := make(chan resultMessage) - doneChan := make(chan struct{}) - defer close(doneChan) - go i.performInstall(rChan, rel, toBeAdopted, resources) - go i.handleContext(ctx, rChan, doneChan, rel) - result := <-rChan - //start preformInstall go routine - return result.r, result.e + + rel, err = i.performInstallCtx(ctx, rel, toBeAdopted, resources) + if err != nil { + rel, err = i.failRelease(rel, err) + } + return rel, err } -func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) { +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 { - i.reportToRun(c, rel, fmt.Errorf("failed pre-install: %s", err)) - return + return rel, fmt.Errorf("failed pre-install: %s", err) } } @@ -380,35 +447,28 @@ func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, t // 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 { - i.reportToRun(c, rel, err) - return - } + _, err = i.cfg.KubeClient.Create(resources) } else if len(resources) > 0 { - if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, i.Force); err != nil { - i.reportToRun(c, rel, err) - return - } + _, 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 { - i.reportToRun(c, rel, err) - return - } + err = i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout) } else { - if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil { - i.reportToRun(c, rel, err) - return - } + 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 { - i.reportToRun(c, rel, fmt.Errorf("failed post-install: %s", err)) - return + return rel, fmt.Errorf("failed post-install: %s", err) } } @@ -429,25 +489,9 @@ func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, t i.cfg.Log("failed to record the release: %s", err) } - i.reportToRun(c, rel, nil) -} -func (i *Install) handleContext(ctx context.Context, c chan<- resultMessage, done chan struct{}, rel *release.Release) { - select { - case <-ctx.Done(): - err := ctx.Err() - i.reportToRun(c, rel, err) - case <-done: - return - } -} -func (i *Install) reportToRun(c chan<- resultMessage, rel *release.Release, err error) { - i.Lock.Lock() - if err != nil { - rel, err = i.failRelease(rel, err) - } - c <- resultMessage{r: rel, e: err} - i.Lock.Unlock() + return rel, nil } + func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) { rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) if i.Atomic { @@ -479,7 +523,8 @@ func (i *Install) availableName() error { 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 } @@ -497,7 +542,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, @@ -510,6 +555,7 @@ func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{ Status: release.StatusUnknown, }, Version: 1, + Labels: labels, } } @@ -686,8 +732,6 @@ 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 there is no registry client and the name is in an OCI registry return - // an error and a lookup will not occur. if registry.IsOCI(name) && c.registryClient == nil { return "", fmt.Errorf("unable to lookup chart %q, missing registry client", name) } @@ -719,6 +763,7 @@ 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, diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 45e5a2670..bc0890115 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -19,10 +19,11 @@ package action import ( "context" "fmt" - "io/ioutil" + "io" "os" "path/filepath" "regexp" + "runtime" "strings" "testing" "time" @@ -132,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) { @@ -254,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) @@ -369,10 +370,14 @@ 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) @@ -388,10 +393,16 @@ func TestInstallRelease_Wait_Interrupted(t *testing.T) { 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) @@ -717,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 7678bcfef..4724ad292 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" @@ -84,7 +83,7 @@ func HasWarningsOrErrors(result *LintResult) bool { return true } } - return false + return len(result.Errors) > 0 } func lintChart(path string, vals map[string]interface{}, namespace string, strict bool, skipSchemaValidation bool) (support.Linter, error) { @@ -92,7 +91,7 @@ func lintChart(path string, vals map[string]interface{}, namespace string, stric 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") } diff --git a/pkg/action/lint_test.go b/pkg/action/lint_test.go index 46da4e103..271fbd680 100644 --- a/pkg/action/lint_test.go +++ b/pkg/action/lint_test.go @@ -150,12 +150,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/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 b4018869e..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" @@ -72,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 @@ -86,6 +90,7 @@ 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, @@ -95,6 +100,7 @@ func (p *Pull) Run(chartRef string) (string, error) { if registry.IsOCI(chartRef) { c.Options = append(c.Options, getter.WithRegistryClient(p.cfg.RegistryClient)) + c.RegistryClient = p.cfg.RegistryClient } if p.Verify { @@ -108,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 index 99d1beadc..68d2ba42d 100644 --- a/pkg/action/push.go +++ b/pkg/action/push.go @@ -17,6 +17,7 @@ limitations under the License. package action import ( + "io" "strings" "helm.sh/helm/v3/pkg/cli" @@ -29,8 +30,14 @@ import ( // // It provides the implementation of 'helm push'. type Push struct { - Settings *cli.EnvSettings - cfg *Configuration + 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. @@ -43,6 +50,36 @@ func WithPushConfig(cfg *Configuration) PushOpt { } } +// 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{} @@ -59,10 +96,15 @@ func (p *Push) Run(chartRef string, remote string) (string, error) { c := uploader.ChartUploader{ Out: &out, Pushers: pusher.All(p.Settings), - Options: []pusher.Option{}, + 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)) } diff --git a/pkg/action/registry_login.go b/pkg/action/registry_login.go index 68bcc7442..a55f2de58 100644 --- a/pkg/action/registry_login.go +++ b/pkg/action/registry_login.go @@ -24,7 +24,45 @@ import ( // 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. @@ -35,9 +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 { +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(insecure)) + 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 dda8c700b..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) diff --git a/pkg/action/show.go b/pkg/action/show.go index 9ba85234d..6ed855b83 100644 --- a/pkg/action/show.go +++ b/pkg/action/show.go @@ -28,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` @@ -82,6 +83,11 @@ func NewShowWithConfig(output ShowOutputFormat, cfg *Configuration) *Show { 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 { @@ -147,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/uninstall.go b/pkg/action/uninstall.go index 9dcbf19b0..40d82243e 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -22,6 +22,8 @@ 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" @@ -35,12 +37,14 @@ import ( type Uninstall struct { cfg *Configuration - DisableHooks bool - DryRun bool - KeepHistory bool - Wait 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. @@ -71,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 { @@ -220,7 +227,25 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri 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 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 9cc75520b..869ffb8c7 100644 --- a/pkg/action/uninstall_test.go +++ b/pkg/action/uninstall_test.go @@ -32,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) @@ -95,3 +106,35 @@ func TestUninstallRelease_Wait(t *testing.T) { 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 63def9e09..7709a4c04 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -32,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" @@ -70,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. @@ -92,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 @@ -124,6 +127,11 @@ func NewUpgrade(cfg *Configuration) *Upgrade { 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() @@ -143,6 +151,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart. 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 { @@ -157,7 +166,8 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart. 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 @@ -167,6 +177,14 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart. 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 { @@ -211,7 +229,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 } @@ -244,11 +262,21 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin } } - hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, u.DryRun, u.EnableDNS) + // 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, @@ -264,6 +292,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 { @@ -311,7 +340,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR 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 { @@ -322,7 +351,8 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR 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 @@ -585,3 +615,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 62922b373..77656e1c5 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -19,10 +19,12 @@ 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" @@ -386,5 +388,97 @@ func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) { 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/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_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 a737098b4..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) } diff --git a/pkg/chart/metadata_test.go b/pkg/chart/metadata_test.go index 691e6bf46..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, diff --git a/pkg/chartutil/capabilities_test.go b/pkg/chartutil/capabilities_test.go index 2704d34c1..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.11" { - t.Errorf("Expected default HelmVersion to be v3.11, 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 f634d6425..6cf23a122 100644 --- a/pkg/chartutil/coalesce.go +++ b/pkg/chartutil/coalesce.go @@ -37,12 +37,42 @@ func concatPrefix(a, b string) string { // // 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 @@ -53,21 +83,26 @@ func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, err if valsCopy == nil { valsCopy = make(map[string]interface{}) } - return coalesce(log.Printf, 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(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string) (map[string]interface{}, error) { - coalesceValues(printf, ch, dest, prefix) - return coalesceDeps(printf, ch, dest, prefix) +// 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(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string) (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. @@ -78,13 +113,11 @@ func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{} 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(printf, dvmap, dest, subPrefix) - + coalesceGlobals(printf, dvmap, dest, subPrefix, merge) // Now coalesce the rest of the values. var err error - dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix) + dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge) if err != nil { return dest, err } @@ -96,7 +129,7 @@ func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{} // coalesceGlobals copies the globals out of src and merges them into dest. // // For convenience, returns dest. -func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string) { +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 { @@ -130,7 +163,10 @@ func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix st // Basically, we reverse order of coalesce here to merge // top-down. subPrefix := concatPrefix(prefix, key) - coalesceTablesFullKey(printf, vv, destvmap, subPrefix) + // 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 } } @@ -156,12 +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(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string) { +func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string, merge bool) { subPrefix := concatPrefix(prefix, c.Metadata.Name) - for key, val := range c.Values { + + // 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) @@ -177,7 +239,7 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr } else { // Because v has higher precedence than nv, dest values override src // values. - coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key)) + coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge) } } } else { @@ -191,13 +253,17 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr // // dest is considered authoritative. func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} { - return coalesceTablesFullKey(log.Printf, dst, src, "") + 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) map[string]interface{} { +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 @@ -209,13 +275,13 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, pref // values. for key, val := range src { fullkey := concatPrefix(prefix, key) - if dv, ok := dst[key]; ok && dv == nil { + 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) { - coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey) + coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey, merge) } else { printf("warning: cannot overwrite table with non table for %s (%v)", fullkey, val) } diff --git a/pkg/chartutil/coalesce_test.go b/pkg/chartutil/coalesce_test.go index 3fe93f5ff..61b718d97 100644 --- a/pkg/chartutil/coalesce_test.go +++ b/pkg/chartutil/coalesce_test.go @@ -213,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", @@ -341,6 +495,143 @@ func TestCoalesceTables(t *testing.T) { } } +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{ @@ -391,7 +682,7 @@ func TestCoalesceValuesWarnings(t *testing.T) { warnings = append(warnings, fmt.Sprintf(format, v...)) } - _, err := coalesce(printf, c, vals, "") + _, err := coalesce(printf, c, vals, "", false) if err != nil { t.Fatal(err) } diff --git a/pkg/chartutil/create.go b/pkg/chartutil/create.go index 3a8f3cc5a..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: @@ -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 f123a37cd..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" @@ -100,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) } @@ -131,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 e01b95bf7..a87f0fbe8 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 } @@ -248,7 +271,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{ @@ -260,26 +287,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(cvals, b) + // 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 7f5e74956..7e035be5f 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" @@ -212,7 +218,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) @@ -225,18 +231,44 @@ 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", 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) { @@ -244,10 +276,25 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { 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" - - if err := processDependencyImportValues(c); err != nil { + 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) @@ -274,7 +321,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/jsonschema_test.go b/pkg/chartutil/jsonschema_test.go index d71668ac8..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) } @@ -43,7 +43,7 @@ func TestValidateAgainstInvalidSingleSchema(t *testing.T) { if err != nil { t.Fatalf("Error reading YAML file: %s", err) } - schema, err := ioutil.ReadFile("./testdata/test-values-invalid.schema.json") + schema, err := os.ReadFile("./testdata/test-values-invalid.schema.json") if err != nil { t.Fatalf("Error reading YAML file: %s", err) } @@ -67,7 +67,7 @@ func TestValidateAgainstSingleSchemaNegative(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) } diff --git a/pkg/chartutil/save_test.go b/pkg/chartutil/save_test.go index 6914cd200..db485c7cb 100644 --- a/pkg/chartutil/save_test.go +++ b/pkg/chartutil/save_test.go @@ -29,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{ @@ -210,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 }}")}, }, } @@ -227,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/three-level-dependent-chart/README.md b/pkg/chartutil/testdata/three-level-dependent-chart/README.md index a5fed642d..536bb9792 100644 --- a/pkg/chartutil/testdata/three-level-dependent-chart/README.md +++ b/pkg/chartutil/testdata/three-level-dependent-chart/README.md @@ -5,10 +5,10 @@ This chart is for testing the processing of multi-level dependencies. Consists of the following charts: - Library Chart -- App Chart (Uses Library Chart as dependecy, 2x: app1/app2) +- App Chart (Uses Library Chart as dependency, 2x: app1/app2) - Umbrella Chart (Has all the app charts as dependencies) -The precendence is as follows: `library < app < umbrella` +The precedence is as follows: `library < app < umbrella` Catches two use-cases: diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/Chart.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/Chart.yaml index 7552e07cd..e5dbe3131 100644 --- a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/Chart.yaml +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/Chart.yaml @@ -11,3 +11,9 @@ dependencies: - 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/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 index 94ee31855..de0bafa51 100644 --- a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/values.yaml +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/values.yaml @@ -6,3 +6,9 @@ app1: app2: enabled: true + +app3: + enabled: true + +app4: + enabled: true diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index 38b36b160..33b580046 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" @@ -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/values/options.go b/pkg/cli/values/options.go index 6f8b12600..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" @@ -31,11 +31,12 @@ import ( // Options captures the different ways to specify values type Options struct { - ValueFiles []string // -f/--values - StringValues []string // --set-string - Values []string // --set - FileValues []string // --set-file - JSONValues []string // --set-json + 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 @@ -94,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 } @@ -119,7 +127,7 @@ 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 { @@ -129,7 +137,7 @@ func readFile(filePath string, p getter.Providers) ([]byte, error) { // 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 { diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 29a9d64c2..a95894e00 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -184,11 +184,11 @@ func (c *ChartDownloader) getOciURI(ref, version string, u *url.URL) (*url.URL, // // 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 { diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go index 8ff780daf..131e21306 100644 --- a/pkg/downloader/chart_downloader_test.go +++ b/pkg/downloader/chart_downloader_test.go @@ -269,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 18b28dde1..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" @@ -249,7 +248,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { destPath := filepath.Join(m.ChartPath, "charts") tmpPath := filepath.Join(m.ChartPath, "tmpcharts") - // Check if 'charts' directory is not actally a directory. If it does not exist, create it. + // 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) @@ -668,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 { @@ -845,7 +845,7 @@ 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 destPath 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 657d5767b..150be16b7 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -391,6 +391,9 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil. newParentID := c.ChartFullPath() for _, t := range c.Templates { + if t == nil { + continue + } if !isTemplateValid(c, t.Name) { continue } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 434b939dc..27bb9e78e 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -22,6 +22,7 @@ import ( "strings" "sync" "testing" + "text/template" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" @@ -869,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/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 653b032fe..45ab4da7e 100644 --- a/pkg/getter/getter.go +++ b/pkg/getter/getter.go @@ -37,6 +37,7 @@ type options struct { caFile string unTar bool insecureSkipVerifyTLS bool + plainHTTP bool username string password string passCredentialsAll bool @@ -96,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) { @@ -172,9 +179,21 @@ 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{ diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index 081bf059e..b53e558e3 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -123,8 +123,8 @@ func (g *HTTPGetter) httpClient() (*http.Client, error) { } }) - if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" { - tlsConf, err := tlsutil.NewClientTLS(g.opts.certFile, g.opts.keyFile, g.opts.caFile) + 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") } diff --git a/pkg/getter/httpgetter_test.go b/pkg/getter/httpgetter_test.go index 94baef3b8..c727d0d7c 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -285,9 +285,10 @@ 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")) } @@ -434,10 +435,10 @@ func verifyInsecureSkipVerify(t *testing.T, g *HTTPGetter, caseName string, expe 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 @@ -458,11 +459,11 @@ func TestDefaultHTTPTransportReuse(t *testing.T) { t.Fatal(err) } - if httpClient1 == nil { + if httpClient1 == nil { //nolint:staticcheck t.Fatalf("Expected non nil value for http client") } - transport1 := (httpClient1.Transport).(*http.Transport) + transport1 := (httpClient1.Transport).(*http.Transport) //nolint:staticcheck httpClient2, err := g.httpClient() @@ -470,11 +471,11 @@ func TestDefaultHTTPTransportReuse(t *testing.T) { t.Fatal(err) } - if httpClient2 == nil { + if httpClient2 == nil { //nolint:staticcheck t.Fatalf("Expected non nil value for http client") } - transport2 := (httpClient2.Transport).(*http.Transport) + transport2 := (httpClient2.Transport).(*http.Transport) //nolint:staticcheck if transport1 != transport2 { t.Fatalf("Expected default transport to be reused") @@ -492,11 +493,11 @@ func TestHTTPTransportOption(t *testing.T) { t.Fatal(err) } - if httpClient1 == nil { + if httpClient1 == nil { //nolint:staticcheck t.Fatalf("Expected non nil value for http client") } - transport1 := (httpClient1.Transport).(*http.Transport) + transport1 := (httpClient1.Transport).(*http.Transport) //nolint:staticcheck if transport1 != transport { t.Fatalf("Expected transport option to be applied") @@ -508,11 +509,11 @@ func TestHTTPTransportOption(t *testing.T) { t.Fatal(err) } - if httpClient2 == nil { + if httpClient2 == nil { //nolint:staticcheck t.Fatalf("Expected non nil value for http client") } - transport2 := (httpClient2.Transport).(*http.Transport) + transport2 := (httpClient2.Transport).(*http.Transport) //nolint:staticcheck if transport1 != transport2 { t.Fatalf("Expected applied transport to be reused") diff --git a/pkg/getter/ocigetter.go b/pkg/getter/ocigetter.go index 14f5cb3ec..209786bd7 100644 --- a/pkg/getter/ocigetter.go +++ b/pkg/getter/ocigetter.go @@ -18,14 +18,22 @@ package getter import ( "bytes" "fmt" + "net" + "net/http" "strings" + "sync" + "time" + "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. @@ -38,6 +46,15 @@ func (g *OCIGetter) Get(href string, options ...Option) (*bytes.Buffer, error) { func (g *OCIGetter) get(href string) (*bytes.Buffer, error) { client := g.opts.registryClient + // 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 + } ref := strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme)) @@ -63,18 +80,7 @@ func (g *OCIGetter) get(href string) (*bytes.Buffer, error) { // NewOCIGetter constructs a valid http/https client as a Getter func NewOCIGetter(ops ...Option) (Getter, error) { - registryClient, err := registry.NewClient( - registry.ClientOptEnableCache(true), - ) - if err != nil { - return nil, err - } - - client := OCIGetter{ - opts: options{ - registryClient: registryClient, - }, - } + var client OCIGetter for _, opt := range ops { opt(&client.opts) @@ -82,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/kube/client.go b/pkg/kube/client.go index d30e5c535..0772678d1 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -37,6 +37,7 @@ 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" @@ -68,6 +69,14 @@ 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 @@ -337,13 +346,7 @@ func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) { validationDirective = metav1.FieldValidationStrict } - dynamicClient, err := c.Factory.DynamicClient() - if err != nil { - return nil, err - } - - verifier := resource.NewQueryParamVerifier(dynamicClient, c.Factory.OpenAPIGetter(), resource.QueryParamFieldValidation) - schema, err := c.Factory.Validator(validationDirective, verifier) + schema, err := c.Factory.Validator(validationDirective) if err != nil { return nil, err } @@ -363,13 +366,7 @@ func (c *Client) BuildTable(reader io.Reader, validate bool) (ResourceList, erro validationDirective = metav1.FieldValidationStrict } - dynamicClient, err := c.Factory.DynamicClient() - if err != nil { - return nil, err - } - - verifier := resource.NewQueryParamVerifier(dynamicClient, c.Factory.OpenAPIGetter(), resource.QueryParamFieldValidation) - schema, err := c.Factory.Validator(validationDirective, verifier) + schema, err := c.Factory.Validator(validationDirective) if err != nil { return nil, err } @@ -456,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 } @@ -465,17 +462,29 @@ 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) - err := deleteResource(info) + 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) @@ -492,10 +501,8 @@ func (c *Client) Delete(resources ResourceList) (*Result, []error) { 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) } @@ -532,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 } @@ -542,10 +551,11 @@ 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. @@ -593,8 +603,7 @@ func createResource(info *resource.Info) error { 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).WithFieldManager(getManagedFieldsManager()).DeleteWithOptions(info.Namespace, info.Name, opts) return err diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index c4823056b..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) } diff --git a/pkg/kube/factory.go b/pkg/kube/factory.go index fdba8cf8f..f19d62dc3 100644 --- a/pkg/kube/factory.go +++ b/pkg/kube/factory.go @@ -18,7 +18,6 @@ package kube // import "helm.sh/helm/v3/pkg/kube" import ( "k8s.io/cli-runtime/pkg/resource" - "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" @@ -27,6 +26,12 @@ 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 @@ -42,7 +47,5 @@ type Factory interface { NewBuilder() *resource.Builder // Returns a schema that can validate objects stored on disk. - Validator(validationDirective string, verifier *resource.QueryParamVerifier) (validation.Schema, error) - // OpenAPIGetter returns a getter for the openapi schema document - OpenAPIGetter() discovery.OpenAPISchemaInterface + Validator(validationDirective string) (validation.Schema, error) } diff --git a/pkg/kube/fake/fake.go b/pkg/kube/fake/fake.go index fb38c3654..267020d57 100644 --- a/pkg/kube/fake/fake.go +++ b/pkg/kube/fake/fake.go @@ -22,6 +22,7 @@ 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" @@ -37,10 +38,12 @@ type FailingKubeClient struct { 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 @@ -116,6 +119,9 @@ 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) } @@ -134,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 267aa1ff8..e6c4b6207 100644 --- a/pkg/kube/fake/printer.go +++ b/pkg/kube/fake/printer.go @@ -22,6 +22,7 @@ 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" @@ -115,6 +116,17 @@ func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Durati 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 11f948e34..ce42ed950 100644 --- a/pkg/kube/interface.go +++ b/pkg/kube/interface.go @@ -21,6 +21,7 @@ import ( "time" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -79,6 +80,14 @@ type InterfaceExt interface { 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. @@ -103,4 +112,5 @@ type InterfaceResources interface { 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 b214c4797..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.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 { @@ -393,8 +397,10 @@ 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 } - - if sts.Status.CurrentRevision != sts.Status.UpdateRevision { + // 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 } diff --git a/pkg/kube/ready_test.go b/pkg/kube/ready_test.go index 9fe20d8cb..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 @@ -189,6 +188,13 @@ func Test_ReadyChecker_statefulSetReady(t *testing.T) { }, 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) { @@ -263,44 +269,52 @@ 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, intToInt32(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, intToInt32(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, intToInt32(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, intToInt32(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, intToInt32(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, intToInt32(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, intToInt32(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", @@ -311,7 +325,12 @@ func Test_ReadyChecker_jobReady(t *testing.T) { 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) } }) 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 8928d6745..ecdd38940 100644 --- a/pkg/kube/wait.go +++ b/pkg/kube/wait.go @@ -50,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 { @@ -58,7 +58,7 @@ 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 @@ -68,7 +68,7 @@ func (w *waiter) waitForDeletedResources(deleted 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 deleted { err := v.Get() if err == nil || !apierrors.IsNotFound(err) { @@ -76,7 +76,7 @@ func (w *waiter) waitForDeletedResources(deleted ResourceList) error { } } return true, nil - }, ctx.Done()) + }) } // 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 5b08f85a5..88f79cf6b 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/lint/lint_test.go @@ -19,6 +19,7 @@ package lint import ( "strings" "testing" + "time" "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/lint/support" @@ -35,6 +36,7 @@ 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, skipSchemaValidation).Messages @@ -42,19 +44,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 +77,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) } } @@ -152,3 +149,26 @@ func TestSubChartValuesChart(t *testing.T) { } } } + +// 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/template.go b/pkg/lint/rules/template.go index b33cbc31c..314f82607 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -72,7 +72,7 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace // lint ignores import-values // See https://github.com/helm/helm/issues/9658 - if err := chartutil.ProcessDependencies(chart, values); err != nil { + if err := chartutil.ProcessDependenciesWithMerge(chart, values); err != nil { return } @@ -152,10 +152,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 @@ -198,10 +199,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 } diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index bb7d156ca..c776e001a 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" @@ -222,8 +221,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) @@ -279,8 +277,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) } @@ -409,8 +406,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) 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/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/http_installer_test.go b/pkg/plugin/installer/http_installer_test.go index 165007af0..177227c5b 100644 --- a/pkg/plugin/installer/http_installer_test.go +++ b/pkg/plugin/installer/http_installer_test.go @@ -79,7 +79,7 @@ func mockArchiveServer() *httptest.Server { } func TestHTTPInstaller(t *testing.T) { - defer ensure.HelmHome(t)() + ensure.HelmHome(t) srv := mockArchiveServer() defer srv.Close() @@ -128,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" @@ -164,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) diff --git a/pkg/plugin/installer/local_installer_test.go b/pkg/plugin/installer/local_installer_test.go index 710a24537..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" @@ -29,7 +28,7 @@ var _ Installer = new(LocalInstaller) func TestLocalInstaller(t *testing.T) { // Make a temp dir tdir := t.TempDir() - if err := ioutil.WriteFile(filepath.Join(tdir, "plugin.yaml"), []byte{}, 0644); err != nil { + if err := os.WriteFile(filepath.Join(tdir, "plugin.yaml"), []byte{}, 0644); err != nil { t.Fatal(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 3b44a6eb5..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"}, }, }, } @@ -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/postrender/exec_test.go b/pkg/postrender/exec_test.go index 9788ed56e..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,8 +25,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "helm.sh/helm/v3/internal/test/ensure" ) const testingScript = `#!/bin/sh @@ -41,8 +38,7 @@ 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) @@ -50,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) @@ -63,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)) @@ -117,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) @@ -134,8 +127,7 @@ func TestNewExecWithOneArgsRun(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, "ARG1") require.NoError(t, err) @@ -151,8 +143,7 @@ func TestNewExecWithTwoArgsRun(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, "ARG1", "ARG2") require.NoError(t, err) @@ -162,12 +153,12 @@ func TestNewExecWithTwoArgsRun(t *testing.T) { is.Contains(output.String(), "ARG1 ARG2") } -func setupTestingScript(t *testing.T) (filepath string, cleanup func()) { +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) } @@ -187,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 c41f90c61..7f89ef3f5 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -20,7 +20,6 @@ import ( "crypto" "encoding/hex" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -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 { @@ -293,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 93c169263..17f727ea7 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -19,7 +19,6 @@ import ( "crypto" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -277,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) } @@ -334,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/ocipusher.go b/pkg/pusher/ocipusher.go index 7c90e85a4..94154d389 100644 --- a/pkg/pusher/ocipusher.go +++ b/pkg/pusher/ocipusher.go @@ -17,13 +17,16 @@ package pusher import ( "fmt" - "io/ioutil" + "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" ) @@ -59,8 +62,15 @@ func (pusher *OCIPusher) push(chartRef, href string) error { } client := pusher.opts.registryClient + if client == nil { + c, err := pusher.newRegistryClient() + if err != nil { + return err + } + client = c + } - chartBytes, err := ioutil.ReadFile(chartRef) + chartBytes, err := os.ReadFile(chartRef) if err != nil { return err } @@ -68,7 +78,7 @@ func (pusher *OCIPusher) push(chartRef, href string) error { var pushOpts []registry.PushOption provRef := fmt.Sprintf("%s.prov", chartRef) if _, err := os.Stat(provRef); err == nil { - provBytes, err := ioutil.ReadFile(provRef) + provBytes, err := os.ReadFile(provRef) if err != nil { return err } @@ -85,18 +95,7 @@ func (pusher *OCIPusher) push(chartRef, href string) error { // NewOCIPusher constructs a valid OCI client as a Pusher func NewOCIPusher(ops ...Option) (Pusher, error) { - registryClient, err := registry.NewClient( - registry.ClientOptEnableCache(true), - ) - if err != nil { - return nil, err - } - - client := OCIPusher{ - opts: options{ - registryClient: registryClient, - }, - } + var client OCIPusher for _, opt := range ops { opt(&client.opts) @@ -104,3 +103,50 @@ func NewOCIPusher(ops ...Option) (Pusher, error) { 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 index 27be15b5d..11842b4ae 100644 --- a/pkg/pusher/ocipusher_test.go +++ b/pkg/pusher/ocipusher_test.go @@ -16,21 +16,81 @@ limitations under the License. package pusher import ( + "path/filepath" "testing" + + "helm.sh/helm/v3/pkg/registry" ) func TestNewOCIPusher(t *testing.T) { - testfn := func(ops *options) { - if ops.registryClient == nil { - t.Fatalf("the OCIPusher's registryClient should not be null") - } + p, err := NewOCIPusher() + if err != nil { + t.Fatal(err) } - p, err := NewOCIPusher(testfn) - if p == nil { - t.Error("NewOCIPusher returned nil") + 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.Error(err) + 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 index 30c6af97c..c99d97b35 100644 --- a/pkg/pusher/pusher.go +++ b/pkg/pusher/pusher.go @@ -27,7 +27,12 @@ import ( // // Pushers may or may not ignore these parameters as they are passed in. type options struct { - registryClient *registry.Client + 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 @@ -41,6 +46,28 @@ func WithRegistryClient(client *registry.Client) Option { } } +// 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 diff --git a/pkg/registry/client.go b/pkg/registry/client.go index c1004f956..829f6b494 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -21,7 +21,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "sort" "strings" @@ -60,7 +59,9 @@ type ( out io.Writer authorizer auth.Client registryAuthorizer *registryauth.Client - resolver remotes.Resolver + 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 @@ -71,7 +72,7 @@ type ( // NewClient returns a new registry client with config func NewClient(options ...ClientOption) (*Client, error) { client := &Client{ - out: ioutil.Discard, + out: io.Discard, } for _, option := range options { option(client) @@ -86,15 +87,39 @@ func NewClient(options ...ClientOption) (*Client, error) { } client.authorizer = authClient } - if client.resolver == nil { + + 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 } - client.resolver = resolver + return resolver, nil } // allocate a cache if option is set @@ -104,6 +129,7 @@ func NewClient(options ...ClientOption) (*Client, error) { } if client.registryAuthorizer == nil { client.registryAuthorizer = ®istryauth.Client{ + Client: client.httpClient, Header: http.Header{ "User-Agent": {version.GetUserAgent()}, }, @@ -113,7 +139,6 @@ func NewClient(options ...ClientOption) (*Client, error) { 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") @@ -166,6 +191,28 @@ func ClientOptCredentialsFile(credentialsFile string) ClientOption { } } +// 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) @@ -174,6 +221,9 @@ type ( username string password string insecure bool + certFile string + keyFile string + caFile string } ) @@ -189,6 +239,7 @@ func (c *Client) Login(host string, options ...LoginOption) error { 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()) @@ -215,6 +266,15 @@ func LoginOptInsecure(insecure bool) LoginOption { } } +// 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) @@ -241,21 +301,21 @@ type ( // 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"` + Manifest *DescriptorPullSummary `json:"manifest"` + Config *DescriptorPullSummary `json:"config"` + Chart *DescriptorPullSummaryWithMeta `json:"chart"` + Prov *DescriptorPullSummary `json:"prov"` Ref string `json:"ref"` } - descriptorPullSummary struct { + DescriptorPullSummary struct { Data []byte `json:"-"` Digest string `json:"digest"` Size int64 `json:"size"` } - descriptorPullSummaryWithMeta struct { - descriptorPullSummary + DescriptorPullSummaryWithMeta struct { + DescriptorPullSummary Meta *chart.Metadata `json:"meta"` } @@ -300,7 +360,11 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { } var descriptors, layers []ocispec.Descriptor - registryStore := content.Registry{Resolver: c.resolver} + 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(), @@ -354,16 +418,16 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { } } result := &PullResult{ - Manifest: &descriptorPullSummary{ + Manifest: &DescriptorPullSummary{ Digest: manifest.Digest.String(), Size: manifest.Size, }, - Config: &descriptorPullSummary{ + Config: &DescriptorPullSummary{ Digest: configDescriptor.Digest.String(), Size: configDescriptor.Size, }, - Chart: &descriptorPullSummaryWithMeta{}, - Prov: &descriptorPullSummary{}, + Chart: &DescriptorPullSummaryWithMeta{}, + Prov: &DescriptorPullSummary{}, Ref: parsedRef.String(), } var getManifestErr error @@ -474,6 +538,7 @@ type ( pushOperation struct { provData []byte strictMode bool + test bool } ) @@ -527,7 +592,9 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu descriptors = append(descriptors, provDescriptor) } - manifestData, manifest, err := content.GenerateManifest(&configDescriptor, nil, descriptors...) + ociAnnotations := generateOCIAnnotations(meta, operation.test) + + manifestData, manifest, err := content.GenerateManifest(&configDescriptor, ociAnnotations, descriptors...) if err != nil { return nil, err } @@ -535,8 +602,11 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu if err := memoryStore.StoreManifest(parsedRef.String(), manifest, manifestData); err != nil { return nil, err } - - registryStore := content.Registry{Resolver: c.resolver} + 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 { @@ -590,6 +660,13 @@ func PushOptStrictMode(strictMode bool) PushOption { } } +// 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) @@ -600,23 +677,14 @@ func (c *Client) Tags(ref string) ([]string, error) { repository := registryremote.Repository{ Reference: parsedReference, Client: c.registryAuthorizer, + PlainHTTP: c.plainHTTP, } var registryTags []string - for { - registryTags, err = registry.Tags(ctx(c.out, c.debug), &repository) - if err != nil { - // Fallback to http based request - if !repository.PlainHTTP && strings.Contains(err.Error(), "server gave HTTP response") { - repository.PlainHTTP = true - continue - } - return nil, err - } - - break - + registryTags, err = registry.Tags(ctx(c.out, c.debug), &repository) + if err != nil { + return nil, err } var tagVersions []*semver.Version 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/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 index 47eed267f..6fb1d0cda 100644 --- a/pkg/registry/util.go +++ b/pkg/registry/util.go @@ -19,20 +19,32 @@ 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)) @@ -129,3 +141,137 @@ func parseReference(raw string) (registry.Reference, error) { 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/client_test.go b/pkg/registry/utils_test.go similarity index 73% rename from pkg/registry/client_test.go rename to pkg/registry/utils_test.go index 138dd4245..74aa0dbc0 100644 --- a/pkg/registry/client_test.go +++ b/pkg/registry/utils_test.go @@ -19,26 +19,36 @@ package registry import ( "bytes" "context" + "crypto/tls" "fmt" "io" - "io/ioutil" + "net" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strings" - "testing" "time" - "github.com/containerd/containerd/errdefs" "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 ( @@ -48,145 +58,218 @@ var ( testPassword = "mypass" ) -type RegistryClientTestSuite struct { +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 (suite *RegistryClientTestSuite) SetupSuite() { +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 + var ( + out bytes.Buffer + err error + ) suite.Out = &out credentialsFile := filepath.Join(suite.WorkspaceDir, CredentialsFileBasename) // init test client - var err error - suite.RegistryClient, err = NewClient( + 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 = 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) 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("127.0.0.1:%d", port) + + // 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{}{}} - config.Auth = configuration.Auth{ - "htpasswd": configuration.Parameters{ - "realm": "localhost", - "path": htpasswdPath, - }, + + // 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() - - // Start Docker registry - go dockerRegistry.ListenAndServe() + return dockerRegistry } -func (suite *RegistryClientTestSuite) TearDownSuite() { - os.RemoveAll(suite.WorkspaceDir) +func teardown(suite *TestSuite) { + if suite.srv != nil { + mockdns.UnpatchNet(net.DefaultResolver) + suite.srv.Close() + } } -func (suite *RegistryClientTestSuite) Test_0_Login() { - err := suite.RegistryClient.Login(suite.DockerRegistryHost, - LoginOptBasicAuth("badverybad", "ohsobad"), - LoginOptInsecure(false)) - suite.NotNil(err, "error logging into registry with bad credentials") - - err = suite.RegistryClient.Login(suite.DockerRegistryHost, - LoginOptBasicAuth("badverybad", "ohsobad"), - LoginOptInsecure(true)) - suite.NotNil(err, "error logging into registry with bad credentials, insecure mode") - - err = suite.RegistryClient.Login(suite.DockerRegistryHost, - LoginOptBasicAuth(testUsername, testPassword), - LoginOptInsecure(false)) - suite.Nil(err, "no error logging into registry with good credentials") - - err = suite.RegistryClient.Login(suite.DockerRegistryHost, - LoginOptBasicAuth(testUsername, testPassword), - LoginOptInsecure(true)) - suite.Nil(err, "no error logging into registry with good credentials, insecure mode") +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 (suite *RegistryClientTestSuite) Test_1_Push() { +func testPush(suite *TestSuite) { // Bad bytes ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost) - _, err := suite.RegistryClient.Push([]byte("hello"), ref) + _, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptTest(true)) suite.NotNil(err, "error pushing non-chart bytes") // Load a test chart - chartData, err := ioutil.ReadFile("../repo/repotest/testdata/examplechart-0.1.0.tgz") + 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) + _, 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)) + _, 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) + _, 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)) + _, 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 = ioutil.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") + 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) + _, 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 = ioutil.ReadFile("../downloader/testdata/signtest-0.1.0.tgz") + 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 := ioutil.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov") + 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)) + 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) @@ -198,12 +281,12 @@ func (suite *RegistryClientTestSuite) Test_1_Push() { suite.Equal(ref, result.Ref) suite.Equal(meta.Name, result.Chart.Meta.Name) suite.Equal(meta.Version, result.Chart.Meta.Version) - suite.Equal(int64(512), result.Manifest.Size) + 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:af4c20a1df1431495e673c14ecfa3a2ba24839a7784349d6787cd67957392e83", + "sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6", result.Manifest.Digest) suite.Equal( "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580", @@ -216,14 +299,14 @@ func (suite *RegistryClientTestSuite) Test_1_Push() { result.Prov.Digest) } -func (suite *RegistryClientTestSuite) Test_2_Pull() { +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 := ioutil.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") + 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") @@ -245,14 +328,14 @@ func (suite *RegistryClientTestSuite) Test_2_Pull() { "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 = ioutil.ReadFile("../downloader/testdata/signtest-0.1.0.tgz") + 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 := ioutil.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov") + 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 @@ -271,12 +354,12 @@ func (suite *RegistryClientTestSuite) Test_2_Pull() { suite.Equal(ref, result.Ref) suite.Equal(meta.Name, result.Chart.Meta.Name) suite.Equal(meta.Version, result.Chart.Meta.Version) - suite.Equal(int64(512), result.Manifest.Size) + 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:af4c20a1df1431495e673c14ecfa3a2ba24839a7784349d6787cd67957392e83", + "sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6", result.Manifest.Digest) suite.Equal( "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580", @@ -287,7 +370,7 @@ func (suite *RegistryClientTestSuite) Test_2_Pull() { 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}]}", + 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)) @@ -295,10 +378,9 @@ func (suite *RegistryClientTestSuite) Test_2_Pull() { suite.Equal(provData, result.Prov.Data) } -func (suite *RegistryClientTestSuite) Test_3_Tags() { - +func testTags(suite *TestSuite) { // Load test chart (to build ref pushed in previous test) - chartData, err := ioutil.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") + 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") @@ -308,66 +390,4 @@ func (suite *RegistryClientTestSuite) Test_3_Tags() { tags, err := suite.RegistryClient.Tags(ref) suite.Nil(err, "no error retrieving tags") suite.Equal(1, len(tags)) - -} - -func (suite *RegistryClientTestSuite) 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 (suite *RegistryClientTestSuite) Test_5_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 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( - 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()) } diff --git a/pkg/releaseutil/kind_sorter.go b/pkg/releaseutil/kind_sorter.go index 1d1874cfa..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", @@ -105,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 afcae6d16..9e24c4399 100644 --- a/pkg/releaseutil/kind_sorter_test.go +++ b/pkg/releaseutil/kind_sorter_test.go @@ -169,6 +169,10 @@ func TestKindSorter(t *testing.T) { Name: "x", Head: &SimpleHead{Kind: "HorizontalPodAutoscaler"}, }, + { + Name: "F", + Head: &SimpleHead{Kind: "PriorityClass"}, + }, } for _, test := range []struct { @@ -176,8 +180,8 @@ func TestKindSorter(t *testing.T) { order KindSortOrder expected string }{ - {"install", InstallOrder, "aAbcC3deEf1gh2iIjJkKlLmnopqrxstuUvw!"}, - {"uninstall", UninstallOrder, "wvUmutsxrqponLlKkJjIi2hg1fEed3CcbAa!"}, + {"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 3901ad1b0..d9022ee6e 100644 --- a/pkg/repo/chartrepo.go +++ b/pkg/repo/chartrepo.go @@ -21,7 +21,7 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io/ioutil" + "io" "log" "net/url" "os" @@ -131,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 } @@ -148,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. @@ -170,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 { diff --git a/pkg/repo/chartrepo_test.go b/pkg/repo/chartrepo_test.go index 790c16203..4d4395c2d 100644 --- a/pkg/repo/chartrepo_test.go +++ b/pkg/repo/chartrepo_test.go @@ -18,7 +18,6 @@ package repo import ( "bytes" - "io/ioutil" "net/http" "net/http/httptest" "os" @@ -31,7 +30,6 @@ import ( "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" @@ -148,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) } @@ -266,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 } @@ -281,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 } @@ -350,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 { 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 60cfe5801..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" @@ -104,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 } @@ -233,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. @@ -337,7 +349,7 @@ func loadIndex(data []byte, source string) (*IndexFile, error) { return i, ErrEmptyIndexYaml } - if err := yaml.UnmarshalStrict(data, i); err != nil { + if err := jsonOrYamlUnmarshal(data, i); err != nil { return i, err } @@ -362,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 2403e9a71..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 @@ -85,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) @@ -123,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) { @@ -139,6 +147,10 @@ func TestLoadIndex(t *testing.T) { Name: "chartmuseum index file", Filename: chartmuseumtestfile, }, + { + Name: "JSON index file", + Filename: jsonTestfile, + }, } for _, tc := range tests { @@ -273,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) } @@ -282,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) } @@ -326,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) } @@ -533,10 +545,31 @@ func TestIndexWrite(t *testing.T) { testpath := filepath.Join(dir, "test") i.WriteFile(testpath, 0600) - got, err := ioutil.ReadFile(testpath) + got, err := os.ReadFile(testpath) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(got), "clipper-0.1.0.tgz") { + t.Fatal("Index files doesn't contain expected content") + } +} + +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.WriteJSONFile(testpath, 0600) + + 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") } diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index ee80d04f4..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) } @@ -122,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 7080a7cef..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) } 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 90ad3d856..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" @@ -102,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") } @@ -194,7 +193,7 @@ func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) { } // load it into memory... - contentBytes, err := ioutil.ReadFile(absPath) + contentBytes, err := os.ReadFile(absPath) if err != nil { t.Fatal("could not load chart into memory") } @@ -222,7 +221,7 @@ func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) { // load it into memory... absPath = filepath.Join(srv.Dir, fmt.Sprintf("%s-%s.tgz", c.Metadata.Name, c.Metadata.Version)) - contentBytes, err = ioutil.ReadFile(absPath) + contentBytes, err = os.ReadFile(absPath) if err != nil { t.Fatal("could not load chart into memory") } @@ -249,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 } @@ -317,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 @@ -345,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() { @@ -360,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 { @@ -367,7 +367,7 @@ 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) } @@ -385,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) } } @@ -400,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 @@ -421,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 b5908e508..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) { @@ -69,7 +71,7 @@ func decodeRelease(data string) (*rspb.Release, error) { 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/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 c59925522..2828f20c0 100644 --- a/pkg/strvals/parser.go +++ b/pkg/strvals/parser.go @@ -20,7 +20,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "strconv" "strings" "unicode" @@ -110,7 +109,6 @@ func ParseIntoString(s string, dest map[string]interface{}) error { // 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) @@ -232,7 +230,7 @@ func (t *parser) key(data map[string]interface{}, nestedNameLevel int) (reterr e return err } set(data, string(k), jsonval) - if _, err = io.CopyN(ioutil.Discard, t.sc, dec.InputOffset()); err != nil { + if _, err = io.CopyN(io.Discard, t.sc, dec.InputOffset()); err != nil { return err } // skip possible blanks and comma @@ -366,7 +364,7 @@ func (t *parser) listItem(list []interface{}, i, nestedNameLevel int) ([]interfa if list, err = setIndex(list, i, jsonval); err != nil { return list, err } - if _, err = io.CopyN(ioutil.Discard, t.sc, dec.InputOffset()); err != nil { + if _, err = io.CopyN(io.Discard, t.sc, dec.InputOffset()); err != nil { return list, err } // skip possible blanks and comma diff --git a/pkg/uploader/doc.go b/pkg/uploader/doc.go index 45eacbbf5..112ddbf2c 100644 --- a/pkg/uploader/doc.go +++ b/pkg/uploader/doc.go @@ -13,7 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package uploader provides a library for uploading charts. +/* +Package uploader provides a library for uploading charts. This package contains tools for uploading charts to registries. */ diff --git a/scripts/coverage.sh b/scripts/coverage.sh index dcdc80735..2d8258866 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -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 fce6abd99..594fd92c3 100755 --- a/scripts/get +++ b/scripts/get @@ -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 6177ba1a2..b5e53bafc 100755 --- a/scripts/get-helm-3 +++ b/scripts/get-helm-3 @@ -108,11 +108,17 @@ verifySupported() { 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]*\"' | sed -E 's/.*\/helm\/helm\/releases\/tag\/(v[0-9\.]+)".*/\1/g' | head -1) + 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]*\"' | sed -E 's/.*\/helm\/helm\/releases\/tag\/(v[0-9\.]+)".*/\1/g' | head -1) + 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 @@ -299,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