Merge branch 'helm:main' into cleanup

pull/10493/head
Denis O 7 months ago committed by GitHub
commit acfa5a581f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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

@ -1,43 +0,0 @@
---
version: 2
jobs:
build:
working_directory: ~/helm.sh/helm
docker:
- image: circleci/golang:1.17
auth:
username: $DOCKER_USER
password: $DOCKER_PASS
environment:
GOCACHE: "/tmp/go/cache"
GOLANGCI_LINT_VERSION: "1.36.0"
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: /.*/

@ -1,49 +0,0 @@
#!/usr/bin/env bash
# Copyright The Helm Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
# Skip on pull request builds
if [[ -n "${CIRCLE_PR_NUMBER:-}" ]]; then
exit
fi
: ${AZURE_STORAGE_CONNECTION_STRING:?"AZURE_STORAGE_CONNECTION_STRING environment variable is not set"}
: ${AZURE_STORAGE_CONTAINER_NAME:?"AZURE_STORAGE_CONTAINER_NAME environment variable is not set"}
VERSION=
if [[ -n "${CIRCLE_TAG:-}" ]]; then
VERSION="${CIRCLE_TAG}"
elif [[ "${CIRCLE_BRANCH:-}" == "main" ]]; then
VERSION="canary"
else
echo "Skipping deploy step; this is neither a releasable branch or a tag"
exit
fi
echo "Installing Azure CLI"
echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ stretch main" | sudo tee /etc/apt/sources.list.d/azure-cli.list
curl -L https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add
sudo apt install apt-transport-https
sudo apt update
sudo apt install azure-cli
echo "Building helm binaries"
make build-cross
make dist checksum VERSION="${VERSION}"
echo "Pushing binaries to Azure"
az storage blob upload-batch -s _dist/ -d "$AZURE_STORAGE_CONTAINER_NAME" --pattern 'helm-*' --connection-string "$AZURE_STORAGE_CONNECTION_STRING"

@ -1,7 +1,39 @@
version: 2 version: 2
updates: updates:
- # Keep dev-v3 branch dependencies up to date, while Helm v3 is within support
package-ecosystem: "gomod"
target-branch: "dev-v3"
directory: "/"
schedule:
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: "gomod" - package-ecosystem: "gomod"
target-branch: "main"
directory: "/"
schedule:
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"
target-branch: "main"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "daily"

@ -7,6 +7,6 @@
**Special notes for your reviewer**: **Special notes for your reviewer**:
**If applicable**: **If applicable**:
- [ ] this PR contains documentation - [ ] this PR contains user facing changes (the `docs needed` label should be applied if so)
- [ ] this PR contains unit tests - [ ] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility - [ ] this PR has been tested for backwards compatibility

@ -1,29 +0,0 @@
name: build-pr
on:
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: '1.17'
- name: Install golangci-lint
run: |
curl -sSLO https://github.com/golangci/golangci-lint/releases/download/v$GOLANGCI_LINT_VERSION/golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz
shasum -a 256 golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz | grep "^$GOLANGCI_LINT_SHA256 " > /dev/null
tar -xf golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz
sudo mv golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64/golangci-lint /usr/local/bin/golangci-lint
rm -rf golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64*
env:
GOLANGCI_LINT_VERSION: '1.43.0'
GOLANGCI_LINT_SHA256: 'f3515cebec926257da703ba0a2b169e4a322c11dc31a8b4656b50a43e48877f4'
- name: Test style
run: make test-style
- name: Run unit tests
run: make test-coverage

@ -0,0 +1,32 @@
name: build-test
on:
push:
branches:
- "main"
- "dev-v3"
- "release-**"
pull_request:
branches:
- "main"
- "dev-v3"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # pin@5.3.0
with:
go-version: '1.23'
check-latest: true
- name: Test source headers are present
run: make test-source-headers
- name: Run unit tests
run: make test-coverage
- name: Test build
run: make build

@ -13,13 +13,21 @@ name: "CodeQL"
on: on:
push: push:
branches: [ main ] branches:
- main
- dev-v3
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ main ] branches:
- main
- dev-v3
schedule: schedule:
- cron: '29 6 * * 6' - cron: '29 6 * * 6'
permissions:
contents: read
security-events: write
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
@ -35,11 +43,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # pinv3.26.6
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -50,7 +58,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@4dd16135b69a43b6c8efb853346f8437d92d3c93 # pinv3.26.6
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -64,4 +72,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # pinv3.26.6

@ -0,0 +1,26 @@
name: golangci-lint
on:
push:
pull_request:
permissions:
contents: read
jobs:
golangci:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # pin@5.3.0
with:
go-version: '1.23'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 #pin@6.5.0
with:
version: v1.62

@ -0,0 +1,24 @@
name: govulncheck
on:
push:
paths:
- go.sum
schedule:
- cron: "0 0 * * *"
permissions: read-all
jobs:
govulncheck:
name: govulncheck
runs-on: ubuntu-latest
steps:
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # pin@5.3.0
with:
go-version: '1.23'
check-latest: true
- name: govulncheck
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # pin@1.0.4
with:
go-package: ./...

@ -0,0 +1,105 @@
name: release
on:
create:
tags:
- v*
push:
branches:
- main
permissions: read-all
# 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-16-cores
steps:
- name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # pin@5.3.0
with:
go-version: '1.23'
- name: Run unit tests
run: make test-coverage
- name: Build Helm Binaries
run: |
set -eu -o pipefail
make build-cross VERSION="${{ github.ref_name }}"
make dist checksum VERSION="${{ github.ref_name }}"
- name: Set latest version
run: |
set -eu -o pipefail
mkdir -p _dist_versions
# Push the latest semver tag, excluding prerelease tags
LATEST_VERSION="$(git tag | sort -r --version-sort | grep '^v[0-9]' | grep -v '-' | head -n1)"
echo "LATEST_VERSION=${LATEST_VERSION}"
echo "${LATEST_VERSION}" > _dist_versions/helm-latest-version
echo "${LATEST_VERSION}" > _dist_versions/helm3-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-*'
- name: Upload Version tag files
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:
overwrite: 'true'
source_dir: _dist_versions
container_name: ${{ secrets.AZURE_STORAGE_CONTAINER_NAME }}
connection_string: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}
canary-release:
runs-on: ubuntu-latest-16-cores
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # pin@5.3.0
with:
go-version: '1.23'
check-latest: true
- 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'

@ -0,0 +1,69 @@
name: Scorecard supply-chain security
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '25 7 * * 0'
push:
branches: [ "main" ]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results and get a badge (see publish_results below).
id-token: write
# Uncomment the permissions below if installing in a private repository.
# contents: read
# actions: read
steps:
- name: "Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
with:
results_file: results.sarif
results_format: sarif
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecard on a *private* repository
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
# Public repositories:
# - Publish results to OpenSSF REST API for easy access by consumers
# - Allows the repository to include the Scorecard badge.
# - See https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories:
# - `publish_results` will always be set to `false`, regardless
# of the value entered here.
publish_results: true
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif

@ -2,15 +2,18 @@ name: "Close stale issues"
on: on:
schedule: schedule:
- cron: "0 0 * * *" - cron: "0 0 * * *"
permissions:
contents: read
jobs: jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v3.0.14 - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.' stale-issue-message: 'This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.'
exempt-issue-labels: 'keep open,v4.x' exempt-issue-labels: 'keep open,v4.x,in progress'
days-before-stale: 90 days-before-stale: 90
days-before-close: 30 days-before-close: 30
operations-per-run: 100 operations-per-run: 100

3
.gitignore vendored

@ -5,8 +5,11 @@
.idea/ .idea/
.vimrc .vimrc
.vscode/ .vscode/
.devcontainer/
_dist/ _dist/
_dist_versions/
bin/ bin/
vendor/ vendor/
# Ignores charts pulled for dependency build tests # Ignores charts pulled for dependency build tests
cmd/helm/testdata/testcharts/issue-7233/charts/* cmd/helm/testdata/testcharts/issue-7233/charts/*
.pre-commit-config.yaml

@ -4,25 +4,42 @@ run:
linters: linters:
disable-all: true disable-all: true
enable: enable:
- deadcode
- dupl - dupl
- gofmt - gofmt
- goimports - goimports
- golint
- gosimple - gosimple
- govet - govet
- ineffassign - ineffassign
- misspell - misspell
- nakedret - nakedret
- structcheck - revive
- unused - unused
- varcheck
- staticcheck - staticcheck
linters-settings: linters-settings:
gofmt: gofmt:
simplify: true simplify: true
goimports: goimports:
local-prefixes: helm.sh/helm/v3 local-prefixes: helm.sh/helm/v4
dupl: dupl:
threshold: 400 threshold: 400
issues:
exclude-rules:
# Helm, and the Go source code itself, sometimes uses these names outside their built-in
# functions. As the Go source code has re-used these names it's ok for Helm to do the same.
# Linting will look for redefinition of built-in id's but we opt-in to the ones we choose to use.
- linters:
- revive
text: "redefines-builtin-id: redefinition of the built-in function append"
- linters:
- revive
text: "redefines-builtin-id: redefinition of the built-in function clear"
- linters:
- revive
text: "redefines-builtin-id: redefinition of the built-in function max"
- linters:
- revive
text: "redefines-builtin-id: redefinition of the built-in function min"
- linters:
- revive
text: "redefines-builtin-id: redefinition of the built-in function new"

@ -5,12 +5,21 @@
# Organizations Using Helm # Organizations Using Helm
- [Blood Orange](https://bloodorange.io) - [IBM](https://www.ibm.com)
- [IBM](https://www.ibm.com) - [InfoCert](https://www.infocert.it/)
- [Microsoft](https://microsoft.com) - [Intercept](https://Intercept.cloud)
- [Qovery](https://www.qovery.com/) - [Microsoft](https://microsoft.com)
- [Samsung SDS](https://www.samsungsds.com/) - [New Relic](https://www.newrelic.com)
- [Softonic](https://hello.softonic.com/) - [Octopus Deploy](https://octopus.com/)
- [Ville de Montreal](https://montreal.ca) - [Omnistrate](https://omnistrate.com)
- [Oracle](www.oracle.com)
- [Percona](https://www.percona.com)
- [Qovery](https://www.qovery.com/)
- [Samsung SDS](https://www.samsungsds.com/)
- [Softonic](https://hello.softonic.com/)
- [SyncTune](https://mb-consulting.dev)
- [Syself](https://syself.com)
- [Ville de Montreal](https://montreal.ca)
_This file is part of the CNCF official documentation for projects._ _This file is part of the CNCF official documentation for projects._

@ -11,9 +11,13 @@ vulnerability_, please email a report to
[cncf-helm-security@lists.cncf.io](mailto:cncf-helm-security@lists.cncf.io). This will give us a [cncf-helm-security@lists.cncf.io](mailto:cncf-helm-security@lists.cncf.io). This will give us a
chance to try to fix the issue before it is exploited in the wild. chance to try to fix the issue before it is exploited in the wild.
## Helm v3 and v4
Helm v4 is currently under development on the `main` branch. During the development of Helm v4 and for some time after its released, Helm v3 will continue to be supported and developed on the `dev-v3` branch. Helm v3 will continue to get bug fixes and updates for new Kubernetes releases. Helm v4 is where new features and major changes will happen. For features to be backported to Helm v3, an exception will be needed. Bugs should first be fixed on Helm v4 and then backported to Helm v3.
## Sign Your Work ## Sign Your Work
The sign-off is a simple line at the end of the explanation for a commit. All commits needs to be The sign-off is a simple line at the end of the explanation for a commit. All commits need to be
signed. Your signature certifies that you wrote the patch or otherwise have the right to contribute signed. Your signature certifies that you wrote the patch or otherwise have the right to contribute
the material. The rules are pretty simple, if you can certify the below (from the material. The rules are pretty simple, if you can certify the below (from
[developercertificate.org](https://developercertificate.org/)): [developercertificate.org](https://developercertificate.org/)):
@ -66,6 +70,18 @@ Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your commit automatically If you set your `user.name` and `user.email` git configs, you can sign your commit automatically
with `git commit -s`. with `git commit -s`.
The following command will update your git config with `user.email`:
``` bash
git config --global user.email joe.smith@example.com
```
This command will update your git config with `user.name`:
``` bash
git config --global user.name "Joe Smith"
```
Note: If your git config information is set properly then viewing the `git log` information for your Note: If your git config information is set properly then viewing the `git log` information for your
commit will look something like this: commit will look something like this:
@ -115,8 +131,9 @@ Helm maintains a strong commitment to backward compatibility. All of our changes
formats are backward compatible from one major release to the next. No features, flags, or commands formats are backward compatible from one major release to the next. No features, flags, or commands
are removed or substantially modified (unless we need to fix a security issue). are removed or substantially modified (unless we need to fix a security issue).
We also try very hard to not change publicly accessible Go library definitions inside of the `pkg/` We also remain committed to not changing publicly accessible Go library definitions inside of the `pkg/` directory of our source code in a non-backwards-compatible way.
directory of our source code.
For more details on Helms minor and patch release backwards-compatibility rules, please read [HIP-0004](https://github.com/helm/community/blob/main/hips/hip-0004.md)
For a quick summary of our backward compatibility guidelines for releases between 3.0 and 4.0: For a quick summary of our backward compatibility guidelines for releases between 3.0 and 4.0:
@ -126,30 +143,9 @@ For a quick summary of our backward compatibility guidelines for releases betwee
(barring the cases where (a) Kubernetes itself changed, and (b) the chart worked because it (barring the cases where (a) Kubernetes itself changed, and (b) the chart worked because it
exploited a bug) exploited a bug)
- Chart repository functionality MUST be backward compatible - Chart repository functionality MUST be backward compatible
- Go libraries inside of `pkg/` SHOULD remain backward compatible, though code inside of `cmd/` and - Go libraries inside of `pkg/` MUST remain backward compatible, though code inside of `cmd/` and
`internal/` may be changed from release to release without notice. `internal/` may be changed from release to release without notice.
## Support Contract for Helm 2
With Helm 2's current release schedule, we want to take into account any migration issues for users
due to the upcoming holiday shopping season and tax season. We also want to clarify what actions may
occur after the support contract ends for Helm 2, so that users will not be surprised or caught off
guard.
After Helm 2.15.0 is released, Helm 2 will go into "maintenance mode". We will continue to accept
bug fixes and fix any security issues that arise, but no new features will be accepted for Helm 2.
All feature development will be moved over to Helm 3.
6 months after Helm 3.0.0's public release, Helm 2 will stop accepting bug fixes. Only security
issues will be accepted.
12 months after Helm 3.0.0's public release, support for Helm 2 will formally end. Download links
for the Helm 2 client through Google Cloud Storage, the Docker image for Tiller stored in Google
Container Registry, and the Google Cloud buckets for the stable and incubator chart repositories may
no longer work at any point. Client downloads through `get.helm.sh` will continue to work, and we
will distribute a Tiller image that will be made available at an alternative location which can be
updated with `helm init --tiller-image`.
## Issues ## Issues
Issues are used as the primary method for tracking anything to do with the Helm project. Issues are used as the primary method for tracking anything to do with the Helm project.
@ -195,7 +191,7 @@ below.
See [Proposing an Idea](#proposing-an-idea). Smaller quality-of-life enhancements are exempt. See [Proposing an Idea](#proposing-an-idea). Smaller quality-of-life enhancements are exempt.
- Issues that are labeled as `feature` or `bug` should be connected to the PR that resolves it. - Issues that are labeled as `feature` or `bug` should be connected to the PR that resolves it.
- Whoever is working on a `feature` or `bug` issue (whether a maintainer or someone from the - Whoever is working on a `feature` or `bug` issue (whether a maintainer or someone from the
community), should either assign the issue to themself or make a comment in the issue saying community), should either assign the issue to themselves or make a comment in the issue saying
that they are taking it. that they are taking it.
- `proposal` and `support/question` issues should stay open until resolved or if they have not - `proposal` and `support/question` issues should stay open until resolved or if they have not
been active for more than 30 days. This will help keep the issue queue to a manageable size been active for more than 30 days. This will help keep the issue queue to a manageable size
@ -280,11 +276,25 @@ Like any good open source project, we use Pull Requests (PRs) to track code chan
or explicitly request another OWNER do that for them. or explicitly request another OWNER do that for them.
- If the owner of a PR is _not_ listed in `OWNERS`, any core maintainer may merge the PR. - If the owner of a PR is _not_ listed in `OWNERS`, any core maintainer may merge the PR.
#### Documentation PRs ### Documentation PRs
Documentation PRs should be made on the docs repo: <https://github.com/helm/helm-www>. Keeping Helm's documentation up to date is highly desirable, and is recommended for all user facing changes. Accurate and helpful documentation is critical for effectively communicating Helm's behavior to a wide audience.
Small, ad-hoc changes/PRs to Helm which introduce user facing changes, which would benefit from documentation changes, should apply the `docs needed` label. Larger changes associated with a HIP should track docs via that HIP. The `docs needed` label doesn't block PRs, and maintainers/PR reviewers should apply discretion judging in whether the `docs needed` label should be applied.
### Profiling PRs
Documentation PRs will follow the same lifecycle as other PRs. They will also be labeled with the If your contribution requires profiling to check memory and/or CPU usage, you can set `HELM_PPROF_CPU_PROFILE=/path/to/cpu.prof` and/or `HELM_PPROF_MEM_PROFILE=/path/to/mem.prof` environment variables to collect runtime profiling data for analysis. You can use Golang's [pprof](https://github.com/google/pprof/blob/main/doc/README.md) tool to inspect the results.
`docs` label. For documentation, special attention will be paid to spelling, grammar, and clarity
(whereas those things don't matter *as* much for comments in code). Example analysing collected profiling data
```
HELM_PPROF_CPU_PROFILE=cpu.prof HELM_PPROF_MEM_PROFILE=mem.prof helm show all bitnami/nginx
# Visualize graphs. You need to have installed graphviz package in your system
go tool pprof -http=":8000" cpu.prof
go tool pprof -http=":8001" mem.prof
```
## The Triager ## The Triager
@ -327,6 +337,7 @@ The following tables define all label types used for Helm. It is split up by cat
| `needs rebase` | Indicates a PR needs to be rebased before it can be merged | | `needs rebase` | Indicates a PR needs to be rebased before it can be merged |
| `needs pick` | Indicates a PR needs to be cherry-picked into a feature branch (generally bugfix branches). Once it has been, the `picked` label should be applied and this one removed | | `needs pick` | Indicates a PR needs to be cherry-picked into a feature branch (generally bugfix branches). Once it has been, the `picked` label should be applied and this one removed |
| `picked` | This PR has been cherry-picked into a feature branch | | `picked` | This PR has been cherry-picked into a feature branch |
| `docs needed` | Tracks PRs that introduces a feature/change for which documentation update would be desirable (non-blocking). Once a suitable documentation PR has been created, then this label should be removed |
#### Size labels #### Size labels

118
KEYS

@ -940,3 +940,121 @@ AirPev6SluPhLJ2mswaK3THlhOZulKO/VIEJ6g50m5Vj3hdYf6sR603yK9rP+3iu
IagTQt2SGfW3Ap0RO3Yt+w29BpZ1CZ5Ml4gAYkXz0hiiMnVRhlcLIOHoFw== IagTQt2SGfW3Ap0RO3Yt+w29BpZ1CZ5Ml4gAYkXz0hiiMnVRhlcLIOHoFw==
=h3+3 =h3+3
-----END PGP PUBLIC KEY BLOCK----- -----END PGP PUBLIC KEY BLOCK-----
pub rsa4096 2018-12-08 [SC]
208DD36ED5BB3745A16743A4C7C6FBB5B91C1155
uid [ultimate] Scott Rigby <scott@r6by.com>
sig 3 C7C6FBB5B91C1155 2018-12-08 [self-signature]
sig 134FC1555856DA4F 2018-12-13 [User ID not found]
sig 62F49E747D911B60 2018-12-17 [User ID not found]
sig F54982D216088EE1 2019-01-05 [User ID not found]
sub rsa4096 2018-12-08 [E]
sig C7C6FBB5B91C1155 2018-12-08 [self-signature]
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFwMUAcBEADplQ+msULZ4kt01bXDvZ66MSVe5Fi1cPqAa/5/ZtaHZWSKrcN6
K0cadpozJp74HSZzORLYV/50EGwXU+OG1dFe73FbsTCgQyLCbh/OjT+Exq553g2D
/IB4/6/vCs9XXiYdKot3P2NsHI6RqeGqgW2IkFVsMXO2Lq1XKFTWQniO7PhHW8nG
Trub7HrxR6i9KHtVtxLs+XoXY7Jj0gB7WyRkYjHLXti4VtvcBq0WK3pSgEIy5MwR
WDepmle8n8EJZrh3T323YM41MXKGT00wCSKMbSHJO7QssiOda9XluC175HDfihm3
q5OKV2ZYIbChsQxuJz1Y97hwZ5KkLn//W2pxTdOElOcynFpQNx7D4b6UTP2DCCRc
n41SiDIyHg25cUXXAkJWlYRD1koGfLBipJA0DcKqlh3W+8zNfngZ0PSxwFtJwSre
Zx5I5uHAgKO5nS4hLxGYUMv+MsSKHMYR6qkqFg1Eal6tTa68bPFTbzypDmMUKXZT
sZtZ79WoIUU9D3O+F+Z9rxwaQ3Dv7J49FdbLPB3zqENqX7OWHZ38m5dsweTFhQi+
4AaDLEMiqMi27SfPkF1/+JDc1SOoLVo9QgukqhFlz6qEIbud7LUfpeKBRNJsfdr6
HE3cH8MWHInnlJ49De1oLl5bwAwScQig5jmv5DZxN5qdTg64vgoreBLgsQARAQAB
tBxTY290dCBSaWdieSA8c2NvdHRAcjZieS5jb20+iQJOBBMBCAA4FiEEII3TbtW7
N0WhZ0Okx8b7tbkcEVUFAlwMUAcCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AA
CgkQx8b7tbkcEVUIPg//cK8zaAOUIFClQ31TeQnwHsJwcBMeRRegUjt4cIYSjv3F
oNe5EQKxgpwT/1iegvRURjxHbhSE0paQ9wsi8Tr5Ygs0DJLDp4PJVZtVHJ5qB3XT
VuCiqyb90kIG+ok1m8FHBwpeU7o2a05InYwJLWowBQTulhrS4VKzrPs2kMu4z5Am
5sbs70f0hRLUXGmEAFUnZm+lpF48/PCMNSPrxgZ6rtNqtXq8oCyPNvyO8Ou130tv
TSoHx5Tobz3RnSeDXpidC/Z1rQGq4Spi+a0WwC9BCArDvtOrQBoeQFgdpy5OsE/t
QfQNEffyLRLlAZgXewOC+PeF+xrz+ku/rMBlt/h0h5UcMS3joUHTRZyQoA5dzWZh
K8pMXYppHXJtxl0fNSG1rH1vsTu+Mjxmd9eBwaDxnBFwzBOukiwSUgW1r2DyZ7S+
25ZW62lcz/E9+sgWV/PGR+YKGbsDdnraPdQEc95k8NQnYsX/7YO/WXJfP7Cdqd8X
dAAVW6/dGUp2i4QogEYk+GeJz3rJJK24Wzgf2FJ32FuSH8uOQUH93h5QZbJAD+pD
R2qZAZjCHYAgvSuqnmDlefvG95TwIVzy3OFWbLwp6YyBdyZrdpTafER4zk17f0HF
xS3z3LG0LsEWHShxccjzjoAUeppAU0Qojag3kKLoCwveambIdjXIxliBr/S2VSWJ
AjMEEAEKAB0WIQTlYbNM52c/3RO87D71SYLSFgiO4QUCXC/89gAKCRD1SYLSFgiO
4YfMEACS7/90VvyhbcQYB/2N6dYYuVd4tMRpM7jgWO6LqDDh3C9S2NI2bzwxGFBR
nqV9N6fD+yLug/dtAPq5D4i7AXzRqPA8XQ2ky/1EJOv5EmOl6NYnUZafEBMDMai6
F6XOji2JR7b6xlRC7GwzUdMR1rn8eyCxuJobgB+wMzfcSAnaDsH1qU7a5ohEewtQ
IgEcLLmiLapCqNXm0l5oIYQQypbRrogw4ePw8KcLDreRtPCpPdLIThqdfkXLQ69Z
ZwGd1+Pg6xgu7MJggnNq2RYortW/Jx9WUFs0Jj9c+Yz5pPeQmc1jjF4uHvxfi0gt
wFqL+bu+HfxI/Hiudkzzp3v+Llmjk6RypJowLLk5cxMxv7XMfK3cLLDO5uw0pPiq
l1f4u4P3YeRLv5YFh8SRrk/PadEqFr10mIOmreASq9LivJkhGK9eqQ0X7zQfHmDq
S3nw62ousqlmEld39MIAMn02Ak9i9CD2M3G2O5gCAcvnFlWblx5CN9Pjc6kOb52W
eDMYisUmKnIkChzvAlfh8PhvQfLUpKN1AmzUOcXJokpu1Yx7OGaoDnfpXSHS8fe8
pu0jMsEhlqsNxNS6y5N0tWjxjYg7D1Qpeq3O4oft4HiO3ZfMPI4V1tatfeohnjic
UJkpVsS4RZybu8aqNGc8i/ggagiWc50oydK8Lp906XfOcEert4kCMwQQAQoAHRYh
BKuiUpWY9mJsQg0zW2L0nnR9kRtgBQJcF85IAAoJEGL0nnR9kRtg0OMQAKqpxGtA
uaMknrZxnxu+y4FXXrX/W2TLlF7Ns71upXhitwSWk0pVJi+OZUvIGj+8yCj2MEg6
o5qBJPyo8TuwIh1YfxBYigY5Hmt/uVVbKBM/VXyKDxzGrSts+r73cxf3BpPfyANB
a90LjKHvFv0czu5sfiRMHU9GCOehnBukdZ9PhOOcRuUlHoHlSf21x9kxa1tUFPVJ
eeoVOOnONDK6Dzmi6GoGRTq3X/HZ2JjhcdYSn37z/KxmZ/SNiwat60gw5zJHTh0a
dM54hwsdsp48/avpF8BlTgXsoH5dVdbaOTyNGXBbQaoL09FY2x2eXaFCP3RbMWK8
TpWh0Ijs/3JLFJ20jrvZqsmxmwIX25TmBb4UoR3HSEHFasYoIxv/me6V4oB7D1SJ
F3q5scPnRzV3I5wCRljKJcNHQbNb/c9Xnt5UVnLHRtkZW/NUw93H5Bq15dkq4U6i
prC0EgjfiWy2Esd9TiGb5kRuN0/duUY/d7dewp1tJGDRZACwWwHyYkPiPA6gzQfN
83yk29evZeX1rSBslnXSLzuwhVJc28KNZCeEcC2o1JninqfqoYnypgSFOS5BK6ZG
5YJD0gkKFX+ImC8OSKsIJ6QyrzyOBb7UwRcK5qlvYZGYgrt+B+mFDpxWPkzgpfMe
CvE0nUCgKDNg0Yvhr5w9JhK+49Qn6TuTADMIiQIzBBABCAAdFiEE2o9xo8issez5
FC4eE0/BVVhW2k8FAlwSzMYACgkQE0/BVVhW2k8jEQ//cV4+ke8eHhwwxCPZd+lA
mvgzalwSiZZ8H0EgAB2cK0LXEFe07XGKxe2tDkf5FDIQcNm7sIk0OcLhJzYX0p9P
A7ZzO2tZu8QuZlUqt4VnDL7B3xeW2Sh3STEmw80wubkgauRRysflAHIw3edchnIX
9Hq55MLBBAplQFkpA/Y7arg3Bn8v/8YQlULc30xRO8EoyxD+zyl+Ic+xFtFUxNc/
2dkqkdjq0Ohq89wTGTy0jaSI8INhZTGqR0cEYQPKZD+PXUUym/TaPKJKXagqxmu2
XXBv6QPp46a58viBxMj6+fl1JJH3DxNF9YY++7Xp8CckA3TKDA9hxOJK6wbrTzDB
DB+tjcwR4ff8QLv/CV3psyk2fX9CGCBdr0k0SCMQSFcHM8pKagkySjG+EJ1Tcflf
UDY2LD33n2BBIdCaQTu6u+Zeqq2e6R3UXm/raXuGrxUxzvOQBIhb7XaC6nhfDu8k
07yN/Tjwp+rgHt9ouH4pfFbGpvaIomBJq6pkTOk9ywDtHlSatqoVkbrbKpNzmwf8
z7pt+ICtKqAAWQTPFPD83h6elP26GKlsyXyhT7HNmKUHaXInEbaD+IoCJ+wY/O3i
gHV2Dn4QllSBSBhYlhl6utmP1zqwJJ0rI39mPS+nMXOhGB+bJ8EzAF/3N5J6y3TG
FnMEJD8qgdpDEgztjHUSAy25Ag0EXAxQBwEQALqYikkk0Ur4gn9PjxtjW4OxS5J4
e/u0UyOsv8znFM7CG9Ndha9rQs/7c7NEf3e+K6a7XqhzDKtyGAFVFlZArxbe6X8e
UV1OidOaH46z2vmtWOJYHIupXHlXS9LeXNO+pJjCNEAzmHbGjpkjGtNz6Opl4Uae
LoMFubRViXhvD8pBF72dGUlp8m+U4yeXJ03/q0sR94AdTA+1OzGd2+1s7PvL5XAx
BwXqx9gccMYhrNRPyBo/yRA+Wf4ewwluIVBMi9cpR1sNF4ITIYCH4i3mf4NJvg7P
0sPBY0s9k2jvHGLpINbFk6PkMtaRpqmgw695szTz16Gp0j41hRnEh7KnGneEp+SU
6A8A9UGnjM9upR/d1591I5gT2U+6Q05B8RtJQUmd3HBeHEBgftjgBR0tstH8qeac
Xc4V81OGlf3tdYP/LVggIlv8V5cdSZ4Bd3BXYWj2TIc2RmwWA2LWf4SA6JYvhEfp
OxOzzphlgPtZF0kneEgV/b/D/KQqEk7MyZl3gN+LNk+zX7VJ2RDeUiUnoxZDFJGi
jsbZd7yoDLkYvGiFkcQXORs2zbucweVXXK1Gyskj0c3Ih4syYYmKS0WJHMEozJUl
b81oa7kSc2XFArcAnPz2c1yErfzcCAlg/HImkZmAgVqAfuRyxZ426F7CYucHAOcE
N7bpIrOkqFp3uUb3ABEBAAGJAjYEGAEIACAWIQQgjdNu1bs3RaFnQ6THxvu1uRwR
VQUCXAxQBwIbDAAKCRDHxvu1uRwRVbFaD/0e57rP3H+1rUoGhRO0oeIveQqIdd9V
LKXUYuwzoK3HLg3BYUDEN03RS0KyNMYlHpnjyFl5L2JuXqGiJd/eu2iRXCwUMRb7
SPvH7gypa1NUK5te85+Y8JhXOMjwZkly3zS2nRTyvHxMn9EV7NPlT/oEVu/woPrM
o7XzmPChuvnk8pLWBW04wg5G5atDbu5+QVZlecNCrtRYJg/Cd8alKpJSeZX7y3cy
fe2P20Gv0UOipKWaAFL55zFLbmu7HWVumYAKs6T+X/pZqmcfMaVwodIBeRJxRIvl
PkrBxljahaFGOdgJ6FVnmO34uoYcpd019NEr9gbPoaFWmw37h3Tnc6U5sLAouaV4
AERWmwBPIVTizYt1h8Qj4qyBhJ+QgZMjPlRqHWPZogHfMXDQV4gw3jgvVWTMVp1Z
gDQgrFNbw02CqPwgtFn15VNwAv/4vbyToRhc3pG54e3xwdAFM8R2uM9lHJKuHafW
7aFUk7aA20k8SG2BsZalb6tZLGxgcZOwMdO3lnLMPu1I5oOLl4cVoUIRZxtgmrbQ
ROaGdXGIgO7fJBXXogMxjUGhMola+v6ioFQpbOnJRAr2AUVBCrrEgHoodAufGTDu
nk38BkgHg3LHjCbCNEVkSK2TMT69A58iwpY9WUQlphsiz4WBpafSPbv/jSlsm7uK
TNWtbFGBRpJyEg==
=w141
-----END PGP PUBLIC KEY BLOCK-----
pub ed25519 2024-07-09 [SC]
7FEC81FACC7FFB2A010ADD13C2D40F4D8196E874
uid [ultimate] Robert Sirchia (I like turtles.) <rsirchia@outlook.com>
sig 3 C2D40F4D8196E874 2024-07-09 [self-signature]
sub cv25519 2024-07-09 [E]
sig C2D40F4D8196E874 2024-07-09 [self-signature]
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEZo2C6xYJKwYBBAHaRw8BAQdA8kCWaI+FlCabcTw8EVeiMkokyWDalgl/Inbn
ACcGN1e0N1JvYmVydCBTaXJjaGlhIChJIGxpa2UgdHVydGxlcy4pIDxyc2lyY2hp
YUBvdXRsb29rLmNvbT6IkwQTFgoAOxYhBH/sgfrMf/sqAQrdE8LUD02Bluh0BQJm
jYLrAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEMLUD02Bluh0dyYA
/i7RB6m3MXNA8ei7GD8uQVpLfCRgEFsqSS/AzAOu8NGhAQCbw1kWL3AUll7KKtiQ
UE96nhCk+HnkQeVkWYS+MZ1tALg4BGaNgusSCisGAQQBl1UBBQEBB0CCA6Au4krL
YinQq9aAs29fFeRu/ye3PqQuz5jZ2r1ScAMBCAeIeAQYFgoAIBYhBH/sgfrMf/sq
AQrdE8LUD02Bluh0BQJmjYLrAhsMAAoJEMLUD02Bluh0KH4BAMSwEIGkoQl10LN3
K6V08VpFmniENmCDHshXYq0gGiTDAP9FsXl2UtmFU5xuYxH4fRKIxgmxJRAFMWI8
u3Rdu/s+DQ==
=smBO
-----END PGP PUBLIC KEY BLOCK-----

@ -1,8 +1,8 @@
BINDIR := $(CURDIR)/bin BINDIR := $(CURDIR)/bin
INSTALL_PATH ?= /usr/local/bin INSTALL_PATH ?= /usr/local/bin
DIST_DIRS := find * -type d -exec DIST_DIRS := find * -type d -exec
TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x windows/amd64 TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum
BINNAME ?= helm BINNAME ?= helm
GOBIN = $(shell go env GOBIN) GOBIN = $(shell go env GOBIN)
@ -11,19 +11,20 @@ GOBIN = $(shell go env GOPATH)/bin
endif endif
GOX = $(GOBIN)/gox GOX = $(GOBIN)/gox
GOIMPORTS = $(GOBIN)/goimports GOIMPORTS = $(GOBIN)/goimports
ARCH = $(shell uname -p) ARCH = $(shell go env GOARCH)
ACCEPTANCE_DIR:=../acceptance-testing ACCEPTANCE_DIR:=../acceptance-testing
# To specify the subset of acceptance tests to run. '.' means all tests # To specify the subset of acceptance tests to run. '.' means all tests
ACCEPTANCE_RUN_TESTS=. ACCEPTANCE_RUN_TESTS=.
# go option # go option
PKG := ./... PKG := ./...
TAGS := TAGS :=
TESTS := . TESTS := .
TESTFLAGS := TESTFLAGS :=
LDFLAGS := -w -s LDFLAGS := -w -s
GOFLAGS := GOFLAGS :=
CGO_ENABLED ?= 0
# Rebuild the binary if any of these files change # Rebuild the binary if any of these files change
SRC := $(shell find . -type f -name '*.go' -print) go.mod go.sum SRC := $(shell find . -type f -name '*.go' -print) go.mod go.sum
@ -43,7 +44,7 @@ BINARY_VERSION ?= ${GIT_TAG}
# Only set Version if building a tag or VERSION is set # Only set Version if building a tag or VERSION is set
ifneq ($(BINARY_VERSION),) ifneq ($(BINARY_VERSION),)
LDFLAGS += -X helm.sh/helm/v3/internal/version.version=${BINARY_VERSION} LDFLAGS += -X helm.sh/helm/v4/internal/version.version=${BINARY_VERSION}
endif endif
VERSION_METADATA = unreleased VERSION_METADATA = unreleased
@ -52,9 +53,9 @@ ifneq ($(GIT_TAG),)
VERSION_METADATA = VERSION_METADATA =
endif endif
LDFLAGS += -X helm.sh/helm/v3/internal/version.metadata=${VERSION_METADATA} LDFLAGS += -X helm.sh/helm/v4/internal/version.metadata=${VERSION_METADATA}
LDFLAGS += -X helm.sh/helm/v3/internal/version.gitCommit=${GIT_COMMIT} LDFLAGS += -X helm.sh/helm/v4/internal/version.gitCommit=${GIT_COMMIT}
LDFLAGS += -X helm.sh/helm/v3/internal/version.gitTreeState=${GIT_DIRTY} LDFLAGS += -X helm.sh/helm/v4/internal/version.gitTreeState=${GIT_DIRTY}
LDFLAGS += $(EXT_LDFLAGS) LDFLAGS += $(EXT_LDFLAGS)
# Define constants based on the client-go version # Define constants based on the client-go version
@ -62,10 +63,10 @@ K8S_MODULES_VER=$(subst ., ,$(subst v,,$(shell go list -f '{{.Version}}' -m k8s.
K8S_MODULES_MAJOR_VER=$(shell echo $$(($(firstword $(K8S_MODULES_VER)) + 1))) K8S_MODULES_MAJOR_VER=$(shell echo $$(($(firstword $(K8S_MODULES_VER)) + 1)))
K8S_MODULES_MINOR_VER=$(word 2,$(K8S_MODULES_VER)) K8S_MODULES_MINOR_VER=$(word 2,$(K8S_MODULES_VER))
LDFLAGS += -X helm.sh/helm/v3/pkg/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) LDFLAGS += -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v3/pkg/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) LDFLAGS += -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v3/pkg/chartutil.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) LDFLAGS += -X helm.sh/helm/v4/pkg/chart/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v3/pkg/chartutil.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) LDFLAGS += -X helm.sh/helm/v4/pkg/chart/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
.PHONY: all .PHONY: all
all: build all: build
@ -77,7 +78,7 @@ all: build
build: $(BINDIR)/$(BINNAME) build: $(BINDIR)/$(BINNAME)
$(BINDIR)/$(BINNAME): $(SRC) $(BINDIR)/$(BINNAME): $(SRC)
GO111MODULE=on go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/helm CGO_ENABLED=$(CGO_ENABLED) go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/helm
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# install # install
@ -103,7 +104,16 @@ test: test-unit
test-unit: test-unit:
@echo @echo
@echo "==> Running unit tests <==" @echo "==> Running unit tests <=="
GO111MODULE=on go test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) go test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS)
@echo
@echo "==> Running unit test(s) with ldflags <=="
# Test to check the deprecation warnings on Kubernetes templates created by `helm create` against the current Kubernetes
# version. Note: The version details are set in var LDFLAGS. To avoid the ldflags impact on other unit tests that are
# based on older versions, this is run separately. When run without the ldflags in the unit test (above) or coverage
# test, it still passes with a false-positive result as the resources shouldnt be deprecated in the older Kubernetes
# version if it only starts failing with the latest.
go test $(GOFLAGS) -run ^TestHelmCreateChart_CheckDeprecatedWarnings$$ ./pkg/lint/ $(TESTFLAGS) -ldflags '$(LDFLAGS)'
.PHONY: test-coverage .PHONY: test-coverage
test-coverage: test-coverage:
@ -113,7 +123,11 @@ test-coverage:
.PHONY: test-style .PHONY: test-style
test-style: test-style:
GO111MODULE=on golangci-lint run golangci-lint run ./...
@scripts/validate-license.sh
.PHONY: test-source-headers
test-source-headers:
@scripts/validate-license.sh @scripts/validate-license.sh
.PHONY: test-acceptance .PHONY: test-acceptance
@ -137,7 +151,7 @@ coverage:
.PHONY: format .PHONY: format
format: $(GOIMPORTS) format: $(GOIMPORTS)
GO111MODULE=on go list -f '{{.Dir}}' ./... | xargs $(GOIMPORTS) -w -local helm.sh/helm go list -f '{{.Dir}}' ./... | xargs $(GOIMPORTS) -w -local helm.sh/helm
# Generate golden files used in unit tests # Generate golden files used in unit tests
.PHONY: gen-test-golden .PHONY: gen-test-golden
@ -149,15 +163,15 @@ gen-test-golden: test-unit
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# dependencies # dependencies
# If go get is run from inside the project directory it will add the dependencies # If go install is run from inside the project directory it will add the
# to the go.mod file. To avoid that we change to a directory without a go.mod file # dependencies to the go.mod file. To avoid that we change to a directory
# when downloading the following dependencies # without a go.mod file when downloading the following dependencies
$(GOX): $(GOX):
(cd /; GO111MODULE=on go get -u github.com/mitchellh/gox) (cd /; go install github.com/mitchellh/gox@v1.0.2-0.20220701044238-9f712387e2d2)
$(GOIMPORTS): $(GOIMPORTS):
(cd /; GO111MODULE=on go get -u golang.org/x/tools/cmd/goimports) (cd /; go install golang.org/x/tools/cmd/goimports@latest)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# release # release
@ -165,7 +179,7 @@ $(GOIMPORTS):
.PHONY: build-cross .PHONY: build-cross
build-cross: LDFLAGS += -extldflags "-static" build-cross: LDFLAGS += -extldflags "-static"
build-cross: $(GOX) build-cross: $(GOX)
GOFLAGS="-trimpath" GO111MODULE=on CGO_ENABLED=0 $(GOX) -parallel=3 -output="_dist/{{.OS}}-{{.Arch}}/$(BINNAME)" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' ./cmd/helm GOFLAGS="-trimpath" CGO_ENABLED=0 $(GOX) -parallel=3 -output="_dist/{{.OS}}-{{.Arch}}/$(BINNAME)" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' ./cmd/helm
.PHONY: dist .PHONY: dist
dist: dist:

@ -1,25 +1,31 @@
maintainers: maintainers:
- adamreese - gjenkins8
- bacongobbler - joejulian
- hickeyma
- jdolitsky
- marckhouzam - marckhouzam
- mattfarina - mattfarina
- robertsirc
- sabre1041
- scottrigby - scottrigby
- SlickNik
- technosophos - technosophos
triage: triage:
- banjoh
- yxxhero - yxxhero
- zonggen - zonggen
- z4ce
emeritus: emeritus:
- adamreese
- bacongobbler
- fibonacci1729 - fibonacci1729
- hickeyma
- jascott1 - jascott1
- jdolitsky
- michelleN - michelleN
- migmartri - migmartri
- nebril - nebril
- prydonius - prydonius
- rimusz - rimusz
- seh - seh
- SlickNik
- thomastaylor312 - thomastaylor312
- vaikas-google - vaikas-google
- viglesiasce - viglesiasce

@ -1,9 +1,10 @@
# Helm # 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) [![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) [![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/helm.sh/helm/v4)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3131/badge)](https://bestpractices.coreinfrastructure.org/projects/3131) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3131/badge)](https://bestpractices.coreinfrastructure.org/projects/3131)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/helm/helm/badge)](https://scorecard.dev/viewer/?uri=github.com/helm/helm)
Helm is a tool for managing Charts. Charts are packages of pre-configured Kubernetes resources. Helm is a tool for managing Charts. Charts are packages of pre-configured Kubernetes resources.
@ -28,8 +29,12 @@ Think of it like apt/yum/homebrew for Kubernetes.
- Charts can be stored on disk, or fetched from remote chart repositories - Charts can be stored on disk, or fetched from remote chart repositories
(like Debian or RedHat packages) (like Debian or RedHat packages)
## Install ## Helm Development and Stable Versions
Helm v4 is currently under development on the `main` branch. This is unstable and the APIs within the Go SDK and at the command line are changing.
Helm v3 (current stable) is maintained on the `dev-v3` branch. APIs there follow semantic versioning.
## Install
Binary downloads of the Helm client can be found on [the Releases page](https://github.com/helm/helm/releases/latest). Binary downloads of the Helm client can be found on [the Releases page](https://github.com/helm/helm/releases/latest).
@ -39,9 +44,10 @@ If you want to use a package manager:
- [Homebrew](https://brew.sh/) users can use `brew install helm`. - [Homebrew](https://brew.sh/) users can use `brew install helm`.
- [Chocolatey](https://chocolatey.org/) users can use `choco install kubernetes-helm`. - [Chocolatey](https://chocolatey.org/) users can use `choco install kubernetes-helm`.
- [Winget](https://learn.microsoft.com/en-us/windows/package-manager/) users can use `winget install Helm.Helm`.
- [Scoop](https://scoop.sh/) users can use `scoop install 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`.
- [Snapcraft](https://snapcraft.io/) users can use `snap install helm --classic` - [Flox](https://flox.dev) users can use `flox install kubernetes-helm`.
To rapidly get Helm up and running, start with the [Quick Start Guide](https://helm.sh/docs/intro/quickstart/). To rapidly get Helm up and running, start with the [Quick Start Guide](https://helm.sh/docs/intro/quickstart/).
@ -56,6 +62,8 @@ Get started with the [Quick Start guide](https://helm.sh/docs/intro/quickstart/)
The [Helm roadmap uses GitHub milestones](https://github.com/helm/helm/milestones) to track the progress of the project. The [Helm roadmap uses GitHub milestones](https://github.com/helm/helm/milestones) to track the progress of the project.
The development of Helm v4 is currently happening on the `main` branch while the development of Helm v3, the stable branch, is happening on the `dev-v3` branch. Changes should be made to the `main` branch prior to being added to the `dev-v3` branch so that all changes are carried along to Helm v4.
## Community, discussion, contribution, and support ## Community, discussion, contribution, and support
You can reach the Helm community and developers via the following channels: You can reach the Helm community and developers via the following channels:
@ -68,6 +76,10 @@ You can reach the Helm community and developers via the following channels:
- [Helm Mailing List](https://lists.cncf.io/g/cncf-helm) - [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)) - 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 ### Code of conduct
Participation in the Helm community is governed by the [Code of Conduct](code-of-conduct.md). Participation in the Helm community is governed by the [Code of Conduct](code-of-conduct.md).

@ -23,7 +23,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
) )
const completionDesc = ` const completionDesc = `
@ -102,8 +102,8 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
Short: "generate autocompletion script for bash", Short: "generate autocompletion script for bash",
Long: bashCompDesc, Long: bashCompDesc,
Args: require.NoArgs, Args: require.NoArgs,
ValidArgsFunction: noCompletions, ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
return runCompletionBash(out, cmd) return runCompletionBash(out, cmd)
}, },
} }
@ -114,8 +114,8 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
Short: "generate autocompletion script for zsh", Short: "generate autocompletion script for zsh",
Long: zshCompDesc, Long: zshCompDesc,
Args: require.NoArgs, Args: require.NoArgs,
ValidArgsFunction: noCompletions, ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
return runCompletionZsh(out, cmd) return runCompletionZsh(out, cmd)
}, },
} }
@ -126,8 +126,8 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
Short: "generate autocompletion script for fish", Short: "generate autocompletion script for fish",
Long: fishCompDesc, Long: fishCompDesc,
Args: require.NoArgs, Args: require.NoArgs,
ValidArgsFunction: noCompletions, ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
return runCompletionFish(out, cmd) return runCompletionFish(out, cmd)
}, },
} }
@ -138,8 +138,8 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
Short: "generate autocompletion script for powershell", Short: "generate autocompletion script for powershell",
Long: powershellCompDesc, Long: powershellCompDesc,
Args: require.NoArgs, Args: require.NoArgs,
ValidArgsFunction: noCompletions, ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
return runCompletionPowershell(out, cmd) return runCompletionPowershell(out, cmd)
}, },
} }
@ -209,7 +209,15 @@ func runCompletionPowershell(out io.Writer, cmd *cobra.Command) error {
return cmd.Root().GenPowerShellCompletionWithDesc(out) return cmd.Root().GenPowerShellCompletionWithDesc(out)
} }
// Function to disable file completion // noMoreArgsCompFunc deactivates file completion when doing argument shell completion.
func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // It also provides some ActiveHelp to indicate no more arguments are accepted.
return nil, cobra.ShellCompDirectiveNoFileComp func noMoreArgsCompFunc(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return noMoreArgsComp()
}
// noMoreArgsComp deactivates file completion when doing argument shell completion.
// It also provides some ActiveHelp to indicate no more arguments are accepted.
func noMoreArgsComp() ([]string, cobra.ShellCompDirective) {
activeHelpMsg := "This command does not take any more arguments (but may accept flags)."
return cobra.AppendActiveHelp(nil, activeHelpMsg), cobra.ShellCompDirectiveNoFileComp
} }

@ -21,8 +21,8 @@ import (
"strings" "strings"
"testing" "testing"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
) )
// Check if file completion should be performed according to parameter 'shouldBePerformed' // Check if file completion should be performed according to parameter 'shouldBePerformed'
@ -47,10 +47,10 @@ func checkFileCompletion(t *testing.T, cmdName string, shouldBePerformed bool) {
} }
if !strings.Contains(out, "ShellCompDirectiveNoFileComp") != shouldBePerformed { if !strings.Contains(out, "ShellCompDirectiveNoFileComp") != shouldBePerformed {
if shouldBePerformed { if shouldBePerformed {
t.Error(fmt.Sprintf("Unexpected directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName)) t.Errorf("Unexpected directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName)
} else { } else {
t.Error(fmt.Sprintf("Did not receive directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName)) t.Errorf("Did not receive directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName)
} }
t.Log(out) t.Log(out)
} }

@ -23,10 +23,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil" chartutil "helm.sh/helm/v4/pkg/chart/util"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath"
) )
const createDesc = ` const createDesc = `
@ -64,16 +64,16 @@ func newCreateCmd(out io.Writer) *cobra.Command {
Short: "create a new chart with the given name", Short: "create a new chart with the given name",
Long: createDesc, Long: createDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 { if len(args) == 0 {
// Allow file completion when completing the argument for the name // Allow file completion when completing the argument for the name
// which could be a path // which could be a path
return nil, cobra.ShellCompDirectiveDefault return nil, cobra.ShellCompDirectiveDefault
} }
// No more completions, so disable file completion // No more completions, so disable file completion
return nil, cobra.ShellCompDirectiveNoFileComp return noMoreArgsComp()
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
o.name = args[0] o.name = args[0]
o.starterDir = helmpath.DataPath("starters") o.starterDir = helmpath.DataPath("starters")
return o.run(out) return o.run(out)

@ -18,22 +18,21 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"helm.sh/helm/v3/internal/test/ensure" "helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v4/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil" chartutil "helm.sh/helm/v4/pkg/chart/util"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath"
) )
func TestCreateCmd(t *testing.T) { func TestCreateCmd(t *testing.T) {
defer ensure.HelmHome(t)() ensure.HelmHome(t)
cname := "testchart" cname := "testchart"
dir := ensure.TempDir(t) dir := t.TempDir()
defer testChdir(t, dir)() defer testChdir(t, dir)()
// Run a create // Run a create
@ -62,7 +61,7 @@ func TestCreateCmd(t *testing.T) {
} }
func TestCreateStarterCmd(t *testing.T) { func TestCreateStarterCmd(t *testing.T) {
defer ensure.HelmHome(t)() ensure.HelmHome(t)
cname := "testchart" cname := "testchart"
defer resetEnv()() defer resetEnv()()
os.MkdirAll(helmpath.CachePath(), 0755) os.MkdirAll(helmpath.CachePath(), 0755)
@ -77,7 +76,7 @@ func TestCreateStarterCmd(t *testing.T) {
t.Logf("Created %s", dest) t.Logf("Created %s", dest)
} }
tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl") 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) t.Fatalf("Could not write template: %s", err)
} }
@ -128,7 +127,7 @@ func TestCreateStarterCmd(t *testing.T) {
func TestCreateStarterAbsoluteCmd(t *testing.T) { func TestCreateStarterAbsoluteCmd(t *testing.T) {
defer resetEnv()() defer resetEnv()()
defer ensure.HelmHome(t)() ensure.HelmHome(t)
cname := "testchart" cname := "testchart"
// Create a starter. // Create a starter.
@ -140,7 +139,7 @@ func TestCreateStarterAbsoluteCmd(t *testing.T) {
t.Logf("Created %s", dest) t.Logf("Created %s", dest)
} }
tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl") 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) t.Fatalf("Could not write template: %s", err)
} }

@ -20,9 +20,10 @@ import (
"path/filepath" "path/filepath"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
) )
const dependencyDesc = ` const dependencyDesc = `
@ -93,7 +94,7 @@ func newDependencyCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
cmd.AddCommand(newDependencyListCmd(out)) cmd.AddCommand(newDependencyListCmd(out))
cmd.AddCommand(newDependencyUpdateCmd(cfg, out)) cmd.AddCommand(newDependencyUpdateCmd(cfg, out))
cmd.AddCommand(newDependencyBuildCmd(cfg, out)) cmd.AddCommand(newDependencyBuildCmd(out))
return cmd return cmd
} }
@ -106,7 +107,7 @@ func newDependencyListCmd(out io.Writer) *cobra.Command {
Short: "list the dependencies for the given chart", Short: "list the dependencies for the given chart",
Long: dependencyListDesc, Long: dependencyListDesc,
Args: require.MaximumNArgs(1), Args: require.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
chartpath := "." chartpath := "."
if len(args) > 0 { if len(args) > 0 {
chartpath = filepath.Clean(args[0]) chartpath = filepath.Clean(args[0])
@ -120,3 +121,16 @@ func newDependencyListCmd(out io.Writer) *cobra.Command {
f.UintVar(&client.ColumnWidth, "max-col-width", 80, "maximum column width for output table") f.UintVar(&client.ColumnWidth, "max-col-width", 80, "maximum column width for output table")
return cmd return cmd
} }
func addDependencySubcommandFlags(f *pflag.FlagSet, client *action.Dependency) {
f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures")
f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys")
f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache")
f.StringVar(&client.Username, "username", "", "chart repository username where to locate the requested chart")
f.StringVar(&client.Password, "password", "", "chart repository password where to locate the requested chart")
f.StringVar(&client.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&client.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.BoolVar(&client.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&client.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download")
f.StringVar(&client.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
}

@ -24,10 +24,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/client-go/util/homedir" "k8s.io/client-go/util/homedir"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v4/pkg/downloader"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v4/pkg/getter"
) )
const dependencyBuildDesc = ` const dependencyBuildDesc = `
@ -41,7 +41,7 @@ If no lock file is found, 'helm dependency build' will mirror the behavior
of 'helm dependency update'. of 'helm dependency update'.
` `
func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func newDependencyBuildCmd(out io.Writer) *cobra.Command {
client := action.NewDependency() client := action.NewDependency()
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -49,18 +49,24 @@ func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Comm
Short: "rebuild the charts/ directory based on the Chart.lock file", Short: "rebuild the charts/ directory based on the Chart.lock file",
Long: dependencyBuildDesc, Long: dependencyBuildDesc,
Args: require.MaximumNArgs(1), Args: require.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
chartpath := "." chartpath := "."
if len(args) > 0 { if len(args) > 0 {
chartpath = filepath.Clean(args[0]) chartpath = filepath.Clean(args[0])
} }
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
man := &downloader.Manager{ man := &downloader.Manager{
Out: out, Out: out,
ChartPath: chartpath, ChartPath: chartpath,
Keyring: client.Keyring, Keyring: client.Keyring,
SkipUpdate: client.SkipRefresh, SkipUpdate: client.SkipRefresh,
Getters: getter.All(settings), Getters: getter.All(settings),
RegistryClient: cfg.RegistryClient, RegistryClient: registryClient,
RepositoryConfig: settings.RepositoryConfig, RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache, RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug, Debug: settings.Debug,
@ -68,7 +74,7 @@ func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Comm
if client.Verify { if client.Verify {
man.Verify = downloader.VerifyIfPossible man.Verify = downloader.VerifyIfPossible
} }
err := man.Build() err = man.Build()
if e, ok := err.(downloader.ErrRepoNotFound); ok { if e, ok := err.(downloader.ErrRepoNotFound); ok {
return fmt.Errorf("%s. Please add the missing repos via 'helm repo add'", e.Error()) return fmt.Errorf("%s. Please add the missing repos via 'helm repo add'", e.Error())
} }
@ -77,9 +83,7 @@ func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Comm
} }
f := cmd.Flags() f := cmd.Flags()
f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") addDependencySubcommandFlags(f, client)
f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys")
f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache")
return cmd return cmd
} }

@ -22,18 +22,18 @@ import (
"strings" "strings"
"testing" "testing"
"helm.sh/helm/v3/pkg/chartutil" chartutil "helm.sh/helm/v4/pkg/chart/util"
"helm.sh/helm/v3/pkg/provenance" "helm.sh/helm/v4/pkg/provenance"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo"
"helm.sh/helm/v3/pkg/repo/repotest" "helm.sh/helm/v4/pkg/repo/repotest"
) )
func TestDependencyBuildCmd(t *testing.T) { func TestDependencyBuildCmd(t *testing.T) {
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz") srv := repotest.NewTempServer(
t,
repotest.WithChartSourceGlob("testdata/testcharts/*.tgz"),
)
defer srv.Stop() defer srv.Stop()
if err != nil {
t.Fatal(err)
}
rootDir := srv.Root() rootDir := srv.Root()
srv.LinkIndices() srv.LinkIndices()
@ -50,11 +50,6 @@ func TestDependencyBuildCmd(t *testing.T) {
} }
ociSrv.Run(t, repotest.WithDependingChart(c)) ociSrv.Run(t, repotest.WithDependingChart(c))
err = os.Setenv("HELM_EXPERIMENTAL_OCI", "1")
if err != nil {
t.Fatal("failed to set environment variable enabling OCI support")
}
dir := func(p ...string) string { dir := func(p ...string) string {
return filepath.Join(append([]string{srv.Root()}, p...)...) return filepath.Join(append([]string{srv.Root()}, p...)...)
} }
@ -63,7 +58,7 @@ func TestDependencyBuildCmd(t *testing.T) {
createTestingChart(t, rootDir, chartname, srv.URL()) createTestingChart(t, rootDir, chartname, srv.URL())
repoFile := filepath.Join(rootDir, "repositories.yaml") repoFile := filepath.Join(rootDir, "repositories.yaml")
cmd := fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s", filepath.Join(rootDir, chartname), repoFile, rootDir) cmd := fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --plain-http", filepath.Join(rootDir, chartname), repoFile, rootDir)
_, out, err := executeActionCommand(cmd) _, out, err := executeActionCommand(cmd)
// In the first pass, we basically want the same results as an update. // In the first pass, we basically want the same results as an update.
@ -122,7 +117,7 @@ func TestDependencyBuildCmd(t *testing.T) {
t.Errorf("mismatched versions. Expected %q, got %q", "0.1.0", v) t.Errorf("mismatched versions. Expected %q, got %q", "0.1.0", v)
} }
skipRefreshCmd := fmt.Sprintf("dependency build '%s' --skip-refresh --repository-config %s --repository-cache %s", filepath.Join(rootDir, chartname), repoFile, rootDir) skipRefreshCmd := fmt.Sprintf("dependency build '%s' --skip-refresh --repository-config %s --repository-cache %s --plain-http", filepath.Join(rootDir, chartname), repoFile, rootDir)
_, out, err = executeActionCommand(skipRefreshCmd) _, out, err = executeActionCommand(skipRefreshCmd)
// In this pass, we check --skip-refresh option becomes effective. // In this pass, we check --skip-refresh option becomes effective.
@ -139,7 +134,7 @@ func TestDependencyBuildCmd(t *testing.T) {
if err := chartutil.SaveDir(c, dir()); err != nil { if err := chartutil.SaveDir(c, dir()); err != nil {
t.Fatal(err) t.Fatal(err)
} }
cmd = fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json", cmd = fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json --plain-http",
dir(ociChartName), dir(ociChartName),
dir("repositories.yaml"), dir("repositories.yaml"),
dir(), dir(),

@ -16,15 +16,16 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"io" "io"
"path/filepath" "path/filepath"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v4/pkg/downloader"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v4/pkg/getter"
) )
const dependencyUpDesc = ` const dependencyUpDesc = `
@ -43,7 +44,7 @@ in the Chart.yaml file, but (b) at the wrong version.
` `
// newDependencyUpdateCmd creates a new dependency update command. // newDependencyUpdateCmd creates a new dependency update command.
func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func newDependencyUpdateCmd(_ *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewDependency() client := action.NewDependency()
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -52,18 +53,24 @@ func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Com
Short: "update charts/ based on the contents of Chart.yaml", Short: "update charts/ based on the contents of Chart.yaml",
Long: dependencyUpDesc, Long: dependencyUpDesc,
Args: require.MaximumNArgs(1), Args: require.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
chartpath := "." chartpath := "."
if len(args) > 0 { if len(args) > 0 {
chartpath = filepath.Clean(args[0]) chartpath = filepath.Clean(args[0])
} }
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
man := &downloader.Manager{ man := &downloader.Manager{
Out: out, Out: out,
ChartPath: chartpath, ChartPath: chartpath,
Keyring: client.Keyring, Keyring: client.Keyring,
SkipUpdate: client.SkipRefresh, SkipUpdate: client.SkipRefresh,
Getters: getter.All(settings), Getters: getter.All(settings),
RegistryClient: cfg.RegistryClient, RegistryClient: registryClient,
RepositoryConfig: settings.RepositoryConfig, RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache, RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug, Debug: settings.Debug,
@ -76,9 +83,7 @@ func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Com
} }
f := cmd.Flags() f := cmd.Flags()
f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") addDependencySubcommandFlags(f, client)
f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys")
f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache")
return cmd return cmd
} }

@ -22,20 +22,20 @@ import (
"strings" "strings"
"testing" "testing"
"helm.sh/helm/v3/internal/test/ensure" "helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil" chartutil "helm.sh/helm/v4/pkg/chart/util"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v3/pkg/provenance" "helm.sh/helm/v4/pkg/provenance"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo"
"helm.sh/helm/v3/pkg/repo/repotest" "helm.sh/helm/v4/pkg/repo/repotest"
) )
func TestDependencyUpdateCmd(t *testing.T) { func TestDependencyUpdateCmd(t *testing.T) {
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz") srv := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testcharts/*.tgz"),
} )
defer srv.Stop() defer srv.Stop()
t.Logf("Listening on directory %s", srv.Root()) t.Logf("Listening on directory %s", srv.Root())
@ -51,11 +51,6 @@ func TestDependencyUpdateCmd(t *testing.T) {
} }
ociSrv.Run(t, repotest.WithDependingChart(c)) ociSrv.Run(t, repotest.WithDependingChart(c))
err = os.Setenv("HELM_EXPERIMENTAL_OCI", "1")
if err != nil {
t.Fatal("failed to set environment variable enabling OCI support")
}
if err := srv.LinkIndices(); err != nil { if err := srv.LinkIndices(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -72,7 +67,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
} }
_, out, err := executeActionCommand( _, out, err := executeActionCommand(
fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir()), fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --plain-http", dir(chartname), dir("repositories.yaml"), dir()),
) )
if err != nil { if err != nil {
t.Logf("Output: %s", out) t.Logf("Output: %s", out)
@ -115,7 +110,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
_, out, err = executeActionCommand(fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir())) _, out, err = executeActionCommand(fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --plain-http", dir(chartname), dir("repositories.yaml"), dir()))
if err != nil { if err != nil {
t.Logf("Output: %s", out) t.Logf("Output: %s", out)
t.Fatal(err) t.Fatal(err)
@ -136,7 +131,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
if err := chartutil.SaveDir(c, dir()); err != nil { if err := chartutil.SaveDir(c, dir()); err != nil {
t.Fatal(err) t.Fatal(err)
} }
cmd := fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json", cmd := fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json --plain-http",
dir(ociChartName), dir(ociChartName),
dir("repositories.yaml"), dir("repositories.yaml"),
dir(), dir(),
@ -154,12 +149,12 @@ func TestDependencyUpdateCmd(t *testing.T) {
func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) { func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) {
defer resetEnv()() defer resetEnv()()
defer ensure.HelmHome(t)() ensure.HelmHome(t)
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz") srv := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testcharts/*.tgz"),
} )
defer srv.Stop() defer srv.Stop()
t.Logf("Listening on directory %s", srv.Root()) t.Logf("Listening on directory %s", srv.Root())
@ -174,7 +169,7 @@ func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) {
} }
createTestingChart(t, dir(), chartname, srv.URL()) createTestingChart(t, dir(), chartname, srv.URL())
_, output, err := executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir())) _, output, err := executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s --plain-http", dir(chartname), dir("repositories.yaml"), dir()))
if err != nil { if err != nil {
t.Logf("Output: %s", output) t.Logf("Output: %s", output)
t.Fatal(err) t.Fatal(err)
@ -183,7 +178,7 @@ func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) {
// Chart repo is down // Chart repo is down
srv.Stop() srv.Stop()
_, output, err = executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir())) _, output, err = executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s --plain-http", dir(chartname), dir("repositories.yaml"), dir()))
if err == nil { if err == nil {
t.Logf("Output: %s", output) t.Logf("Output: %s", output)
t.Fatal("Expected error, got nil") t.Fatal("Expected error, got nil")
@ -205,12 +200,68 @@ func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) {
} }
} }
// Make sure tmpcharts is deleted // Make sure tmpcharts-x is deleted
if _, err := os.Stat(filepath.Join(dir(chartname), "tmpcharts")); !os.IsNotExist(err) { tmpPath := filepath.Join(dir(chartname), fmt.Sprintf("tmpcharts-%d", os.Getpid()))
if _, err := os.Stat(tmpPath); !os.IsNotExist(err) {
t.Fatalf("tmpcharts dir still exists") t.Fatalf("tmpcharts dir still exists")
} }
} }
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 doesnt cause the creation "+
"of an ad hoc repo index cache file\n%s", out)
}
}
func setupMockRepoServer(t *testing.T) *repotest.Server {
srv := repotest.NewTempServer(
t,
repotest.WithChartSourceGlob("testdata/testcharts/*.tgz"),
)
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 // createTestingMetadata creates a basic chart that depends on reqtest-0.1.0
// //
// The baseURL can be used to point to a particular repository server. // The baseURL can be used to point to a particular repository server.

@ -25,8 +25,10 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/cobra/doc" "github.com/spf13/cobra/doc"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
) )
const docsDesc = ` const docsDesc = `
@ -56,8 +58,8 @@ func newDocsCmd(out io.Writer) *cobra.Command {
Long: docsDesc, Long: docsDesc,
Hidden: true, Hidden: true,
Args: require.NoArgs, Args: require.NoArgs,
ValidArgsFunction: noCompletions, ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
o.topCmd = cmd.Root() o.topCmd = cmd.Root()
return o.run(out) return o.run(out)
}, },
@ -68,21 +70,14 @@ func newDocsCmd(out io.Writer) *cobra.Command {
f.StringVar(&o.docTypeString, "type", "markdown", "the type of documentation to generate (markdown, man, bash)") f.StringVar(&o.docTypeString, "type", "markdown", "the type of documentation to generate (markdown, man, bash)")
f.BoolVar(&o.generateHeaders, "generate-headers", false, "generate standard headers for markdown files") f.BoolVar(&o.generateHeaders, "generate-headers", false, "generate standard headers for markdown files")
cmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { cmd.RegisterFlagCompletionFunc("type", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
types := []string{"bash", "man", "markdown"} return []string{"bash", "man", "markdown"}, cobra.ShellCompDirectiveNoFileComp
var comps []string
for _, t := range types {
if strings.HasPrefix(t, toComplete) {
comps = append(comps, t)
}
}
return comps, cobra.ShellCompDirectiveNoFileComp
}) })
return cmd return cmd
} }
func (o *docsOptions) run(out io.Writer) error { func (o *docsOptions) run(_ io.Writer) error {
switch o.docTypeString { switch o.docTypeString {
case "markdown", "mdown", "md": case "markdown", "mdown", "md":
if o.generateHeaders { if o.generateHeaders {
@ -91,7 +86,7 @@ func (o *docsOptions) run(out io.Writer) error {
hdrFunc := func(filename string) string { hdrFunc := func(filename string) string {
base := filepath.Base(filename) base := filepath.Base(filename)
name := strings.TrimSuffix(base, path.Ext(base)) name := strings.TrimSuffix(base, path.Ext(base))
title := strings.Title(strings.Replace(name, "_", " ", -1)) title := cases.Title(language.Und, cases.NoLower).String(strings.Replace(name, "_", " ", -1))
return fmt.Sprintf("---\ntitle: \"%s\"\n---\n\n", title) return fmt.Sprintf("---\ntitle: \"%s\"\n---\n\n", title)
} }

@ -26,9 +26,9 @@ func TestDocsTypeFlagCompletion(t *testing.T) {
cmd: "__complete docs --type ''", cmd: "__complete docs --type ''",
golden: "output/docs-type-comp.txt", golden: "output/docs-type-comp.txt",
}, { }, {
name: "completion for docs --type", name: "completion for docs --type, no filter",
cmd: "__complete docs --type mar", cmd: "__complete docs --type mar",
golden: "output/docs-type-filtered-comp.txt", golden: "output/docs-type-comp.txt",
}} }}
runTestCmd(t, tests) runTestCmd(t, tests)
} }

@ -23,7 +23,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
) )
var envHelp = ` var envHelp = `
@ -36,15 +36,15 @@ func newEnvCmd(out io.Writer) *cobra.Command {
Short: "helm client environment information", Short: "helm client environment information",
Long: envHelp, Long: envHelp,
Args: require.MaximumNArgs(1), Args: require.MaximumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 { if len(args) == 0 {
keys := getSortedEnvVarKeys() keys := getSortedEnvVarKeys()
return keys, cobra.ShellCompDirectiveNoFileComp return keys, cobra.ShellCompDirectiveNoFileComp
} }
return nil, cobra.ShellCompDirectiveNoFileComp return noMoreArgsComp()
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(_ *cobra.Command, args []string) {
envVars := settings.EnvVars() envVars := settings.EnvVars()
if len(args) == 0 { if len(args) == 0 {

@ -28,22 +28,27 @@ import (
"github.com/spf13/pflag" "github.com/spf13/pflag"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v3/pkg/postrender" "helm.sh/helm/v4/pkg/postrender"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo"
) )
const outputFlag = "output" const (
const postRenderFlag = "post-renderer" outputFlag = "output"
postRenderFlag = "post-renderer"
postRenderArgsFlag = "post-renderer-args"
)
func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) {
f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)") f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)")
f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") f.StringArrayVar(&v.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.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 or using json format: {\"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) { func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
@ -56,6 +61,7 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
f.StringVar(&c.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") f.StringVar(&c.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.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.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.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") f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains")
} }
@ -66,12 +72,10 @@ func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) {
cmd.Flags().VarP(newOutputValue(output.Table, varRef), outputFlag, "o", cmd.Flags().VarP(newOutputValue(output.Table, varRef), outputFlag, "o",
fmt.Sprintf("prints the output in the specified format. Allowed values: %s", strings.Join(output.Formats(), ", "))) fmt.Sprintf("prints the output in the specified format. Allowed values: %s", strings.Join(output.Formats(), ", ")))
err := cmd.RegisterFlagCompletionFunc(outputFlag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc(outputFlag, func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
var formatNames []string var formatNames []string
for format, desc := range output.FormatsWithDesc() { for format, desc := range output.FormatsWithDesc() {
if strings.HasPrefix(format, toComplete) { formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc))
formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc))
}
} }
// Sort the results to get a deterministic order for the tests // Sort the results to get a deterministic order for the tests
@ -112,34 +116,86 @@ func (o *outputValue) Set(s string) error {
} }
func bindPostRenderFlag(cmd *cobra.Command, varRef *postrender.PostRenderer) { func bindPostRenderFlag(cmd *cobra.Command, varRef *postrender.PostRenderer) {
cmd.Flags().Var(&postRenderer{varRef}, postRenderFlag, "the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path") p := &postRendererOptions{varRef, "", []string{}}
cmd.Flags().Var(&postRendererString{p}, postRenderFlag, "the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path")
cmd.Flags().Var(&postRendererArgsSlice{p}, postRenderArgsFlag, "an argument to the post-renderer (can specify multiple)")
}
type postRendererOptions struct {
renderer *postrender.PostRenderer
binaryPath string
args []string
}
type postRendererString struct {
options *postRendererOptions
}
func (p *postRendererString) String() string {
return p.options.binaryPath
}
func (p *postRendererString) Type() string {
return "postRendererString"
}
func (p *postRendererString) Set(val string) error {
if val == "" {
return nil
}
p.options.binaryPath = val
pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...)
if err != nil {
return err
}
*p.options.renderer = pr
return nil
} }
type postRenderer struct { type postRendererArgsSlice struct {
renderer *postrender.PostRenderer options *postRendererOptions
} }
func (p postRenderer) String() string { func (p *postRendererArgsSlice) String() string {
return "exec" return "[" + strings.Join(p.options.args, ",") + "]"
} }
func (p postRenderer) Type() string { func (p *postRendererArgsSlice) Type() string {
return "postrenderer" return "postRendererArgsSlice"
} }
func (p postRenderer) Set(s string) error { func (p *postRendererArgsSlice) Set(val string) error {
if s == "" {
// a post-renderer defined by a user may accept empty arguments
p.options.args = append(p.options.args, val)
if p.options.binaryPath == "" {
return nil return nil
} }
pr, err := postrender.NewExec(s) // overwrite if already create PostRenderer by `post-renderer` flags
pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...)
if err != nil { if err != nil {
return err return err
} }
*p.renderer = pr *p.options.renderer = pr
return nil
}
func (p *postRendererArgsSlice) Append(val string) error {
p.options.args = append(p.options.args, val)
return nil return nil
} }
func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellCompDirective) { func (p *postRendererArgsSlice) Replace(val []string) error {
p.options.args = val
return nil
}
func (p *postRendererArgsSlice) GetSlice() []string {
return p.options.args
}
func compVersionFlag(chartRef string, _ string) ([]string, cobra.ShellCompDirective) {
chartInfo := strings.Split(chartRef, "/") chartInfo := strings.Split(chartRef, "/")
if len(chartInfo) != 2 { if len(chartInfo) != 2 {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
@ -153,24 +209,21 @@ func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellC
var versions []string var versions []string
if indexFile, err := repo.LoadIndexFile(path); err == nil { if indexFile, err := repo.LoadIndexFile(path); err == nil {
for _, details := range indexFile.Entries[chartName] { for _, details := range indexFile.Entries[chartName] {
version := details.Metadata.Version appVersion := details.Metadata.AppVersion
if strings.HasPrefix(version, toComplete) { appVersionDesc := ""
appVersion := details.Metadata.AppVersion if appVersion != "" {
appVersionDesc := "" appVersionDesc = fmt.Sprintf("App: %s, ", appVersion)
if appVersion != "" { }
appVersionDesc = fmt.Sprintf("App: %s, ", appVersion) created := details.Created.Format("January 2, 2006")
} createdDesc := ""
created := details.Created.Format("January 2, 2006") if created != "" {
createdDesc := "" createdDesc = fmt.Sprintf("Created: %s ", created)
if created != "" { }
createdDesc = fmt.Sprintf("Created: %s ", created) deprecated := ""
} if details.Metadata.Deprecated {
deprecated := "" deprecated = "(deprecated)"
if details.Metadata.Deprecated {
deprecated = "(deprecated)"
}
versions = append(versions, fmt.Sprintf("%s\t%s%s%s", version, appVersionDesc, createdDesc, deprecated))
} }
versions = append(versions, fmt.Sprintf("%s\t%s%s%s", details.Metadata.Version, appVersionDesc, createdDesc, deprecated))
} }
} }

@ -20,9 +20,9 @@ import (
"fmt" "fmt"
"testing" "testing"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time" helmtime "helm.sh/helm/v4/pkg/time"
) )
func outputFlagCompletionTest(t *testing.T, cmdName string) { func outputFlagCompletionTest(t *testing.T, cmdName string) {
@ -83,6 +83,13 @@ func outputFlagCompletionTest(t *testing.T, cmdName string) {
rels: releasesMockWithStatus(&release.Info{ rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed, Status: release.StatusDeployed,
}), }),
}, {
name: "completion for output flag, no filter",
cmd: fmt.Sprintf("__complete %s --output jso", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}} }}
runTestCmd(t, tests) runTestCmd(t, tests)
} }

@ -21,8 +21,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
) )
var getHelp = ` var getHelp = `
@ -33,6 +33,7 @@ get extended information about the release, including:
- The generated manifest file - The generated manifest file
- The notes provided by the chart of the release - The notes provided by the chart of the release
- The hooks associated with the release - The hooks associated with the release
- The metadata of the release
` `
func newGetCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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(newGetManifestCmd(cfg, out))
cmd.AddCommand(newGetHooksCmd(cfg, out)) cmd.AddCommand(newGetHooksCmd(cfg, out))
cmd.AddCommand(newGetNotesCmd(cfg, out)) cmd.AddCommand(newGetNotesCmd(cfg, out))
cmd.AddCommand(newGetMetadataCmd(cfg, out))
return cmd return cmd
} }

@ -22,9 +22,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
) )
var getAllHelp = ` var getAllHelp = `
@ -41,13 +41,13 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Short: "download all information for a named release", Short: "download all information for a named release",
Long: getAllHelp, Long: getAllHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 { if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp return noMoreArgsComp()
} }
return compListReleases(toComplete, args, cfg) return compListReleases(toComplete, args, cfg)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
res, err := client.Run(args[0]) res, err := client.Run(args[0])
if err != nil { if err != nil {
return err return err
@ -58,20 +58,23 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
return tpl(template, data, out) return tpl(template, data, out)
} }
return output.Table.Write(out, &statusPrinter{
return output.Table.Write(out, &statusPrinter{res, true, false}) release: res,
debug: true,
showMetadata: true,
hideNotes: false,
})
}, },
} }
f := cmd.Flags() f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision") f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
err := cmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("revision", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 1 { if len(args) == 1 {
return compListRevisions(toComplete, cfg, args[0]) return compListRevisions(toComplete, cfg, args[0])
} }
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

@ -19,7 +19,7 @@ package main
import ( import (
"testing" "testing"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
) )
func TestGetCmd(t *testing.T) { func TestGetCmd(t *testing.T) {

@ -23,8 +23,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
) )
const getHooksHelp = ` const getHooksHelp = `
@ -41,13 +41,13 @@ func newGetHooksCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Short: "download all hooks for a named release", Short: "download all hooks for a named release",
Long: getHooksHelp, Long: getHooksHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 { if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp return noMoreArgsComp()
} }
return compListReleases(toComplete, args, cfg) return compListReleases(toComplete, args, cfg)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
res, err := client.Run(args[0]) res, err := client.Run(args[0])
if err != nil { if err != nil {
return err return err
@ -60,7 +60,7 @@ func newGetHooksCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision") cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision")
err := cmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("revision", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 1 { if len(args) == 1 {
return compListRevisions(toComplete, cfg, args[0]) return compListRevisions(toComplete, cfg, args[0])
} }

@ -19,7 +19,7 @@ package main
import ( import (
"testing" "testing"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
) )
func TestGetHooks(t *testing.T) { func TestGetHooks(t *testing.T) {

@ -23,8 +23,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
) )
var getManifestHelp = ` var getManifestHelp = `
@ -43,13 +43,13 @@ func newGetManifestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
Short: "download the manifest for a named release", Short: "download the manifest for a named release",
Long: getManifestHelp, Long: getManifestHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 { if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp return noMoreArgsComp()
} }
return compListReleases(toComplete, args, cfg) return compListReleases(toComplete, args, cfg)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
res, err := client.Run(args[0]) res, err := client.Run(args[0])
if err != nil { if err != nil {
return err return err
@ -60,7 +60,7 @@ func newGetManifestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
} }
cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision") cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision")
err := cmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("revision", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 1 { if len(args) == 1 {
return compListRevisions(toComplete, cfg, args[0]) return compListRevisions(toComplete, cfg, args[0])
} }

@ -19,7 +19,7 @@ package main
import ( import (
"testing" "testing"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
) )
func TestGetManifest(t *testing.T) { func TestGetManifest(t *testing.T) {

@ -0,0 +1,98 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"io"
"log"
"github.com/spf13/cobra"
k8sLabels "k8s.io/apimachinery/pkg/labels"
"helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/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(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return noMoreArgsComp()
}
return compListReleases(toComplete, args, cfg)
},
RunE: func(_ *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(_ *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, "ANNOTATIONS: %v\n", k8sLabels.Set(w.metadata.Annotations).String())
_, _ = fmt.Fprintf(out, "DEPENDENCIES: %v\n", w.metadata.FormattedDepNames())
_, _ = 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)
}

@ -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/v4/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)
}

@ -23,8 +23,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
) )
var getNotesHelp = ` var getNotesHelp = `
@ -39,13 +39,13 @@ func newGetNotesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Short: "download the notes for a named release", Short: "download the notes for a named release",
Long: getNotesHelp, Long: getNotesHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 { if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp return noMoreArgsComp()
} }
return compListReleases(toComplete, args, cfg) return compListReleases(toComplete, args, cfg)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
res, err := client.Run(args[0]) res, err := client.Run(args[0])
if err != nil { if err != nil {
return err return err
@ -59,7 +59,7 @@ func newGetNotesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision") f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
err := cmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("revision", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 1 { if len(args) == 1 {
return compListRevisions(toComplete, cfg, args[0]) return compListRevisions(toComplete, cfg, args[0])
} }

@ -19,7 +19,7 @@ package main
import ( import (
"testing" "testing"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
) )
func TestGetNotesCmd(t *testing.T) { func TestGetNotesCmd(t *testing.T) {

@ -23,9 +23,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
) )
var getValuesHelp = ` var getValuesHelp = `
@ -46,13 +46,13 @@ func newGetValuesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Short: "download the values file for a named release", Short: "download the values file for a named release",
Long: getValuesHelp, Long: getValuesHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 { if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp return noMoreArgsComp()
} }
return compListReleases(toComplete, args, cfg) return compListReleases(toComplete, args, cfg)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
vals, err := client.Run(args[0]) vals, err := client.Run(args[0])
if err != nil { if err != nil {
return err return err
@ -63,7 +63,7 @@ func newGetValuesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision") f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
err := cmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("revision", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 1 { if len(args) == 1 {
return compListRevisions(toComplete, cfg, args[0]) return compListRevisions(toComplete, cfg, args[0])
} }

@ -19,7 +19,7 @@ package main
import ( import (
"testing" "testing"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
) )
func TestGetValuesCmd(t *testing.T) { func TestGetValuesCmd(t *testing.T) {

@ -14,14 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main // import "helm.sh/helm/v3/cmd/helm" package main // import "helm.sh/helm/v4/cmd/helm"
import ( import (
"fmt" "fmt"
"io/ioutil" "io"
"log" "log"
"os" "os"
"strings" "strings"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
@ -29,18 +30,14 @@ import (
// Import to initialize client auth plugins. // Import to initialize client auth plugins.
_ "k8s.io/client-go/plugin/pkg/client/auth" _ "k8s.io/client-go/plugin/pkg/client/auth"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v3/pkg/gates" "helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v3/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake"
kubefake "helm.sh/helm/v3/pkg/kube/fake" "helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/storage/driver"
"helm.sh/helm/v3/pkg/storage/driver"
) )
// FeatureGateOCI is the feature gate for checking if `helm chart` and `helm registry` commands should work
const FeatureGateOCI = gates.Gate("HELM_EXPERIMENTAL_OCI")
var settings = cli.New() var settings = cli.New()
func init() { func init() {
@ -49,7 +46,8 @@ func init() {
func debug(format string, v ...interface{}) { func debug(format string, v ...interface{}) {
if settings.Debug { if settings.Debug {
format = fmt.Sprintf("[debug] %s\n", format) timeNow := time.Now().String()
format = fmt.Sprintf("%s [debug] %s\n", timeNow, format)
log.Output(2, fmt.Sprintf(format, v...)) log.Output(2, fmt.Sprintf(format, v...))
} }
} }
@ -59,6 +57,11 @@ func warning(format string, v ...interface{}) {
fmt.Fprintf(os.Stderr, format, v...) fmt.Fprintf(os.Stderr, format, v...)
} }
// hookOutputWriter provides the writer for writing hook logs.
func hookOutputWriter(_, _, _ string) io.Writer {
return log.Writer()
}
func main() { func main() {
// Setting the name of the app for managedFields in the Kubernetes client. // Setting the name of the app for managedFields in the Kubernetes client.
// It is set here to the full name of "helm" so that renaming of helm to // It is set here to the full name of "helm" so that renaming of helm to
@ -82,6 +85,7 @@ func main() {
if helmDriver == "memory" { if helmDriver == "memory" {
loadReleasesInMemory(actionConfig) loadReleasesInMemory(actionConfig)
} }
actionConfig.SetHookOutputFunc(hookOutputWriter)
}) })
if err := cmd.Execute(); err != nil { if err := cmd.Execute(); err != nil {
@ -95,15 +99,6 @@ func main() {
} }
} }
func checkOCIFeatureGate() func(_ *cobra.Command, _ []string) error {
return func(_ *cobra.Command, _ []string) error {
if !FeatureGateOCI.IsEnabled() {
return FeatureGateOCI.Error()
}
return nil
}
}
// This function loads releases into the memory storage if the // This function loads releases into the memory storage if the
// environment variable is properly set. // environment variable is properly set.
func loadReleasesInMemory(actionConfig *action.Configuration) { func loadReleasesInMemory(actionConfig *action.Configuration) {
@ -119,10 +114,10 @@ func loadReleasesInMemory(actionConfig *action.Configuration) {
return return
} }
actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard} actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard}
for _, path := range filePaths { for _, path := range filePaths {
b, err := ioutil.ReadFile(path) b, err := os.ReadFile(path)
if err != nil { if err != nil {
log.Fatal("Unable to read memory driver data", err) log.Fatal("Unable to read memory driver data", err)
} }

@ -18,7 +18,7 @@ package main
import ( import (
"bytes" "bytes"
"io/ioutil" "io"
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
@ -28,15 +28,15 @@ import (
shellwords "github.com/mattn/go-shellwords" shellwords "github.com/mattn/go-shellwords"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/internal/test" "helm.sh/helm/v4/internal/test"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/chartutil" chartutil "helm.sh/helm/v4/pkg/chart/util"
"helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v4/pkg/cli"
kubefake "helm.sh/helm/v3/pkg/kube/fake" kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v3/pkg/storage" "helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver" "helm.sh/helm/v4/pkg/storage/driver"
"helm.sh/helm/v3/pkg/time" "helm.sh/helm/v4/pkg/time"
) )
func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() } func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() }
@ -60,8 +60,11 @@ func runTestCmd(t *testing.T, tests []cmdTestCase) {
} }
t.Logf("running cmd (attempt %d): %s", i+1, tt.cmd) t.Logf("running cmd (attempt %d): %s", i+1, tt.cmd)
_, out, err := executeActionCommandC(storage, tt.cmd) _, out, err := executeActionCommandC(storage, tt.cmd)
if (err != nil) != tt.wantError { if tt.wantError && err == nil {
t.Errorf("expected error, got '%v'", err) t.Errorf("expected error, got success with the following output:\n%s", out)
}
if !tt.wantError && err != nil {
t.Errorf("expected no error, got: '%v'", err)
} }
if tt.golden != "" { if tt.golden != "" {
test.AssertGoldenString(t, out, tt.golden) test.AssertGoldenString(t, out, tt.golden)
@ -71,27 +74,6 @@ func runTestCmd(t *testing.T, tests []cmdTestCase) {
} }
} }
func runTestActionCmd(t *testing.T, tests []cmdTestCase) {
t.Helper()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer resetEnv()()
store := storageFixture()
for _, rel := range tt.rels {
store.Create(rel)
}
_, out, err := executeActionCommandC(store, tt.cmd)
if (err != nil) != tt.wantError {
t.Errorf("expected error, got '%v'", err)
}
if tt.golden != "" {
test.AssertGoldenString(t, out, tt.golden)
}
})
}
}
func storageFixture() *storage.Storage { func storageFixture() *storage.Storage {
return storage.Init(driver.NewMemory()) return storage.Init(driver.NewMemory())
} }
@ -110,9 +92,9 @@ func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string)
actionConfig := &action.Configuration{ actionConfig := &action.Configuration{
Releases: store, Releases: store,
KubeClient: &kubefake.PrintingKubeClient{Out: ioutil.Discard}, KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard},
Capabilities: chartutil.DefaultCapabilities, Capabilities: chartutil.DefaultCapabilities,
Log: func(format string, v ...interface{}) {}, Log: func(_ string, _ ...interface{}) {},
} }
root, err := newRootCmd(actionConfig, buf, args) root, err := newRootCmd(actionConfig, buf, args)

@ -20,19 +20,18 @@ import (
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil" releaseutil "helm.sh/helm/v4/pkg/release/util"
helmtime "helm.sh/helm/v3/pkg/time" helmtime "helm.sh/helm/v4/pkg/time"
) )
var historyHelp = ` var historyHelp = `
@ -61,13 +60,13 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Short: "fetch release history", Short: "fetch release history",
Aliases: []string{"hist"}, Aliases: []string{"hist"},
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 { if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp return noMoreArgsComp()
} }
return compListReleases(toComplete, args, cfg) return compListReleases(toComplete, args, cfg)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
history, err := getHistory(client, args[0]) history, err := getHistory(client, args[0])
if err != nil { if err != nil {
return err return err
@ -137,7 +136,7 @@ func getHistory(client *action.History, name string) (releaseHistory, error) {
func getReleaseHistory(rls []*release.Release) (history releaseHistory) { func getReleaseHistory(rls []*release.Release) (history releaseHistory) {
for i := len(rls) - 1; i >= 0; i-- { for i := len(rls) - 1; i >= 0; i-- {
r := rls[i] r := rls[i]
c := formatChartname(r.Chart) c := formatChartName(r.Chart)
s := r.Info.Status.String() s := r.Info.Status.String()
v := r.Version v := r.Version
d := r.Info.Description d := r.Info.Description
@ -160,7 +159,7 @@ func getReleaseHistory(rls []*release.Release) (history releaseHistory) {
return history return history
} }
func formatChartname(c *chart.Chart) string { func formatChartName(c *chart.Chart) string {
if c == nil || c.Metadata == nil { if c == nil || c.Metadata == nil {
// This is an edge case that has happened in prod, though we don't // This is an edge case that has happened in prod, though we don't
// know how: https://github.com/helm/helm/issues/1347 // know how: https://github.com/helm/helm/issues/1347
@ -178,25 +177,15 @@ func formatAppVersion(c *chart.Chart) string {
return c.AppVersion() return c.AppVersion()
} }
func min(x, y int) int { func compListRevisions(_ string, cfg *action.Configuration, releaseName string) ([]string, cobra.ShellCompDirective) {
if x < y {
return x
}
return y
}
func compListRevisions(toComplete string, cfg *action.Configuration, releaseName string) ([]string, cobra.ShellCompDirective) {
client := action.NewHistory(cfg) client := action.NewHistory(cfg)
var revisions []string var revisions []string
if hist, err := client.Run(releaseName); err == nil { if hist, err := client.Run(releaseName); err == nil {
for _, release := range hist { for _, version := range hist {
version := strconv.Itoa(release.Version) appVersion := fmt.Sprintf("App: %s", version.Chart.Metadata.AppVersion)
if strings.HasPrefix(version, toComplete) { chartDesc := fmt.Sprintf("Chart: %s-%s", version.Chart.Metadata.Name, version.Chart.Metadata.Version)
appVersion := fmt.Sprintf("App: %s", release.Chart.Metadata.AppVersion) revisions = append(revisions, fmt.Sprintf("%s\t%s, %s", strconv.Itoa(version.Version), appVersion, chartDesc))
chartDesc := fmt.Sprintf("Chart: %s-%s", release.Chart.Metadata.Name, release.Chart.Metadata.Version)
revisions = append(revisions, fmt.Sprintf("%s\t%s, %s", version, appVersion, chartDesc))
}
} }
return revisions, cobra.ShellCompDirectiveNoFileComp return revisions, cobra.ShellCompDirectiveNoFileComp
} }

@ -20,7 +20,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
) )
func TestHistoryCmd(t *testing.T) { func TestHistoryCmd(t *testing.T) {
@ -95,6 +95,11 @@ func revisionFlagCompletionTest(t *testing.T, cmdName string) {
cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName), cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName),
rels: releases, rels: releases,
golden: "output/revision-comp.txt", golden: "output/revision-comp.txt",
}, {
name: "completion for revision flag, no filter",
cmd: fmt.Sprintf("__complete %s musketeers --revision 1", cmdName),
rels: releases,
golden: "output/revision-comp.txt",
}, { }, {
name: "completion for revision flag with too few args", name: "completion for revision flag with too few args",
cmd: fmt.Sprintf("__complete %s --revision ''", cmdName), cmd: fmt.Sprintf("__complete %s --revision ''", cmdName),

@ -30,15 +30,15 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v4/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v4/pkg/downloader"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
) )
const installDesc = ` const installDesc = `
@ -49,9 +49,10 @@ a path to an unpacked chart directory or a URL.
To override values in a chart, use either the '--values' flag and pass in a file To override values in a chart, use either the '--values' flag and pass in a file
or use the '--set' flag and pass configuration from the command line, to force or use the '--set' flag and pass configuration from the command line, to force
a string value use '--set-string'. In case a value is large and therefore a string value use '--set-string'. You can use '--set-file' to set individual
you want not to use neither '--values' nor '--set', use '--set-file' to read the values from a file when the value itself is too long for the command line
single large value from file. or is dynamically generated. You can also use '--set-json' to set json values
(scalars/objects/arrays) from the command line. Additionally, you can use '--set-json' and passing json object as a string.
$ helm install -f myvalues.yaml myredis ./redis $ helm install -f myvalues.yaml myredis ./redis
@ -67,6 +68,14 @@ or
$ helm install --set-file my_script=dothings.sh myredis ./redis $ helm install --set-file my_script=dothings.sh myredis ./redis
or
$ helm install --set-json 'master.sidecars=[{"name":"sidecar","image":"myImage","imagePullPolicy":"Always","ports":[{"name":"portname","containerPort":1234}]}]' myredis ./redis
or
$ helm install --set-json '{"master":{"sidecars":[{"name":"sidecar","image":"myImage","imagePullPolicy":"Always","ports":[{"name":"portname","containerPort":1234}]}]}}' myredis ./redis
You can specify the '--values'/'-f' flag multiple times. The priority will be given to the You can specify the '--values'/'-f' flag multiple times. The priority will be given to the
last (right-most) file specified. For example, if both myvalues.yaml and override.yaml last (right-most) file specified. For example, if both myvalues.yaml and override.yaml
contained a key called 'Test', the value set in override.yaml would take precedence: contained a key called 'Test', the value set in override.yaml would take precedence:
@ -79,20 +88,32 @@ set for a key called 'foo', the 'newbar' value would take precedence:
$ helm install --set foo=bar --set foo=newbar myredis ./redis $ helm install --set foo=bar --set foo=newbar myredis ./redis
Similarly, in the following example 'foo' is set to '["four"]':
$ helm install --set-json='foo=["one", "two", "three"]' --set-json='foo=["four"]' myredis ./redis
And in the following example, 'foo' is set to '{"key1":"value1","key2":"bar"}':
$ helm install --set-json='foo={"key1":"value1","key2":"value2"}' --set-json='foo.key2="bar"' myredis ./redis
To check the generated manifests of a release without installing the chart, To check the generated manifests of a release without installing the chart,
the '--debug' and '--dry-run' flags can be combined. the --debug and --dry-run flags can be combined.
The --dry-run flag will output all generated chart manifests, including Secrets
which can contain sensitive values. To hide Kubernetes Secrets use the
--hide-secret flag. Please carefully consider how and when these flags are used.
If --verify is set, the chart MUST have a provenance file, and the provenance If --verify is set, the chart MUST have a provenance file, and the provenance
file MUST pass all verification steps. file MUST pass all verification steps.
There are five different ways you can express the chart you want to install: There are six different ways you can express the chart you want to install:
1. By chart reference: helm install mymaria example/mariadb 1. By chart reference: helm install mymaria example/mariadb
2. By path to a packaged chart: helm install mynginx ./nginx-1.2.3.tgz 2. By path to a packaged chart: helm install mynginx ./nginx-1.2.3.tgz
3. By path to an unpacked chart directory: helm install mynginx ./nginx 3. By path to an unpacked chart directory: helm install mynginx ./nginx
4. By absolute URL: helm install mynginx https://example.com/charts/nginx-1.2.3.tgz 4. By absolute URL: helm install mynginx https://example.com/charts/nginx-1.2.3.tgz
5. By chart reference and repo url: helm install --repo https://example.com/charts/ mynginx nginx 5. By chart reference and repo url: helm install --repo https://example.com/charts/ mynginx nginx
6. By OCI registries: helm install mynginx --version 1.2.3 oci://example.com/charts/nginx
CHART REFERENCES CHART REFERENCES
@ -118,20 +139,42 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Short: "install a chart", Short: "install a chart",
Long: installDesc, Long: installDesc,
Args: require.MinimumNArgs(1), Args: require.MinimumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstall(args, toComplete, client) return compInstall(args, toComplete, client)
}, },
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
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) rel, err := runInstall(args, client, valueOpts, out)
if err != nil { if err != nil {
return errors.Wrap(err, "INSTALLATION FAILED") return errors.Wrap(err, "INSTALLATION FAILED")
} }
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}) return outfmt.Write(out, &statusPrinter{
release: rel,
debug: settings.Debug,
showMetadata: false,
hideNotes: client.HideNotes,
})
}, },
} }
addInstallFlags(cmd, cmd.Flags(), client, valueOpts) addInstallFlags(cmd, cmd.Flags(), client, valueOpts)
// hide-secret is not available in all places the install flags are used so
// it is added separately
f := cmd.Flags()
f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag")
bindOutputFlag(cmd, &outfmt) bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer) bindPostRenderFlag(cmd, &client.PostRenderer)
@ -140,9 +183,16 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) { 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.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.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production") f.BoolVar(&client.Replace, "replace", false, "reuse the given name, only if that name is a deleted release which remains in the history. This is unsafe in production")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout") f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout")
f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout") f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout")
@ -155,10 +205,15 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
f.BoolVar(&client.Atomic, "atomic", false, "if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used") f.BoolVar(&client.Atomic, "atomic", false, "if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used")
f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed. By default, CRDs are installed if not already present") f.BoolVar(&client.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.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")
f.BoolVar(&client.HideNotes, "hide-notes", false, "if set, do not show notes in install output. Does not affect presence in chart metadata")
f.BoolVar(&client.TakeOwnership, "take-ownership", false, "if set, install will ignore the check for helm annotations and take ownership of the existing resources")
addValueOptionsFlags(f, valueOpts) addValueOptionsFlags(f, valueOpts)
addChartPathOptionsFlags(f, &client.ChartPathOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions)
err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("version", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
requiredArgs := 2 requiredArgs := 2
if client.GenerateName { if client.GenerateName {
requiredArgs = 1 requiredArgs = 1
@ -168,7 +223,6 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
} }
return compVersionFlag(args[requiredArgs-1], toComplete) return compVersionFlag(args[requiredArgs-1], toComplete)
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -187,10 +241,6 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
} }
client.ReleaseName = name client.ReleaseName = name
if err := checkOCI(chart); err != nil {
return nil, err
}
cp, err := client.ChartPathOptions.LocateChart(chart, settings) cp, err := client.ChartPathOptions.LocateChart(chart, settings)
if err != nil { if err != nil {
return nil, err return nil, err
@ -234,6 +284,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
RepositoryConfig: settings.RepositoryConfig, RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache, RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug, Debug: settings.Debug,
RegistryClient: client.GetRegistryClient(),
} }
if err := man.Update(); err != nil { if err := man.Update(); err != nil {
return nil, err return nil, err
@ -250,6 +301,11 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
client.Namespace = settings.Namespace() 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 // Create context and prepare the handle of SIGTERM
ctx := context.Background() ctx := context.Background()
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
@ -290,3 +346,19 @@ func compInstall(args []string, toComplete string, client *action.Install) ([]st
} }
return nil, cobra.ShellCompDirectiveNoFileComp 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
}

@ -23,23 +23,17 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"helm.sh/helm/v3/pkg/repo/repotest" "helm.sh/helm/v4/pkg/repo/repotest"
) )
func TestInstall(t *testing.T) { func TestInstall(t *testing.T) {
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*") srv := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"),
} repotest.WithMiddleware(repotest.BasicAuthMiddleware(t)),
)
defer srv.Stop() defer srv.Stop()
srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "username" || password != "password" {
t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password)
}
}))
srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.FileServer(http.Dir(srv.Root())).ServeHTTP(w, r) http.FileServer(http.Dir(srv.Root())).ServeHTTP(w, r)
})) }))
@ -96,12 +90,18 @@ func TestInstall(t *testing.T) {
golden: "output/install-no-args.txt", golden: "output/install-no-args.txt",
wantError: true, wantError: true,
}, },
// Install, re-use name // Install, reuse name
{ {
name: "install and replace release", name: "install and replace release",
cmd: "install aeneas testdata/testcharts/empty --replace", cmd: "install aeneas testdata/testcharts/empty --replace",
golden: "output/install-and-replace.txt", golden: "output/install-and-replace.txt",
}, },
// Install, take ownership
{
name: "install and replace release",
cmd: "install aeneas-take-ownership testdata/testcharts/empty --take-ownership",
golden: "output/install-and-take-ownership.txt",
},
// Install, with timeout // Install, with timeout
{ {
name: "install with a timeout", name: "install with a timeout",
@ -123,7 +123,7 @@ func TestInstall(t *testing.T) {
// Install, using the name-template // Install, using the name-template
{ {
name: "install with name-template", name: "install with name-template",
cmd: "install testdata/testcharts/empty --name-template '{{upper \"foobar\"}}'", cmd: "install testdata/testcharts/empty --name-template '{{ \"foobar\"}}'",
golden: "output/install-name-template.txt", golden: "output/install-name-template.txt",
}, },
// Install, perform chart verification along the way. // Install, perform chart verification along the way.
@ -225,6 +225,12 @@ func TestInstall(t *testing.T) {
wantError: true, wantError: true,
golden: "output/subchart-schema-cli-negative.txt", golden: "output/subchart-schema-cli-negative.txt",
}, },
// Install, values from yaml, schematized with errors but skip schema validation, expect success
{
name: "install with schema file and schematized subchart, extra values from cli, skip schema validation",
cmd: "install schema testdata/testcharts/chart-with-schema-and-subchart --set lastname=doe --set subchart-with-schema.age=-25 --skip-schema-validation",
golden: "output/schema.txt",
},
// Install deprecated chart // Install deprecated chart
{ {
name: "install with warning about deprecated chart", name: "install with warning about deprecated chart",
@ -252,9 +258,25 @@ func TestInstall(t *testing.T) {
cmd: fmt.Sprintf("install aeneas test/reqtest --username username --password password --repository-config %s --repository-cache %s", repoFile, srv.Root()), cmd: fmt.Sprintf("install aeneas test/reqtest --username username --password password --repository-config %s --repository-cache %s", repoFile, srv.Root()),
golden: "output/install.txt", golden: "output/install.txt",
}, },
{
name: "dry-run displaying secret",
cmd: "install secrets testdata/testcharts/chart-with-secret --dry-run",
golden: "output/install-dry-run-with-secret.txt",
},
{
name: "dry-run hiding secret",
cmd: "install secrets testdata/testcharts/chart-with-secret --dry-run --hide-secret",
golden: "output/install-dry-run-with-secret-hidden.txt",
},
{
name: "hide-secret error without dry-run",
cmd: "install secrets testdata/testcharts/chart-with-secret --hide-secret",
wantError: true,
golden: "output/install-hide-secret.txt",
},
} }
runTestActionCmd(t, tests) runTestCmd(t, tests)
} }
func TestInstallOutputCompletion(t *testing.T) { func TestInstallOutputCompletion(t *testing.T) {
@ -275,6 +297,10 @@ func TestInstallVersionCompletion(t *testing.T) {
name: "completion for install version flag with generate-name", name: "completion for install version flag with generate-name",
cmd: fmt.Sprintf("%s __complete install --generate-name testing/alpine --version ''", repoSetup), cmd: fmt.Sprintf("%s __complete install --generate-name testing/alpine --version ''", repoSetup),
golden: "output/version-comp.txt", golden: "output/version-comp.txt",
}, {
name: "completion for install version flag, no filter",
cmd: fmt.Sprintf("%s __complete install releasename testing/alpine --version 0.3", repoSetup),
golden: "output/version-comp.txt",
}, { }, {
name: "completion for install version flag too few args", name: "completion for install version flag too few args",
cmd: fmt.Sprintf("%s __complete install testing/alpine --version ''", repoSetup), cmd: fmt.Sprintf("%s __complete install testing/alpine --version ''", repoSetup),

@ -26,9 +26,11 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/cli/values" chartutil "helm.sh/helm/v4/pkg/chart/util"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/lint/support"
) )
var longLintHelp = ` var longLintHelp = `
@ -43,19 +45,29 @@ or recommendation, it will emit [WARNING] messages.
func newLintCmd(out io.Writer) *cobra.Command { func newLintCmd(out io.Writer) *cobra.Command {
client := action.NewLint() client := action.NewLint()
valueOpts := &values.Options{} valueOpts := &values.Options{}
var kubeVersion string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "lint PATH", Use: "lint PATH",
Short: "examine a chart for possible issues", Short: "examine a chart for possible issues",
Long: longLintHelp, Long: longLintHelp,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
paths := []string{"."} paths := []string{"."}
if len(args) > 0 { if len(args) > 0 {
paths = args paths = args
} }
if kubeVersion != "" {
parsedKubeVersion, err := chartutil.ParseKubeVersion(kubeVersion)
if err != nil {
return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err)
}
client.KubeVersion = parsedKubeVersion
}
if client.WithSubcharts { if client.WithSubcharts {
for _, p := range paths { for _, p := range paths {
filepath.Walk(filepath.Join(p, "charts"), func(path string, info os.FileInfo, err error) error { filepath.Walk(filepath.Join(p, "charts"), func(path string, info os.FileInfo, _ error) error {
if info != nil { if info != nil {
if info.Name() == "Chart.yaml" { if info.Name() == "Chart.yaml" {
paths = append(paths, filepath.Dir(path)) paths = append(paths, filepath.Dir(path))
@ -76,12 +88,23 @@ func newLintCmd(out io.Writer) *cobra.Command {
var message strings.Builder var message strings.Builder
failed := 0 failed := 0
errorsOrWarnings := 0
for _, path := range paths { for _, path := range paths {
fmt.Fprintf(&message, "==> Linting %s\n", path)
result := client.Run([]string{path}, vals) result := client.Run([]string{path}, vals)
// If there is no errors/warnings and quiet flag is set
// go to the next chart
hasWarningsOrErrors := action.HasWarningsOrErrors(result)
if hasWarningsOrErrors {
errorsOrWarnings++
}
if client.Quiet && !hasWarningsOrErrors {
continue
}
fmt.Fprintf(&message, "==> Linting %s\n", path)
// All the Errors that are generated by a chart // All the Errors that are generated by a chart
// that failed a lint will be included in the // that failed a lint will be included in the
// results.Messages so we only need to print // results.Messages so we only need to print
@ -93,7 +116,9 @@ func newLintCmd(out io.Writer) *cobra.Command {
} }
for _, msg := range result.Messages { for _, msg := range result.Messages {
fmt.Fprintf(&message, "%s\n", msg) if !client.Quiet || msg.Severity > support.InfoSev {
fmt.Fprintf(&message, "%s\n", msg)
}
} }
if len(result.Errors) != 0 { if len(result.Errors) != 0 {
@ -112,7 +137,9 @@ func newLintCmd(out io.Writer) *cobra.Command {
if failed > 0 { if failed > 0 {
return errors.New(summary) return errors.New(summary)
} }
fmt.Fprintln(out, summary) if !client.Quiet || errorsOrWarnings > 0 {
fmt.Fprintln(out, summary)
}
return nil return nil
}, },
} }
@ -120,6 +147,9 @@ func newLintCmd(out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings") f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings")
f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts") f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
f.BoolVar(&client.Quiet, "quiet", false, "print only warnings and errors")
f.BoolVar(&client.SkipSchemaValidation, "skip-schema-validation", false, "if set, disables JSON schema validation")
f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for capabilities and deprecation checks")
addValueOptionsFlags(f, valueOpts) addValueOptionsFlags(f, valueOpts)
return cmd return cmd

@ -37,6 +37,60 @@ func TestLintCmdWithSubchartsFlag(t *testing.T) {
runTestCmd(t, tests) runTestCmd(t, tests)
} }
func TestLintCmdWithQuietFlag(t *testing.T) {
testChart1 := "testdata/testcharts/alpine"
testChart2 := "testdata/testcharts/chart-bad-requirements"
tests := []cmdTestCase{{
name: "lint good chart using --quiet flag",
cmd: fmt.Sprintf("lint --quiet %s", testChart1),
golden: "output/lint-quiet.txt",
}, {
name: "lint two charts, one with error using --quiet flag",
cmd: fmt.Sprintf("lint --quiet %s %s", testChart1, testChart2),
golden: "output/lint-quiet-with-error.txt",
wantError: true,
}, {
name: "lint chart with warning using --quiet flag",
cmd: "lint --quiet testdata/testcharts/chart-with-only-crds",
golden: "output/lint-quiet-with-warning.txt",
}, {
name: "lint non-existent chart using --quiet flag",
cmd: "lint --quiet thischartdoesntexist/",
golden: "",
wantError: true,
}}
runTestCmd(t, tests)
}
func TestLintCmdWithKubeVersionFlag(t *testing.T) {
testChart := "testdata/testcharts/chart-with-deprecated-api"
tests := []cmdTestCase{{
name: "lint chart with deprecated api version using kube version flag",
cmd: fmt.Sprintf("lint --kube-version 1.22.0 %s", testChart),
golden: "output/lint-chart-with-deprecated-api.txt",
wantError: false,
}, {
name: "lint chart with deprecated api version using kube version and strict flag",
cmd: fmt.Sprintf("lint --kube-version 1.22.0 --strict %s", testChart),
golden: "output/lint-chart-with-deprecated-api-strict.txt",
wantError: true,
}, {
// the test builds will use the default k8sVersionMinor const in deprecations.go and capabilities.go
// which is "20"
name: "lint chart with deprecated api version without kube version",
cmd: fmt.Sprintf("lint %s", testChart),
golden: "output/lint-chart-with-deprecated-api-old-k8s.txt",
wantError: false,
}, {
name: "lint chart with deprecated api version with older kube version",
cmd: fmt.Sprintf("lint --kube-version 1.21.0 --strict %s", testChart),
golden: "output/lint-chart-with-deprecated-api-old-k8s.txt",
wantError: false,
}}
runTestCmd(t, tests)
}
func TestLintFileCompletion(t *testing.T) { func TestLintFileCompletion(t *testing.T) {
checkFileCompletion(t, "lint", true) checkFileCompletion(t, "lint", true)
checkFileCompletion(t, "lint mypath", true) // Multiple paths can be given checkFileCompletion(t, "lint mypath", true) // Multiple paths can be given

@ -25,10 +25,10 @@ import (
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
) )
var listHelp = ` var listHelp = `
@ -68,8 +68,8 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: listHelp, Long: listHelp,
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Args: require.NoArgs, Args: require.NoArgs,
ValidArgsFunction: noCompletions, ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
if client.AllNamespaces { if client.AllNamespaces {
if err := cfg.Init(settings.RESTClientGetter(), "", os.Getenv("HELM_DRIVER"), debug); err != nil { if err := cfg.Init(settings.RESTClientGetter(), "", os.Getenv("HELM_DRIVER"), debug); err != nil {
return err return err
@ -83,8 +83,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
if client.Short { if client.Short {
names := make([]string, 0, len(results))
names := make([]string, 0)
for _, res := range results { for _, res := range results {
names = append(names, res.Name) names = append(names, res.Name)
} }
@ -103,17 +102,16 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
fmt.Fprintln(out, res.Name) fmt.Fprintln(out, res.Name)
} }
return nil return nil
default:
return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat))
} }
} }
return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat)) return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat, client.NoHeaders))
}, },
} }
f := cmd.Flags() f := cmd.Flags()
f.BoolVarP(&client.Short, "short", "q", false, "output short (quiet) listing format") f.BoolVarP(&client.Short, "short", "q", false, "output short (quiet) listing format")
f.BoolVarP(&client.NoHeaders, "no-headers", "", false, "don't print headers when using the default output format")
f.StringVar(&client.TimeFormat, "time-format", "", `format time using golang time formatter. Example: --time-format "2006-01-02 15:04:05Z0700"`) f.StringVar(&client.TimeFormat, "time-format", "", `format time using golang time formatter. Example: --time-format "2006-01-02 15:04:05Z0700"`)
f.BoolVarP(&client.ByDate, "date", "d", false, "sort by release date") f.BoolVarP(&client.ByDate, "date", "d", false, "sort by release date")
f.BoolVarP(&client.SortReverse, "reverse", "r", false, "reverse the sort order") f.BoolVarP(&client.SortReverse, "reverse", "r", false, "reverse the sort order")
@ -145,10 +143,11 @@ type releaseElement struct {
} }
type releaseListWriter struct { type releaseListWriter struct {
releases []releaseElement releases []releaseElement
noHeaders bool
} }
func newReleaseListWriter(releases []*release.Release, timeFormat string) *releaseListWriter { func newReleaseListWriter(releases []*release.Release, timeFormat string, noHeaders bool) *releaseListWriter {
// Initialize the array so no results returns an empty array instead of null // Initialize the array so no results returns an empty array instead of null
elements := make([]releaseElement, 0, len(releases)) elements := make([]releaseElement, 0, len(releases))
for _, r := range releases { for _, r := range releases {
@ -157,7 +156,7 @@ func newReleaseListWriter(releases []*release.Release, timeFormat string) *relea
Namespace: r.Namespace, Namespace: r.Namespace,
Revision: strconv.Itoa(r.Version), Revision: strconv.Itoa(r.Version),
Status: r.Info.Status.String(), Status: r.Info.Status.String(),
Chart: formatChartname(r.Chart), Chart: formatChartName(r.Chart),
AppVersion: formatAppVersion(r.Chart), AppVersion: formatAppVersion(r.Chart),
} }
@ -173,12 +172,14 @@ func newReleaseListWriter(releases []*release.Release, timeFormat string) *relea
elements = append(elements, element) elements = append(elements, element)
} }
return &releaseListWriter{elements} return &releaseListWriter{elements, noHeaders}
} }
func (r *releaseListWriter) WriteTable(out io.Writer) error { func (r *releaseListWriter) WriteTable(out io.Writer) error {
table := uitable.New() table := uitable.New()
table.AddRow("NAME", "NAMESPACE", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION") if !r.noHeaders {
table.AddRow("NAME", "NAMESPACE", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION")
}
for _, r := range r.releases { for _, r := range r.releases {
table.AddRow(r.Name, r.Namespace, r.Revision, r.Updated, r.Status, r.Chart, r.AppVersion) table.AddRow(r.Name, r.Namespace, r.Revision, r.Updated, r.Status, r.Chart, r.AppVersion)
} }
@ -224,7 +225,14 @@ func compListReleases(toComplete string, ignoredReleaseNames []string, cfg *acti
client := action.NewList(cfg) client := action.NewList(cfg)
client.All = true client.All = true
client.Limit = 0 client.Limit = 0
client.Filter = fmt.Sprintf("^%s", toComplete) // Do not filter so as to get the entire list of releases.
// This will allow zsh and fish to match completion choices
// on other criteria then prefix. For example:
// helm status ingress<TAB>
// can match
// helm status nginx-ingress
//
// client.Filter = fmt.Sprintf("^%s", toComplete)
client.SetStateMask() client.SetStateMask()
releases, err := client.Run() releases, err := client.Run()

@ -19,9 +19,9 @@ package main
import ( import (
"testing" "testing"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v3/pkg/time" "helm.sh/helm/v4/pkg/time"
) )
func TestListCmd(t *testing.T) { func TestListCmd(t *testing.T) {
@ -148,6 +148,11 @@ func TestListCmd(t *testing.T) {
cmd: "list", cmd: "list",
golden: "output/list.txt", golden: "output/list.txt",
rels: releaseFixture, rels: releaseFixture,
}, {
name: "list without headers",
cmd: "list --no-headers",
golden: "output/list-no-headers.txt",
rels: releaseFixture,
}, { }, {
name: "list all releases", name: "list all releases",
cmd: "list --all", cmd: "list --all",

@ -19,7 +19,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"os/exec" "os/exec"
@ -32,7 +31,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"helm.sh/helm/v3/pkg/plugin" "helm.sh/helm/v4/pkg/plugin"
) )
const ( const (
@ -129,7 +128,8 @@ func callPluginExecutable(pluginName string, main string, argv []string, out io.
env = append(env, fmt.Sprintf("%s=%s", k, v)) 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.Env = env
prog.Stdin = os.Stdin prog.Stdin = os.Stdin
prog.Stdout = out prog.Stdout = out
@ -154,7 +154,7 @@ func callPluginExecutable(pluginName string, main string, argv []string, out io.
func manuallyProcessArgs(args []string) ([]string, []string) { func manuallyProcessArgs(args []string) ([]string, []string) {
known := []string{} known := []string{}
unknown := []string{} unknown := []string{}
kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--kube-as-user", "--kube-as-group", "--kube-ca-file", "--registry-config", "--repository-cache", "--repository-config"} kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--kube-as-user", "--kube-as-group", "--kube-ca-file", "--registry-config", "--repository-cache", "--repository-config", "--kube-insecure-skip-tls-verify", "--kube-tls-server-name"}
knownArg := func(a string) bool { knownArg := func(a string) bool {
for _, pre := range kvargs { for _, pre := range kvargs {
if strings.HasPrefix(a, pre+"=") { if strings.HasPrefix(a, pre+"=") {
@ -286,7 +286,7 @@ func addPluginCommands(plugin *plugin.Plugin, baseCmd *cobra.Command, cmds *plug
f.BoolP(longs[i], shorts[i], false, "") f.BoolP(longs[i], shorts[i], false, "")
} else { } else {
// Create a long flag with the same name as the short flag. // Create a long flag with the same name as the short flag.
// Not a perfect solution, but its better than ignoring the extra short flags. // Not a perfect solution, but it's better than ignoring the extra short flags.
f.BoolP(shorts[i], shorts[i], false, "") f.BoolP(shorts[i], shorts[i], false, "")
} }
} }
@ -301,7 +301,7 @@ func addPluginCommands(plugin *plugin.Plugin, baseCmd *cobra.Command, cmds *plug
// to the dynamic completion script of the plugin. // to the dynamic completion script of the plugin.
DisableFlagParsing: true, DisableFlagParsing: true,
// A Run is required for it to be a valid command without subcommands // A Run is required for it to be a valid command without subcommands
Run: func(cmd *cobra.Command, args []string) {}, Run: func(_ *cobra.Command, _ []string) {},
} }
baseCmd.AddCommand(subCmd) baseCmd.AddCommand(subCmd)
addPluginCommands(plugin, subCmd, &cmd) addPluginCommands(plugin, subCmd, &cmd)
@ -311,9 +311,9 @@ func addPluginCommands(plugin *plugin.Plugin, baseCmd *cobra.Command, cmds *plug
// loadFile takes a yaml file at the given path, parses it and returns a pluginCommand object // loadFile takes a yaml file at the given path, parses it and returns a pluginCommand object
func loadFile(path string) (*pluginCommand, error) { func loadFile(path string) (*pluginCommand, error) {
cmds := new(pluginCommand) cmds := new(pluginCommand)
b, err := ioutil.ReadFile(path) b, err := os.ReadFile(path)
if err != nil { if err != nil {
return cmds, errors.New(fmt.Sprintf("File (%s) not provided by plugin. No plugin auto-completion possible.", path)) return cmds, fmt.Errorf("file (%s) not provided by plugin. No plugin auto-completion possible", path)
} }
err = yaml.Unmarshal(b, cmds) err = yaml.Unmarshal(b, cmds)

@ -19,17 +19,16 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v4/pkg/downloader"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v4/pkg/getter"
) )
const packageDesc = ` const packageDesc = `
@ -56,7 +55,7 @@ func newPackageCmd(out io.Writer) *cobra.Command {
Use: "package [CHART_PATH] [...]", Use: "package [CHART_PATH] [...]",
Short: "package a chart directory into a chart archive", Short: "package a chart directory into a chart archive",
Long: packageDesc, Long: packageDesc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return errors.Errorf("need at least one argument, the path to the chart") return errors.Errorf("need at least one argument, the path to the chart")
} }
@ -76,6 +75,12 @@ func newPackageCmd(out io.Writer) *cobra.Command {
return err return err
} }
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
for i := 0; i < len(args); i++ { for i := 0; i < len(args); i++ {
path, err := filepath.Abs(args[i]) path, err := filepath.Abs(args[i])
if err != nil { if err != nil {
@ -87,11 +92,12 @@ func newPackageCmd(out io.Writer) *cobra.Command {
if client.DependencyUpdate { if client.DependencyUpdate {
downloadManager := &downloader.Manager{ downloadManager := &downloader.Manager{
Out: ioutil.Discard, Out: io.Discard,
ChartPath: path, ChartPath: path,
Keyring: client.Keyring, Keyring: client.Keyring,
Getters: p, Getters: p,
Debug: settings.Debug, Debug: settings.Debug,
RegistryClient: registryClient,
RepositoryConfig: settings.RepositoryConfig, RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache, RepositoryCache: settings.RepositoryCache,
} }
@ -119,6 +125,13 @@ func newPackageCmd(out io.Writer) *cobra.Command {
f.StringVar(&client.AppVersion, "app-version", "", "set the appVersion on the chart to this version") f.StringVar(&client.AppVersion, "app-version", "", "set the appVersion on the chart to this version")
f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.") f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.")
f.BoolVarP(&client.DependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`) f.BoolVarP(&client.DependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`)
f.StringVar(&client.Username, "username", "", "chart repository username where to locate the requested chart")
f.StringVar(&client.Password, "password", "", "chart repository password where to locate the requested chart")
f.StringVar(&client.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&client.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.BoolVar(&client.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&client.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download")
f.StringVar(&client.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
return cmd return cmd
} }

@ -16,17 +16,15 @@ limitations under the License.
package main package main
import ( import (
"bytes" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings"
"testing" "testing"
"github.com/spf13/cobra" "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/loader"
"helm.sh/helm/v3/internal/test/ensure"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
) )
func TestPackage(t *testing.T) { func TestPackage(t *testing.T) {
@ -112,21 +110,18 @@ func TestPackage(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
cachePath := ensure.TempDir(t) cachePath := t.TempDir()
defer testChdir(t, cachePath)() defer testChdir(t, cachePath)()
if err := os.MkdirAll("toot", 0777); err != nil { if err := os.MkdirAll("toot", 0777); err != nil {
t.Fatal(err) t.Fatal(err)
} }
var buf bytes.Buffer
c := newPackageCmd(&buf)
// This is an unfortunate byproduct of the tmpdir // This is an unfortunate byproduct of the tmpdir
if v, ok := tt.flags["keyring"]; ok && len(v) > 0 { if v, ok := tt.flags["keyring"]; ok && len(v) > 0 {
tt.flags["keyring"] = filepath.Join(origDir, v) tt.flags["keyring"] = filepath.Join(origDir, v)
} }
setFlags(c, tt.flags)
re := regexp.MustCompile(tt.expect) re := regexp.MustCompile(tt.expect)
adjustedArgs := make([]string, len(tt.args)) adjustedArgs := make([]string, len(tt.args))
@ -134,7 +129,16 @@ func TestPackage(t *testing.T) {
adjustedArgs[i] = filepath.Join(origDir, f) adjustedArgs[i] = filepath.Join(origDir, f)
} }
err := c.RunE(c, adjustedArgs) cmd := []string{"package"}
if len(adjustedArgs) > 0 {
cmd = append(cmd, adjustedArgs...)
}
for k, v := range tt.flags {
if v != "0" {
cmd = append(cmd, fmt.Sprintf("--%s=%s", k, v))
}
}
_, _, err = executeActionCommand(strings.Join(cmd, " "))
if err != nil { if err != nil {
if tt.err && re.MatchString(err.Error()) { if tt.err && re.MatchString(err.Error()) {
return return
@ -142,10 +146,6 @@ func TestPackage(t *testing.T) {
t.Fatalf("%q: expected error %q, got %q", tt.name, tt.expect, err) t.Fatalf("%q: expected error %q, got %q", tt.name, tt.expect, err)
} }
if !re.Match(buf.Bytes()) {
t.Errorf("%q: expected output %q, got %q", tt.name, tt.expect, buf.String())
}
if len(tt.hasfile) > 0 { if len(tt.hasfile) > 0 {
if fi, err := os.Stat(tt.hasfile); err != nil { if fi, err := os.Stat(tt.hasfile); err != nil {
t.Errorf("%q: expected file %q, got err %q", tt.name, tt.hasfile, err) t.Errorf("%q: expected file %q, got err %q", tt.name, tt.hasfile, err)
@ -168,26 +168,21 @@ func TestPackage(t *testing.T) {
func TestSetAppVersion(t *testing.T) { func TestSetAppVersion(t *testing.T) {
var ch *chart.Chart var ch *chart.Chart
expectedAppVersion := "app-version-foo" 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)
c := newPackageCmd(&bytes.Buffer{}) _, output, err := executeActionCommand(cmd)
flags := map[string]string{ if err != nil {
"destination": dir, t.Logf("Output: %s", output)
"app-version": expectedAppVersion, t.Fatal(err)
}
setFlags(c, flags)
if err := c.RunE(c, []string{"testdata/testcharts/alpine"}); err != nil {
t.Errorf("unexpected error %q", err)
} }
chartPath := filepath.Join(dir, "alpine-0.1.0.tgz") chartPath := filepath.Join(dir, "alpine-0.1.0.tgz")
if fi, err := os.Stat(chartPath); err != nil { if fi, err := os.Stat(chartPath); err != nil {
t.Errorf("expected file %q, got err %q", chartPath, err) t.Errorf("expected file %q, got err %q", chartPath, err)
} else if fi.Size() == 0 { } else if fi.Size() == 0 {
t.Errorf("file %q has zero bytes.", chartPath) t.Errorf("file %q has zero bytes.", chartPath)
} }
ch, err := loader.Load(chartPath) ch, err = loader.Load(chartPath)
if err != nil { if err != nil {
t.Fatalf("unexpected error loading packaged chart: %v", err) t.Fatalf("unexpected error loading packaged chart: %v", err)
} }
@ -196,13 +191,6 @@ func TestSetAppVersion(t *testing.T) {
} }
} }
func setFlags(cmd *cobra.Command, flags map[string]string) {
dest := cmd.Flags()
for f, v := range flags {
dest.Set(f, v)
}
}
func TestPackageFileCompletion(t *testing.T) { func TestPackageFileCompletion(t *testing.T) {
checkFileCompletion(t, "package", true) checkFileCompletion(t, "package", true)
checkFileCompletion(t, "package mypath", true) // Multiple paths can be given checkFileCompletion(t, "package mypath", true) // Multiple paths can be given

@ -23,7 +23,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/plugin" "helm.sh/helm/v4/pkg/plugin"
) )
const pluginHelp = ` const pluginHelp = `
@ -47,19 +47,27 @@ func newPluginCmd(out io.Writer) *cobra.Command {
// runHook will execute a plugin hook. // runHook will execute a plugin hook.
func runHook(p *plugin.Plugin, event string) error { func runHook(p *plugin.Plugin, event string) error {
hook := p.Metadata.Hooks[event] plugin.SetupPluginEnv(settings, p.Metadata.Name, p.Dir)
if hook == "" {
cmds := p.Metadata.PlatformHooks[event]
expandArgs := true
if len(cmds) == 0 && len(p.Metadata.Hooks) > 0 {
cmd := p.Metadata.Hooks[event]
if len(cmd) > 0 {
cmds = []plugin.PlatformCommand{{Command: "sh", Args: []string{"-c", cmd}}}
expandArgs = false
}
}
main, argv, err := plugin.PrepareCommands(cmds, expandArgs, []string{})
if err != nil {
return nil return nil
} }
prog := exec.Command("sh", "-c", hook) prog := exec.Command(main, argv...)
// TODO make this work on windows
// I think its ... ¯\_(ツ)_/¯
// prog := exec.Command("cmd", "/C", p.Metadata.Hooks.Install())
debug("running %s hook: %s", event, prog) debug("running %s hook: %s", event, prog)
plugin.SetupPluginEnv(settings, p.Metadata.Name, p.Dir)
prog.Stdout, prog.Stderr = os.Stdout, os.Stderr prog.Stdout, prog.Stderr = os.Stdout, os.Stderr
if err := prog.Run(); err != nil { if err := prog.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok { if eerr, ok := err.(*exec.ExitError); ok {

@ -22,9 +22,9 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/plugin" "helm.sh/helm/v4/pkg/plugin"
"helm.sh/helm/v3/pkg/plugin/installer" "helm.sh/helm/v4/pkg/plugin/installer"
) )
type pluginInstallOptions struct { type pluginInstallOptions struct {
@ -39,23 +39,23 @@ This command allows you to install a plugin from a url to a VCS repo or a local
func newPluginInstallCmd(out io.Writer) *cobra.Command { func newPluginInstallCmd(out io.Writer) *cobra.Command {
o := &pluginInstallOptions{} o := &pluginInstallOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "install [options] <path|url>...", Use: "install [options] <path|url>",
Short: "install one or more Helm plugins", Short: "install a Helm plugin",
Long: pluginInstallDesc, Long: pluginInstallDesc,
Aliases: []string{"add"}, Aliases: []string{"add"},
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 { if len(args) == 0 {
// We do file completion, in case the plugin is local // We do file completion, in case the plugin is local
return nil, cobra.ShellCompDirectiveDefault return nil, cobra.ShellCompDirectiveDefault
} }
// No more completion once the plugin path has been specified // No more completion once the plugin path has been specified
return nil, cobra.ShellCompDirectiveNoFileComp return noMoreArgsComp()
}, },
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(_ *cobra.Command, args []string) error {
return o.complete(args) return o.complete(args)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
return o.run(out) return o.run(out)
}, },
} }

@ -18,12 +18,11 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/plugin" "helm.sh/helm/v4/pkg/plugin"
) )
func newPluginListCmd(out io.Writer) *cobra.Command { func newPluginListCmd(out io.Writer) *cobra.Command {
@ -31,8 +30,8 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
Use: "list", Use: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Short: "list installed Helm plugins", Short: "list installed Helm plugins",
ValidArgsFunction: noCompletions, ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
debug("pluginDirs: %s", settings.PluginsDirectory) debug("pluginDirs: %s", settings.PluginsDirectory)
plugins, err := plugin.FindPlugins(settings.PluginsDirectory) plugins, err := plugin.FindPlugins(settings.PluginsDirectory)
if err != nil { if err != nil {
@ -76,15 +75,13 @@ func filterPlugins(plugins []*plugin.Plugin, ignoredPluginNames []string) []*plu
} }
// Provide dynamic auto-completion for plugin names // Provide dynamic auto-completion for plugin names
func compListPlugins(toComplete string, ignoredPluginNames []string) []string { func compListPlugins(_ string, ignoredPluginNames []string) []string {
var pNames []string var pNames []string
plugins, err := plugin.FindPlugins(settings.PluginsDirectory) plugins, err := plugin.FindPlugins(settings.PluginsDirectory)
if err == nil && len(plugins) > 0 { if err == nil && len(plugins) > 0 {
filteredPlugins := filterPlugins(plugins, ignoredPluginNames) filteredPlugins := filterPlugins(plugins, ignoredPluginNames)
for _, p := range filteredPlugins { for _, p := range filteredPlugins {
if strings.HasPrefix(p.Metadata.Name, toComplete) { pNames = append(pNames, fmt.Sprintf("%s\t%s", p.Metadata.Name, p.Metadata.Usage))
pNames = append(pNames, fmt.Sprintf("%s\t%s", p.Metadata.Name, p.Metadata.Usage))
}
} }
} }
return pNames return pNames

@ -26,7 +26,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
) )
func TestManuallyProcessArgs(t *testing.T) { func TestManuallyProcessArgs(t *testing.T) {
@ -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 { type staticCompletionDetails struct {
use string use string
validArgs []string validArgs []string
@ -307,6 +382,11 @@ func TestPluginCmdsCompletion(t *testing.T) {
cmd: "__complete plugin update ''", cmd: "__complete plugin update ''",
golden: "output/plugin_list_comp.txt", golden: "output/plugin_list_comp.txt",
rels: []*release.Release{}, rels: []*release.Release{},
}, {
name: "completion for plugin update, no filter",
cmd: "__complete plugin update full",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, { }, {
name: "completion for plugin update repetition", name: "completion for plugin update repetition",
cmd: "__complete plugin update args ''", cmd: "__complete plugin update args ''",
@ -317,6 +397,11 @@ func TestPluginCmdsCompletion(t *testing.T) {
cmd: "__complete plugin uninstall ''", cmd: "__complete plugin uninstall ''",
golden: "output/plugin_list_comp.txt", golden: "output/plugin_list_comp.txt",
rels: []*release.Release{}, rels: []*release.Release{},
}, {
name: "completion for plugin uninstall, no filter",
cmd: "__complete plugin uninstall full",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, { }, {
name: "completion for plugin uninstall repetition", name: "completion for plugin uninstall repetition",
cmd: "__complete plugin uninstall args ''", cmd: "__complete plugin uninstall args ''",

@ -24,7 +24,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/plugin" "helm.sh/helm/v4/pkg/plugin"
) )
type pluginUninstallOptions struct { type pluginUninstallOptions struct {
@ -38,13 +38,13 @@ func newPluginUninstallCmd(out io.Writer) *cobra.Command {
Use: "uninstall <plugin>...", Use: "uninstall <plugin>...",
Aliases: []string{"rm", "remove"}, Aliases: []string{"rm", "remove"},
Short: "uninstall one or more Helm plugins", Short: "uninstall one or more Helm plugins",
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp
}, },
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(_ *cobra.Command, args []string) error {
return o.complete(args) return o.complete(args)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
return o.run(out) return o.run(out)
}, },
} }
@ -78,7 +78,7 @@ func (o *pluginUninstallOptions) run(out io.Writer) error {
} }
} }
if len(errorPlugins) > 0 { if len(errorPlugins) > 0 {
return errors.Errorf(strings.Join(errorPlugins, "\n")) return errors.New(strings.Join(errorPlugins, "\n"))
} }
return nil return nil
} }

@ -24,8 +24,8 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/plugin" "helm.sh/helm/v4/pkg/plugin"
"helm.sh/helm/v3/pkg/plugin/installer" "helm.sh/helm/v4/pkg/plugin/installer"
) )
type pluginUpdateOptions struct { type pluginUpdateOptions struct {
@ -39,13 +39,13 @@ func newPluginUpdateCmd(out io.Writer) *cobra.Command {
Use: "update <plugin>...", Use: "update <plugin>...",
Aliases: []string{"up"}, Aliases: []string{"up"},
Short: "update one or more Helm plugins", Short: "update one or more Helm plugins",
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp
}, },
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(_ *cobra.Command, args []string) error {
return o.complete(args) return o.complete(args)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
return o.run(out) return o.run(out)
}, },
} }
@ -81,7 +81,7 @@ func (o *pluginUpdateOptions) run(out io.Writer) error {
} }
} }
if len(errorPlugins) > 0 { if len(errorPlugins) > 0 {
return errors.Errorf(strings.Join(errorPlugins, "\n")) return errors.New(strings.Join(errorPlugins, "\n"))
} }
return nil return nil
} }

@ -0,0 +1,91 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"errors"
"fmt"
"os"
"runtime"
"runtime/pprof"
)
var (
cpuProfileFile *os.File
cpuProfilePath string
memProfilePath string
)
func init() {
cpuProfilePath = os.Getenv("HELM_PPROF_CPU_PROFILE")
memProfilePath = os.Getenv("HELM_PPROF_MEM_PROFILE")
}
// startProfiling starts profiling CPU usage if HELM_PPROF_CPU_PROFILE is set
// to a file path. It returns an error if the file could not be created or
// CPU profiling could not be started.
func startProfiling() error {
if cpuProfilePath != "" {
var err error
cpuProfileFile, err = os.Create(cpuProfilePath)
if err != nil {
return fmt.Errorf("could not create CPU profile: %w", err)
}
if err := pprof.StartCPUProfile(cpuProfileFile); err != nil {
cpuProfileFile.Close()
cpuProfileFile = nil
return fmt.Errorf("could not start CPU profile: %w", err)
}
}
return nil
}
// stopProfiling stops profiling CPU and memory usage.
// It writes memory profile to the file path specified in HELM_PPROF_MEM_PROFILE
// environment variable.
func stopProfiling() error {
errs := []error{}
// Stop CPU profiling if it was started
if cpuProfileFile != nil {
pprof.StopCPUProfile()
err := cpuProfileFile.Close()
if err != nil {
errs = append(errs, err)
}
cpuProfileFile = nil
}
if memProfilePath != "" {
f, err := os.Create(memProfilePath)
if err != nil {
errs = append(errs, err)
}
defer f.Close()
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
errs = append(errs, err)
}
}
if err := errors.Join(errs...); err != nil {
return fmt.Errorf("error(s) while stopping profiling: %w", err)
}
return nil
}

@ -23,8 +23,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
) )
const pullDesc = ` const pullDesc = `
@ -43,7 +43,7 @@ result in an error, and the chart will not be saved locally.
` `
func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewPullWithOpts(action.WithConfig(cfg)) client := action.NewPull(action.WithConfig(cfg))
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "pull [chart URL | repo/chartname] [...]", Use: "pull [chart URL | repo/chartname] [...]",
@ -51,22 +51,25 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Aliases: []string{"fetch"}, Aliases: []string{"fetch"},
Long: pullDesc, Long: pullDesc,
Args: require.MinimumNArgs(1), Args: require.MinimumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 { if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
} }
return compListCharts(toComplete, false) return compListCharts(toComplete, false)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
client.Settings = settings client.Settings = settings
if client.Version == "" && client.Devel { if client.Version == "" && client.Devel {
debug("setting version to >0.0.0-0") debug("setting version to >0.0.0-0")
client.Version = ">0.0.0-0" client.Version = ">0.0.0-0"
} }
if err := checkOCI(args[0]); err != nil { registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
return err client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
} }
client.SetRegistryClient(registryClient)
for i := 0; i < len(args); i++ { for i := 0; i < len(args); i++ {
output, err := client.Run(args[i]) output, err := client.Run(args[i])
@ -84,10 +87,10 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.Untar, "untar", false, "if set to true, will untar the chart after downloading it") f.BoolVar(&client.Untar, "untar", false, "if set to true, will untar the chart after downloading it")
f.BoolVar(&client.VerifyLater, "prov", false, "fetch the provenance file, but don't perform verification") f.BoolVar(&client.VerifyLater, "prov", false, "fetch the provenance file, but don't perform verification")
f.StringVar(&client.UntarDir, "untardir", ".", "if untar is specified, this flag specifies the name of the directory into which the chart is expanded") f.StringVar(&client.UntarDir, "untardir", ".", "if untar is specified, this flag specifies the name of the directory into which the chart is expanded")
f.StringVarP(&client.DestDir, "destination", "d", ".", "location to write the chart. If this and tardir are specified, tardir is appended to this") f.StringVarP(&client.DestDir, "destination", "d", ".", "location to write the chart. If this and untardir are specified, untardir is appended to this")
addChartPathOptionsFlags(f, &client.ChartPathOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions)
err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("version", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 1 { if len(args) != 1 {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
} }

@ -24,17 +24,16 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"helm.sh/helm/v3/pkg/repo/repotest" "helm.sh/helm/v4/pkg/repo/repotest"
) )
func TestPullCmd(t *testing.T) { func TestPullCmd(t *testing.T) {
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*") srv := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"),
} )
defer srv.Stop() defer srv.Stop()
os.Setenv("HELM_EXPERIMENTAL_OCI", "1")
ociSrv, err := repotest.NewOCIServer(t, srv.Root()) ociSrv, err := repotest.NewOCIServer(t, srv.Root())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -184,22 +183,27 @@ func TestPullCmd(t *testing.T) {
wantError: true, wantError: true,
}, },
{ {
name: "Fail fetching OCI chart without version specified", name: "Fetching OCI chart without version option specified",
args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0", ociSrv.RegistryURL), args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0", ociSrv.RegistryURL),
wantErrorMsg: "Error: --version flag is explicitly required for OCI registries", expectFile: "./oci-dependent-chart-0.1.0.tgz",
wantError: true,
}, },
{ {
name: "Fail fetching OCI chart without version specified", name: "Fetching OCI chart with version specified",
args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0 --version 0.1.0", ociSrv.RegistryURL), args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0 --version 0.1.0", ociSrv.RegistryURL),
wantError: true, expectFile: "./oci-dependent-chart-0.1.0.tgz",
},
{
name: "Fail fetching OCI chart with version mismatch",
args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.2.0 --version 0.1.0", ociSrv.RegistryURL),
wantErrorMsg: "Error: chart reference and version mismatch: 0.2.0 is not 0.1.0",
wantError: true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
outdir := srv.Root() outdir := srv.Root()
cmd := fmt.Sprintf("fetch %s -d '%s' --repository-config %s --repository-cache %s --registry-config %s", cmd := fmt.Sprintf("fetch %s -d '%s' --repository-config %s --repository-cache %s --registry-config %s --plain-http",
tt.args, tt.args,
outdir, outdir,
filepath.Join(outdir, "repositories.yaml"), filepath.Join(outdir, "repositories.yaml"),
@ -253,19 +257,13 @@ func TestPullCmd(t *testing.T) {
} }
func TestPullWithCredentialsCmd(t *testing.T) { func TestPullWithCredentialsCmd(t *testing.T) {
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*") srv := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"),
} repotest.WithMiddleware(repotest.BasicAuthMiddleware(t)),
)
defer srv.Stop() defer srv.Stop()
srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "username" || password != "password" {
t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password)
}
}))
srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.FileServer(http.Dir(srv.Root())).ServeHTTP(w, r) http.FileServer(http.Dir(srv.Root())).ServeHTTP(w, r)
})) }))
@ -371,6 +369,10 @@ func TestPullVersionCompletion(t *testing.T) {
name: "completion for pull version flag", name: "completion for pull version flag",
cmd: fmt.Sprintf("%s __complete pull testing/alpine --version ''", repoSetup), cmd: fmt.Sprintf("%s __complete pull testing/alpine --version ''", repoSetup),
golden: "output/version-comp.txt", golden: "output/version-comp.txt",
}, {
name: "completion for pull version flag, no filter",
cmd: fmt.Sprintf("%s __complete pull testing/alpine --version 0.3", repoSetup),
golden: "output/version-comp.txt",
}, { }, {
name: "completion for pull version flag too few args", name: "completion for pull version flag too few args",
cmd: fmt.Sprintf("%s __complete pull --version ''", repoSetup), cmd: fmt.Sprintf("%s __complete pull --version ''", repoSetup),

@ -22,9 +22,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
experimental "helm.sh/helm/v3/internal/experimental/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/pusher"
) )
const pushDesc = ` const pushDesc = `
@ -34,19 +34,57 @@ If the chart has an associated provenance file,
it will also be uploaded. it will also be uploaded.
` `
type registryPushOptions struct {
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
plainHTTP bool
password string
username string
}
func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := experimental.NewPushWithOpts(experimental.WithPushConfig(cfg)) o := &registryPushOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "push [chart] [remote]", Use: "push [chart] [remote]",
Short: "push a chart to remote", Short: "push a chart to remote",
Long: pushDesc, Long: pushDesc,
Hidden: !FeatureGateOCI.IsEnabled(), Args: require.MinimumNArgs(2),
PersistentPreRunE: checkOCIFeatureGate(), ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
Args: require.MinimumNArgs(2), if len(args) == 0 {
RunE: func(cmd *cobra.Command, args []string) error { // Do file completion for the chart file to push
return nil, cobra.ShellCompDirectiveDefault
}
if len(args) == 1 {
providers := []pusher.Provider(pusher.All(settings))
var comps []string
for _, p := range providers {
for _, scheme := range p.Schemes {
comps = append(comps, fmt.Sprintf("%s://", scheme))
}
}
return comps, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
}
return noMoreArgsComp()
},
RunE: func(_ *cobra.Command, args []string) error {
registryClient, err := newRegistryClient(
o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSverify, o.plainHTTP, o.username, o.password,
)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
cfg.RegistryClient = registryClient
chartRef := args[0] chartRef := args[0]
remote := args[1] 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 client.Settings = settings
output, err := client.Run(chartRef, remote) output, err := client.Run(chartRef, remote)
if err != nil { if err != nil {
@ -57,5 +95,14 @@ 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")
f.StringVar(&o.username, "username", "", "chart repository username where to locate the requested chart")
f.StringVar(&o.password, "password", "", "chart repository password where to locate the requested chart")
return cmd return cmd
} }

@ -14,17 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package kube // import "helm.sh/helm/v3/pkg/kube" package main
import "k8s.io/cli-runtime/pkg/genericclioptions" import (
"testing"
)
// GetConfig returns a Kubernetes client config. func TestPushFileCompletion(t *testing.T) {
// checkFileCompletion(t, "push", true)
// Deprecated checkFileCompletion(t, "push package.tgz", false)
func GetConfig(kubeconfig, context, namespace string) *genericclioptions.ConfigFlags { checkFileCompletion(t, "push package.tgz oci://localhost:5000", false)
cf := genericclioptions.NewConfigFlags(true)
cf.Namespace = &namespace
cf.Context = &context
cf.KubeConfig = &kubeconfig
return cf
} }

@ -20,7 +20,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
) )
const registryHelp = ` const registryHelp = `
@ -29,11 +29,9 @@ This command consists of multiple subcommands to interact with registries.
func newRegistryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func newRegistryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "registry", Use: "registry",
Short: "login to or logout from a registry", Short: "login to or logout from a registry",
Long: registryHelp, Long: registryHelp,
Hidden: !FeatureGateOCI.IsEnabled(),
PersistentPreRunE: checkOCIFeatureGate(),
} }
cmd.AddCommand( cmd.AddCommand(
newRegistryLoginCmd(cfg, out), newRegistryLoginCmd(cfg, out),

@ -21,49 +21,66 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"strings" "strings"
"github.com/docker/docker/pkg/term" //nolint "github.com/moby/term"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
experimental "helm.sh/helm/v3/internal/experimental/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/action"
) )
const registryLoginDesc = ` const registryLoginDesc = `
Authenticate to a remote registry. Authenticate to a remote registry.
` `
type registryLoginOptions struct {
username string
password string
passwordFromStdinOpt bool
certFile string
keyFile string
caFile string
insecure bool
plainHTTP bool
}
func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
var usernameOpt, passwordOpt string o := &registryLoginOptions{}
var passwordFromStdinOpt, insecureOpt bool
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "login [host]", Use: "login [host]",
Short: "login to a registry", Short: "login to a registry",
Long: registryLoginDesc, Long: registryLoginDesc,
Args: require.MinimumNArgs(1), Args: require.MinimumNArgs(1),
Hidden: !FeatureGateOCI.IsEnabled(), ValidArgsFunction: cobra.NoFileCompletions,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
hostname := args[0] hostname := args[0]
username, password, err := getUsernamePassword(usernameOpt, passwordOpt, passwordFromStdinOpt) username, password, err := getUsernamePassword(o.username, o.password, o.passwordFromStdinOpt)
if err != nil { if err != nil {
return err return err
} }
return experimental.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),
action.WithPlainHTTPLogin(o.plainHTTP))
}, },
} }
f := cmd.Flags() f := cmd.Flags()
f.StringVarP(&usernameOpt, "username", "u", "", "registry username") f.StringVarP(&o.username, "username", "u", "", "registry username")
f.StringVarP(&passwordOpt, "password", "p", "", "registry password or identity token") f.StringVarP(&o.password, "password", "p", "", "registry password or identity token")
f.BoolVarP(&passwordFromStdinOpt, "password-stdin", "", false, "read password or identity token from stdin") f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read password or identity token from stdin")
f.BoolVarP(&insecureOpt, "insecure", "", false, "allow connections to TLS registry without certs") 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")
f.BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the chart upload")
return cmd return cmd
} }
@ -75,7 +92,7 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd
password := passwordOpt password := passwordOpt
if passwordFromStdinOpt { if passwordFromStdinOpt {
passwordFromStdin, err := ioutil.ReadAll(os.Stdin) passwordFromStdin, err := io.ReadAll(os.Stdin)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }

@ -16,7 +16,10 @@ limitations under the License.
package main package main
func checkPerms() { import (
// Not yet implemented on Windows. If you know how to do a comprehensive perms "testing"
// check on Windows, contributions welcomed! )
func TestRegistryLoginFileCompletion(t *testing.T) {
checkFileCompletion(t, "registry login", false)
} }

@ -21,9 +21,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
experimental "helm.sh/helm/v3/internal/experimental/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/action"
) )
const registryLogoutDesc = ` const registryLogoutDesc = `
@ -32,14 +31,14 @@ Remove credentials stored for a remote registry.
func newRegistryLogoutCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func newRegistryLogoutCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "logout [host]", Use: "logout [host]",
Short: "logout from a registry", Short: "logout from a registry",
Long: registryLogoutDesc, Long: registryLogoutDesc,
Args: require.MinimumNArgs(1), Args: require.MinimumNArgs(1),
Hidden: !FeatureGateOCI.IsEnabled(), ValidArgsFunction: cobra.NoFileCompletions,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
hostname := args[0] hostname := args[0]
return experimental.NewRegistryLogout(cfg).Run(out, hostname) return action.NewRegistryLogout(cfg).Run(out, hostname)
}, },
} }
} }

@ -0,0 +1,25 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"testing"
)
func TestRegistryLogoutFileCompletion(t *testing.T) {
checkFileCompletion(t, "registry logout", false)
}

@ -25,9 +25,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
) )
const releaseTestHelp = ` const releaseTestHelp = `
@ -39,7 +39,7 @@ The tests to be run are defined in the chart that was installed.
func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewReleaseTesting(cfg) client := action.NewReleaseTesting(cfg)
var outfmt = output.Table outfmt := output.Table
var outputLogs bool var outputLogs bool
var filter []string var filter []string
@ -48,20 +48,20 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
Short: "run tests for a release", Short: "run tests for a release",
Long: releaseTestHelp, Long: releaseTestHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 { if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp return noMoreArgsComp()
} }
return compListReleases(toComplete, args, cfg) return compListReleases(toComplete, args, cfg)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
client.Namespace = settings.Namespace() client.Namespace = settings.Namespace()
notName := regexp.MustCompile(`^!\s?name=`) notName := regexp.MustCompile(`^!\s?name=`)
for _, f := range filter { for _, f := range filter {
if strings.HasPrefix(f, "name=") { 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) { } 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]) rel, runErr := client.Run(args[0])
@ -72,7 +72,12 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
return runErr return runErr
} }
if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}); err != nil { if err := outfmt.Write(out, &statusPrinter{
release: rel,
debug: settings.Debug,
showMetadata: false,
hideNotes: client.HideNotes,
}); err != nil {
return err return err
} }
@ -92,6 +97,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&outputLogs, "logs", false, "dump the logs from test pods (this runs after all tests are complete, but before any cleanup)") f.BoolVar(&outputLogs, "logs", false, "dump the logs from test pods (this runs after all tests are complete, but before any cleanup)")
f.StringSliceVar(&filter, "filter", []string{}, "specify tests by attribute (currently \"name\") using attribute=value syntax or '!attribute=value' to exclude a test (can specify multiple or separate values with commas: name=test1,name=test2)") f.StringSliceVar(&filter, "filter", []string{}, "specify tests by attribute (currently \"name\") using attribute=value syntax or '!attribute=value' to exclude a test (can specify multiple or separate values with commas: name=test1,name=test2)")
f.BoolVar(&client.HideNotes, "hide-notes", false, "if set, do not show notes in test output. Does not affect presence in chart metadata")
return cmd return cmd
} }

@ -23,7 +23,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
) )
var repoHelm = ` var repoHelm = `

@ -20,7 +20,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -32,9 +31,9 @@ import (
"golang.org/x/term" "golang.org/x/term"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo"
) )
// Repositories that have been permanently deleted and no longer work // Repositories that have been permanently deleted and no longer work
@ -60,20 +59,22 @@ type repoAddOptions struct {
repoFile string repoFile string
repoCache string repoCache string
// Deprecated, but cannot be removed until Helm 4
deprecatedNoUpdate bool
} }
func newRepoAddCmd(out io.Writer) *cobra.Command { func newRepoAddCmd(out io.Writer) *cobra.Command {
o := &repoAddOptions{} o := &repoAddOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "add [NAME] [URL]", Use: "add [NAME] [URL]",
Short: "add a chart repository", Short: "add a chart repository",
Args: require.ExactArgs(2), Args: require.ExactArgs(2),
ValidArgsFunction: noCompletions, ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 1 {
return noMoreArgsComp()
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
RunE: func(_ *cobra.Command, args []string) error {
o.name = args[0] o.name = args[0]
o.url = args[1] o.url = args[1]
o.repoFile = settings.RepositoryConfig o.repoFile = settings.RepositoryConfig
@ -88,7 +89,6 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
f.StringVar(&o.password, "password", "", "chart repository password") f.StringVar(&o.password, "password", "", "chart repository password")
f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read chart repository password from stdin") f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read chart repository password from stdin")
f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists") f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists")
f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.")
f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
@ -119,7 +119,7 @@ func (o *repoAddOptions) run(out io.Writer) error {
repoFileExt := filepath.Ext(o.repoFile) repoFileExt := filepath.Ext(o.repoFile)
var lockPath string var lockPath string
if len(repoFileExt) > 0 && len(repoFileExt) < len(o.repoFile) { if len(repoFileExt) > 0 && len(repoFileExt) < len(o.repoFile) {
lockPath = strings.Replace(o.repoFile, repoFileExt, ".lock", 1) lockPath = strings.TrimSuffix(o.repoFile, repoFileExt) + ".lock"
} else { } else {
lockPath = o.repoFile + ".lock" lockPath = o.repoFile + ".lock"
} }
@ -134,7 +134,7 @@ func (o *repoAddOptions) run(out io.Writer) error {
return err return err
} }
b, err := ioutil.ReadFile(o.repoFile) b, err := os.ReadFile(o.repoFile)
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return err return err
} }
@ -177,6 +177,11 @@ func (o *repoAddOptions) run(out io.Writer) error {
InsecureSkipTLSverify: o.insecureSkipTLSverify, InsecureSkipTLSverify: o.insecureSkipTLSverify,
} }
// Check if the repo name is legal
if strings.Contains(o.name, "/") {
return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", o.name)
}
// If the repo exists do one of two things: // If the repo exists do one of two things:
// 1. If the configuration for the name is the same continue without error // 1. If the configuration for the name is the same continue without error
// 2. When the config is different require --force-update // 2. When the config is different require --force-update
@ -208,7 +213,7 @@ func (o *repoAddOptions) run(out io.Writer) error {
f.Update(&c) f.Update(&c)
if err := f.WriteFile(o.repoFile, 0644); err != nil { if err := f.WriteFile(o.repoFile, 0600); err != nil {
return err return err
} }
fmt.Fprintf(out, "%q has been added to your repositories\n", o.name) fmt.Fprintf(out, "%q has been added to your repositories\n", o.name)

@ -18,7 +18,7 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -27,28 +27,30 @@ import (
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"helm.sh/helm/v3/internal/test/ensure" "helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath/xdg"
"helm.sh/helm/v3/pkg/helmpath/xdg" "helm.sh/helm/v4/pkg/repo"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo/repotest"
"helm.sh/helm/v3/pkg/repo/repotest"
) )
func TestRepoAddCmd(t *testing.T) { func TestRepoAddCmd(t *testing.T) {
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") srv := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testserver/*.*"),
} )
defer srv.Stop() defer srv.Stop()
// A second test server is setup to verify URL changing // A second test server is setup to verify URL changing
srv2, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") srv2 := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testserver/*.*"),
} )
defer srv2.Stop() defer srv2.Stop()
tmpdir := ensure.TempDir(t) tmpdir := filepath.Join(t.TempDir(), "path-component.yaml/data")
if err := os.MkdirAll(tmpdir, 0777); err != nil {
t.Fatal(err)
}
repoFile := filepath.Join(tmpdir, "repositories.yaml") repoFile := filepath.Join(tmpdir, "repositories.yaml")
tests := []cmdTestCase{ tests := []cmdTestCase{
@ -78,27 +80,26 @@ func TestRepoAddCmd(t *testing.T) {
} }
func TestRepoAdd(t *testing.T) { func TestRepoAdd(t *testing.T) {
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") ts := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testserver/*.*"),
} )
defer ts.Stop() defer ts.Stop()
rootDir := ensure.TempDir(t) rootDir := t.TempDir()
repoFile := filepath.Join(rootDir, "repositories.yaml") repoFile := filepath.Join(rootDir, "repositories.yaml")
const testRepoName = "test-name" const testRepoName = "test-name"
o := &repoAddOptions{ o := &repoAddOptions{
name: testRepoName, name: testRepoName,
url: ts.URL(), url: ts.URL(),
forceUpdate: false, forceUpdate: false,
deprecatedNoUpdate: true, repoFile: repoFile,
repoFile: repoFile,
} }
os.Setenv(xdg.CacheHomeEnvVar, rootDir) os.Setenv(xdg.CacheHomeEnvVar, rootDir)
if err := o.run(ioutil.Discard); err != nil { if err := o.run(io.Discard); err != nil {
t.Error(err) t.Error(err)
} }
@ -122,44 +123,76 @@ func TestRepoAdd(t *testing.T) {
o.forceUpdate = true 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) 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") t.Errorf("Duplicate repository name was added")
} }
} }
func TestRepoAddCheckLegalName(t *testing.T) {
ts := repotest.NewTempServer(
t,
repotest.WithChartSourceGlob("testdata/testserver/*.*"),
)
defer ts.Stop()
defer resetEnv()()
const testRepoName = "test-hub/test-name"
rootDir := t.TempDir()
repoFile := filepath.Join(t.TempDir(), "repositories.yaml")
o := &repoAddOptions{
name: testRepoName,
url: ts.URL(),
forceUpdate: false,
repoFile: repoFile,
}
os.Setenv(xdg.CacheHomeEnvVar, rootDir)
wantErrorMsg := fmt.Sprintf("repository name (%s) contains '/', please specify a different name without '/'", testRepoName)
if err := o.run(io.Discard); err != nil {
if wantErrorMsg != err.Error() {
t.Fatalf("Actual error %s, not equal to expected error %s", err, wantErrorMsg)
}
} else {
t.Fatalf("expect reported an error.")
}
}
func TestRepoAddConcurrentGoRoutines(t *testing.T) { func TestRepoAddConcurrentGoRoutines(t *testing.T) {
const testName = "test-name" const testName = "test-name"
repoFile := filepath.Join(ensure.TempDir(t), "repositories.yaml") repoFile := filepath.Join(t.TempDir(), "repositories.yaml")
repoAddConcurrent(t, testName, repoFile) repoAddConcurrent(t, testName, repoFile)
} }
func TestRepoAddConcurrentDirNotExist(t *testing.T) { func TestRepoAddConcurrentDirNotExist(t *testing.T) {
const testName = "test-name-2" 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) repoAddConcurrent(t, testName, repoFile)
} }
func TestRepoAddConcurrentNoFileExtension(t *testing.T) { func TestRepoAddConcurrentNoFileExtension(t *testing.T) {
const testName = "test-name-3" const testName = "test-name-3"
repoFile := filepath.Join(ensure.TempDir(t), "repositories") repoFile := filepath.Join(t.TempDir(), "repositories")
repoAddConcurrent(t, testName, repoFile) repoAddConcurrent(t, testName, repoFile)
} }
func TestRepoAddConcurrentHiddenFile(t *testing.T) { func TestRepoAddConcurrentHiddenFile(t *testing.T) {
const testName = "test-name-4" const testName = "test-name-4"
repoFile := filepath.Join(ensure.TempDir(t), ".repositories") repoFile := filepath.Join(t.TempDir(), ".repositories")
repoAddConcurrent(t, testName, repoFile) repoAddConcurrent(t, testName, repoFile)
} }
func repoAddConcurrent(t *testing.T, testName, repoFile string) { func repoAddConcurrent(t *testing.T, testName, repoFile string) {
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") ts := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testserver/*.*"),
} )
defer ts.Stop() defer ts.Stop()
var wg sync.WaitGroup var wg sync.WaitGroup
@ -168,20 +201,19 @@ func repoAddConcurrent(t *testing.T, testName, repoFile string) {
go func(name string) { go func(name string) {
defer wg.Done() defer wg.Done()
o := &repoAddOptions{ o := &repoAddOptions{
name: name, name: name,
url: ts.URL(), url: ts.URL(),
deprecatedNoUpdate: true, forceUpdate: false,
forceUpdate: false, repoFile: repoFile,
repoFile: repoFile,
} }
if err := o.run(ioutil.Discard); err != nil { if err := o.run(io.Discard); err != nil {
t.Error(err) t.Error(err)
} }
}(fmt.Sprintf("%s-%d", testName, i)) }(fmt.Sprintf("%s-%d", testName, i))
} }
wg.Wait() wg.Wait()
b, err := ioutil.ReadFile(repoFile) b, err := os.ReadFile(repoFile)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -207,7 +239,11 @@ func TestRepoAddFileCompletion(t *testing.T) {
} }
func TestRepoAddWithPasswordFromStdin(t *testing.T) { func TestRepoAddWithPasswordFromStdin(t *testing.T) {
srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/testserver/*.*") srv := repotest.NewTempServer(
t,
repotest.WithChartSourceGlob("testdata/testserver/*.*"),
repotest.WithMiddleware(repotest.BasicAuthMiddleware(t)),
)
defer srv.Stop() defer srv.Stop()
defer resetEnv()() defer resetEnv()()
@ -217,7 +253,7 @@ func TestRepoAddWithPasswordFromStdin(t *testing.T) {
t.Errorf("unexpected error, got '%v'", err) t.Errorf("unexpected error, got '%v'", err)
} }
tmpdir := ensure.TempDir(t) tmpdir := t.TempDir()
repoFile := filepath.Join(tmpdir, "repositories.yaml") repoFile := filepath.Join(tmpdir, "repositories.yaml")
store := storageFixture() store := storageFixture()

@ -24,25 +24,28 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo"
) )
const repoIndexDesc = ` const repoIndexDesc = `
Read the current directory and generate an index file based on the charts found. Read the current directory, generate an index file based on the charts found
and write the result to 'index.yaml' in the current directory.
This tool is used for creating an 'index.yaml' file for a chart repository. To This tool is used for creating an 'index.yaml' file for a chart repository. To
set an absolute URL to the charts, use '--url' flag. set an absolute URL to the charts, use '--url' flag.
To merge the generated index with an existing index file, use the '--merge' To merge the generated index with an existing index file, use the '--merge'
flag. In this case, the charts found in the current directory will be merged flag. In this case, the charts found in the current directory will be merged
into the existing index, with local charts taking priority over existing charts. into the index passed in with --merge, with local charts taking priority over
existing charts.
` `
type repoIndexOptions struct { type repoIndexOptions struct {
dir string dir string
url string url string
merge string merge string
json bool
} }
func newRepoIndexCmd(out io.Writer) *cobra.Command { func newRepoIndexCmd(out io.Writer) *cobra.Command {
@ -53,15 +56,15 @@ func newRepoIndexCmd(out io.Writer) *cobra.Command {
Short: "generate an index file given a directory containing packaged charts", Short: "generate an index file given a directory containing packaged charts",
Long: repoIndexDesc, Long: repoIndexDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 { if len(args) == 0 {
// Allow file completion when completing the argument for the directory // Allow file completion when completing the argument for the directory
return nil, cobra.ShellCompDirectiveDefault return nil, cobra.ShellCompDirectiveDefault
} }
// No more completions, so disable file completion // No more completions, so disable file completion
return nil, cobra.ShellCompDirectiveNoFileComp return noMoreArgsComp()
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
o.dir = args[0] o.dir = args[0]
return o.run(out) return o.run(out)
}, },
@ -70,20 +73,21 @@ func newRepoIndexCmd(out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.StringVar(&o.url, "url", "", "url of chart repository") f.StringVar(&o.url, "url", "", "url of chart repository")
f.StringVar(&o.merge, "merge", "", "merge the generated index into the given index") 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 return cmd
} }
func (i *repoIndexOptions) run(out io.Writer) error { func (i *repoIndexOptions) run(_ io.Writer) error {
path, err := filepath.Abs(i.dir) path, err := filepath.Abs(i.dir)
if err != nil { if err != nil {
return err 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") out := filepath.Join(dir, "index.yaml")
i, err := repo.IndexDirectory(dir, url) i, err := repo.IndexDirectory(dir, url)
@ -95,7 +99,7 @@ func index(dir, url, mergeTo string) error {
var i2 *repo.IndexFile var i2 *repo.IndexFile
if _, err := os.Stat(mergeTo); os.IsNotExist(err) { if _, err := os.Stat(mergeTo); os.IsNotExist(err) {
i2 = repo.NewIndexFile() i2 = repo.NewIndexFile()
i2.WriteFile(mergeTo, 0644) writeIndexFile(i2, mergeTo, json)
} else { } else {
i2, err = repo.LoadIndexFile(mergeTo) i2, err = repo.LoadIndexFile(mergeTo)
if err != nil { if err != nil {
@ -105,5 +109,12 @@ func index(dir, url, mergeTo string) error {
i.Merge(i2) i.Merge(i2)
} }
i.SortEntries() 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) return i.WriteFile(out, 0644)
} }

@ -18,18 +18,18 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"helm.sh/helm/v3/internal/test/ensure" "helm.sh/helm/v4/pkg/repo"
"helm.sh/helm/v3/pkg/repo"
) )
func TestRepoIndexCmd(t *testing.T) { func TestRepoIndexCmd(t *testing.T) {
dir := ensure.TempDir(t) dir := t.TempDir()
comp := filepath.Join(dir, "compressedchart-0.1.0.tgz") comp := filepath.Join(dir, "compressedchart-0.1.0.tgz")
if err := linkOrCopy("testdata/testcharts/compressedchart-0.1.0.tgz", comp); err != nil { 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) 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` // Test with `--merge`
// Remove first two charts. // Remove first two charts.
@ -140,9 +162,9 @@ func TestRepoIndexCmd(t *testing.T) {
} }
} }
func linkOrCopy(old, new string) error { func linkOrCopy(source, target string) error {
if err := os.Link(old, new); err != nil { if err := os.Link(source, target); err != nil {
return copyFile(old, new) return copyFile(source, target)
} }
return nil return nil

@ -19,15 +19,14 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo"
) )
func newRepoListCmd(out io.Writer) *cobra.Command { func newRepoListCmd(out io.Writer) *cobra.Command {
@ -37,10 +36,10 @@ func newRepoListCmd(out io.Writer) *cobra.Command {
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Short: "list chart repositories", Short: "list chart repositories",
Args: require.NoArgs, Args: require.NoArgs,
ValidArgsFunction: noCompletions, ValidArgsFunction: noMoreArgsCompFunc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
f, err := repo.LoadFile(settings.RepositoryConfig) f, _ := repo.LoadFile(settings.RepositoryConfig)
if isNotExist(err) || (len(f.Repositories) == 0 && !(outfmt == output.JSON || outfmt == output.YAML)) { if len(f.Repositories) == 0 && !(outfmt == output.JSON || outfmt == output.YAML) {
return errors.New("no repositories to show") return errors.New("no repositories to show")
} }
@ -124,16 +123,14 @@ func filterRepos(repos []*repo.Entry, ignoredRepoNames []string) []*repo.Entry {
} }
// Provide dynamic auto-completion for repo names // Provide dynamic auto-completion for repo names
func compListRepos(prefix string, ignoredRepoNames []string) []string { func compListRepos(_ string, ignoredRepoNames []string) []string {
var rNames []string var rNames []string
f, err := repo.LoadFile(settings.RepositoryConfig) f, err := repo.LoadFile(settings.RepositoryConfig)
if err == nil && len(f.Repositories) > 0 { if err == nil && len(f.Repositories) > 0 {
filteredRepos := filterRepos(f.Repositories, ignoredRepoNames) filteredRepos := filterRepos(f.Repositories, ignoredRepoNames)
for _, repo := range filteredRepos { for _, repo := range filteredRepos {
if strings.HasPrefix(repo.Name, prefix) { rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL))
rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL))
}
} }
} }
return rNames return rNames

@ -25,9 +25,9 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo"
) )
type repoRemoveOptions struct { type repoRemoveOptions struct {
@ -44,10 +44,10 @@ func newRepoRemoveCmd(out io.Writer) *cobra.Command {
Aliases: []string{"rm"}, Aliases: []string{"rm"},
Short: "remove one or more chart repositories", Short: "remove one or more chart repositories",
Args: require.MinimumNArgs(1), Args: require.MinimumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compListRepos(toComplete, args), cobra.ShellCompDirectiveNoFileComp return compListRepos(toComplete, args), cobra.ShellCompDirectiveNoFileComp
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
o.repoFile = settings.RepositoryConfig o.repoFile = settings.RepositoryConfig
o.repoCache = settings.RepositoryCache o.repoCache = settings.RepositoryCache
o.names = args o.names = args
@ -67,7 +67,7 @@ func (o *repoRemoveOptions) run(out io.Writer) error {
if !r.Remove(name) { if !r.Remove(name) {
return errors.Errorf("no repo named %q found", 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 return err
} }

@ -24,20 +24,19 @@ import (
"strings" "strings"
"testing" "testing"
"helm.sh/helm/v3/internal/test/ensure" "helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v4/pkg/repo"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo/repotest"
"helm.sh/helm/v3/pkg/repo/repotest"
) )
func TestRepoRemove(t *testing.T) { func TestRepoRemove(t *testing.T) {
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") ts := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testserver/*.*"),
} )
defer ts.Stop() defer ts.Stop()
rootDir := ensure.TempDir(t) rootDir := t.TempDir()
repoFile := filepath.Join(rootDir, "repositories.yaml") repoFile := filepath.Join(rootDir, "repositories.yaml")
const testRepoName = "test-name" const testRepoName = "test-name"
@ -163,13 +162,14 @@ func testCacheFiles(t *testing.T, cacheIndexFile string, cacheChartsFile string,
} }
func TestRepoRemoveCompletion(t *testing.T) { func TestRepoRemoveCompletion(t *testing.T) {
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") ts := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testserver/*.*"),
} )
defer ts.Stop() defer ts.Stop()
rootDir := ensure.TempDir(t) rootDir := t.TempDir()
repoFile := filepath.Join(rootDir, "repositories.yaml") repoFile := filepath.Join(rootDir, "repositories.yaml")
repoCache := filepath.Join(rootDir, "cache/") repoCache := filepath.Join(rootDir, "cache/")
@ -197,6 +197,10 @@ func TestRepoRemoveCompletion(t *testing.T) {
name: "completion for repo remove", name: "completion for repo remove",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove ''", repoSetup), cmd: fmt.Sprintf("%s __completeNoDesc repo remove ''", repoSetup),
golden: "output/repo_list_comp.txt", golden: "output/repo_list_comp.txt",
}, {
name: "completion for repo remove, no filter",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove fo", repoSetup),
golden: "output/repo_list_comp.txt",
}, { }, {
name: "completion for repo remove repetition", name: "completion for repo remove repetition",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove foo ''", repoSetup), cmd: fmt.Sprintf("%s __completeNoDesc repo remove foo ''", repoSetup),

@ -19,14 +19,15 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"slices"
"sync" "sync"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo"
) )
const updateDesc = ` const updateDesc = `
@ -57,10 +58,10 @@ func newRepoUpdateCmd(out io.Writer) *cobra.Command {
Short: "update information of available charts locally from chart repositories", Short: "update information of available charts locally from chart repositories",
Long: updateDesc, Long: updateDesc,
Args: require.MinimumNArgs(0), Args: require.MinimumNArgs(0),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compListRepos(toComplete, args), cobra.ShellCompDirectiveNoFileComp return compListRepos(toComplete, args), cobra.ShellCompDirectiveNoFileComp
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
o.repoFile = settings.RepositoryConfig o.repoFile = settings.RepositoryConfig
o.repoCache = settings.RepositoryCache o.repoCache = settings.RepositoryCache
o.names = args o.names = args
@ -133,8 +134,8 @@ func updateCharts(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdate
wg.Wait() wg.Wait()
if len(repoFailList) > 0 && failOnRepoUpdateFail { if len(repoFailList) > 0 && failOnRepoUpdateFail {
return errors.New(fmt.Sprintf("Failed to update the following repositories: %s", return fmt.Errorf("Failed to update the following repositories: %s",
repoFailList)) repoFailList)
} }
fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
@ -158,10 +159,5 @@ func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) erro
} }
func isRepoRequested(repoName string, requestedRepos []string) bool { func isRepoRequested(repoName string, requestedRepos []string) bool {
for _, requestedRepo := range requestedRepos { return slices.Contains(requestedRepos, repoName)
if repoName == requestedRepo {
return true
}
}
return false
} }

@ -19,23 +19,22 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"helm.sh/helm/v3/internal/test/ensure" "helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo"
"helm.sh/helm/v3/pkg/repo/repotest" "helm.sh/helm/v4/pkg/repo/repotest"
) )
func TestUpdateCmd(t *testing.T) { func TestUpdateCmd(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test. // Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently. // The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error { updater := func(repos []*repo.ChartRepository, out io.Writer, _ bool) error {
for _, re := range repos { for _, re := range repos {
fmt.Fprintln(out, re.Config.Name) fmt.Fprintln(out, re.Config.Name)
} }
@ -60,7 +59,7 @@ func TestUpdateCmdMultiple(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test. // Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently. // The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error { updater := func(repos []*repo.ChartRepository, out io.Writer, _ bool) error {
for _, re := range repos { for _, re := range repos {
fmt.Fprintln(out, re.Config.Name) fmt.Fprintln(out, re.Config.Name)
} }
@ -86,7 +85,7 @@ func TestUpdateCmdInvalid(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test. // Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently. // The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error { updater := func(repos []*repo.ChartRepository, out io.Writer, _ bool) error {
for _, re := range repos { for _, re := range repos {
fmt.Fprintln(out, re.Config.Name) fmt.Fprintln(out, re.Config.Name)
} }
@ -103,15 +102,15 @@ func TestUpdateCmdInvalid(t *testing.T) {
} }
func TestUpdateCustomCacheCmd(t *testing.T) { func TestUpdateCustomCacheCmd(t *testing.T) {
rootDir := ensure.TempDir(t) rootDir := t.TempDir()
cachePath := filepath.Join(rootDir, "updcustomcache") cachePath := filepath.Join(rootDir, "updcustomcache")
os.Mkdir(cachePath, os.ModePerm) os.Mkdir(cachePath, os.ModePerm)
defer os.RemoveAll(cachePath)
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") ts := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testserver/*.*"),
} )
defer ts.Stop() defer ts.Stop()
o := &repoUpdateOptions{ o := &repoUpdateOptions{
@ -119,7 +118,7 @@ func TestUpdateCustomCacheCmd(t *testing.T) {
repoFile: filepath.Join(ts.Root(), "repositories.yaml"), repoFile: filepath.Join(ts.Root(), "repositories.yaml"),
repoCache: cachePath, repoCache: cachePath,
} }
b := ioutil.Discard b := io.Discard
if err := o.run(b); err != nil { if err := o.run(b); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -130,12 +129,11 @@ func TestUpdateCustomCacheCmd(t *testing.T) {
func TestUpdateCharts(t *testing.T) { func TestUpdateCharts(t *testing.T) {
defer resetEnv()() defer resetEnv()()
defer ensure.HelmHome(t)() ensure.HelmHome(t)
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") ts := repotest.NewTempServer(t,
if err != nil { repotest.WithChartSourceGlob("testdata/testserver/*.*"),
t.Fatal(err) )
}
defer ts.Stop() defer ts.Stop()
r, err := repo.NewChartRepository(&repo.Entry{ r, err := repo.NewChartRepository(&repo.Entry{
@ -165,12 +163,12 @@ func TestRepoUpdateFileCompletion(t *testing.T) {
func TestUpdateChartsFail(t *testing.T) { func TestUpdateChartsFail(t *testing.T) {
defer resetEnv()() defer resetEnv()()
defer ensure.HelmHome(t)() ensure.HelmHome(t)
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") ts := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testserver/*.*"),
} )
defer ts.Stop() defer ts.Stop()
var invalidURL = ts.URL() + "55" var invalidURL = ts.URL() + "55"
@ -198,12 +196,12 @@ func TestUpdateChartsFail(t *testing.T) {
func TestUpdateChartsFailWithError(t *testing.T) { func TestUpdateChartsFailWithError(t *testing.T) {
defer resetEnv()() defer resetEnv()()
defer ensure.HelmHome(t)() ensure.HelmHome(t)
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") ts := repotest.NewTempServer(
if err != nil { t,
t.Fatal(err) repotest.WithChartSourceGlob("testdata/testserver/*.*"),
} )
defer ts.Stop() defer ts.Stop()
var invalidURL = ts.URL() + "55" var invalidURL = ts.URL() + "55"

@ -17,7 +17,7 @@ package require
import ( import (
"fmt" "fmt"
"io/ioutil" "io"
"strings" "strings"
"testing" "testing"
@ -71,7 +71,8 @@ func runTestCases(t *testing.T, testCases []testCase) {
Args: tc.validateFunc, Args: tc.validateFunc,
} }
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard) cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
err := cmd.Execute() err := cmd.Execute()
if tc.wantError == "" { if tc.wantError == "" {

@ -24,16 +24,16 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v4/cmd/helm/require"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
) )
const rollbackDesc = ` const rollbackDesc = `
This command rolls back a release to a previous revision. 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 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 second is a revision (version) number. If this argument is omitted or set to
roll back to the previous release. 0, it will roll back to the previous release.
To see revision numbers, run 'helm history RELEASE'. To see revision numbers, run 'helm history RELEASE'.
` `
@ -46,7 +46,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Short: "roll back a release to a previous revision", Short: "roll back a release to a previous revision",
Long: rollbackDesc, Long: rollbackDesc,
Args: require.MinimumNArgs(1), Args: require.MinimumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 { if len(args) == 0 {
return compListReleases(toComplete, args, cfg) return compListReleases(toComplete, args, cfg)
} }
@ -55,9 +55,9 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return compListRevisions(toComplete, cfg, args[0]) return compListRevisions(toComplete, cfg, args[0])
} }
return nil, cobra.ShellCompDirectiveNoFileComp return noMoreArgsComp()
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
if len(args) > 1 { if len(args) > 1 {
ver, err := strconv.Atoi(args[1]) ver, err := strconv.Atoi(args[1])
if err != nil { if err != nil {

@ -17,10 +17,12 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"reflect"
"testing" "testing"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v4/pkg/release"
) )
func TestRollbackCmd(t *testing.T) { func TestRollbackCmd(t *testing.T) {
@ -64,6 +66,12 @@ func TestRollbackCmd(t *testing.T) {
cmd: "rollback funny-honey", cmd: "rollback funny-honey",
golden: "output/rollback-no-revision.txt", golden: "output/rollback-no-revision.txt",
rels: rels, 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", name: "rollback a release without release name",
cmd: "rollback", cmd: "rollback",
@ -115,3 +123,44 @@ func TestRollbackFileCompletion(t *testing.T) {
checkFileCompletion(t, "rollback myrelease", false) checkFileCompletion(t, "rollback myrelease", false)
checkFileCompletion(t, "rollback myrelease 1", false) checkFileCompletion(t, "rollback myrelease 1", false)
} }
func TestRollbackWithLabels(t *testing.T) {
labels1 := map[string]string{"operation": "install", "firstLabel": "firstValue"}
labels2 := map[string]string{"operation": "upgrade", "secondLabel": "secondValue"}
releaseName := "funny-bunny-labels"
rels := []*release.Release{
{
Name: releaseName,
Info: &release.Info{Status: release.StatusSuperseded},
Chart: &chart.Chart{},
Version: 1,
Labels: labels1,
},
{
Name: releaseName,
Info: &release.Info{Status: release.StatusDeployed},
Chart: &chart.Chart{},
Version: 2,
Labels: labels2,
},
}
storage := storageFixture()
for _, rel := range rels {
if err := storage.Create(rel); err != nil {
t.Fatal(err)
}
}
_, _, err := executeActionCommandC(storage, fmt.Sprintf("rollback %s 1", releaseName))
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
updatedRel, err := storage.Get(releaseName, 3)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
if !reflect.DeepEqual(updatedRel.Labels, labels1) {
t.Errorf("Expected {%v}, got {%v}", labels1, updatedRel.Labels)
}
}

@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main // import "helm.sh/helm/v3/cmd/helm" package main // import "helm.sh/helm/v4/cmd/helm"
import ( import (
"context" "context"
"fmt" "fmt"
"io" "io"
"log" "log"
"net/http"
"os" "os"
"strings" "strings"
@ -29,9 +30,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"helm.sh/helm/v3/internal/experimental/registry" "helm.sh/helm/v4/internal/tlsutil"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/registry"
"helm.sh/helm/v4/pkg/repo"
) )
var globalUsage = `The Kubernetes package manager var globalUsage = `The Kubernetes package manager
@ -45,28 +47,32 @@ Common actions for Helm:
Environment variables: Environment variables:
| Name | Description | | Name | Description |
|------------------------------------|-----------------------------------------------------------------------------------| |------------------------------------|------------------------------------------------------------------------------------------------------------|
| $HELM_CACHE_HOME | set an alternative location for storing cached files. | | $HELM_CACHE_HOME | set an alternative location for storing cached files. |
| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. | | $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. |
| $HELM_DATA_HOME | set an alternative location for storing Helm data. | | $HELM_DATA_HOME | set an alternative location for storing Helm data. |
| $HELM_DEBUG | indicate whether or not Helm is running in Debug mode | | $HELM_DEBUG | indicate whether or not Helm is running in Debug mode |
| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. | | $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. |
| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. | | $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. |
| $HELM_MAX_HISTORY | set the maximum number of helm release history. | | $HELM_MAX_HISTORY | set the maximum number of helm release history. |
| $HELM_NAMESPACE | set the namespace used for the helm operations. | | $HELM_NAMESPACE | set the namespace used for the helm operations. |
| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. | | $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. |
| $HELM_PLUGINS | set the path to the plugins directory | | $HELM_PLUGINS | set the path to the plugins directory |
| $HELM_REGISTRY_CONFIG | set the path to the registry config file. | | $HELM_REGISTRY_CONFIG | set the path to the registry config file. |
| $HELM_REPOSITORY_CACHE | set the path to the repository cache directory | | $HELM_REPOSITORY_CACHE | set the path to the repository cache directory |
| $HELM_REPOSITORY_CONFIG | set the path to the repositories file. | | $HELM_REPOSITORY_CONFIG | set the path to the repositories file. |
| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") | | $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") |
| $HELM_KUBEAPISERVER | set the Kubernetes API Server Endpoint for authentication | | $HELM_KUBEAPISERVER | set the Kubernetes API Server Endpoint for authentication |
| $HELM_KUBECAFILE | set the Kubernetes certificate authority file. | | $HELM_KUBECAFILE | set the Kubernetes certificate authority file. |
| $HELM_KUBEASGROUPS | set the Groups to use for impersonation using a comma-separated list. | | $HELM_KUBEASGROUPS | set the Groups to use for impersonation using a comma-separated list. |
| $HELM_KUBEASUSER | set the Username to impersonate for the operation. | | $HELM_KUBEASUSER | set the Username to impersonate for the operation. |
| $HELM_KUBECONTEXT | set the name of the kubeconfig context. | | $HELM_KUBECONTEXT | set the name of the kubeconfig context. |
| $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. | | $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. |
| $HELM_KUBEINSECURE_SKIP_TLS_VERIFY | indicate if the Kubernetes API server's certificate validation should be skipped (insecure) |
| $HELM_KUBETLS_SERVER_NAME | set the server name used to validate the Kubernetes API server certificate |
| $HELM_BURST_LIMIT | set the default burst limit in the case the server contains many CRDs (default 100, -1 to disable) |
| $HELM_QPS | set the Queries Per Second in cases where a high number of calls exceed the option for higher burst values |
Helm stores cache, configuration, and data based on the following configuration order: Helm stores cache, configuration, and data based on the following configuration order:
@ -89,6 +95,16 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
Short: "The Helm package manager for Kubernetes.", Short: "The Helm package manager for Kubernetes.",
Long: globalUsage, Long: globalUsage,
SilenceUsage: true, SilenceUsage: true,
PersistentPreRun: func(_ *cobra.Command, _ []string) {
if err := startProfiling(); err != nil {
log.Printf("Warning: Failed to start profiling: %v", err)
}
},
PersistentPostRun: func(_ *cobra.Command, _ []string) {
if err := stopProfiling(); err != nil {
log.Printf("Warning: Failed to stop profiling: %v", err)
}
},
} }
flags := cmd.PersistentFlags() flags := cmd.PersistentFlags()
@ -96,7 +112,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
addKlogFlags(flags) addKlogFlags(flags)
// Setup shell completion for the namespace flag // Setup shell completion for the namespace flag
err := cmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("namespace", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
if client, err := actionConfig.KubernetesClientSet(); err == nil { if client, err := actionConfig.KubernetesClientSet(); err == nil {
// Choose a long enough timeout that the user notices something is not working // Choose a long enough timeout that the user notices something is not working
// but short enough that the user is not made to wait very long // but short enough that the user is not made to wait very long
@ -106,9 +122,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
nsNames := []string{} nsNames := []string{}
if namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{TimeoutSeconds: &to}); err == nil { if namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{TimeoutSeconds: &to}); err == nil {
for _, ns := range namespaces.Items { for _, ns := range namespaces.Items {
if strings.HasPrefix(ns.Name, toComplete) { nsNames = append(nsNames, ns.Name)
nsNames = append(nsNames, ns.Name)
}
} }
return nsNames, cobra.ShellCompDirectiveNoFileComp return nsNames, cobra.ShellCompDirectiveNoFileComp
} }
@ -121,7 +135,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
} }
// Setup shell completion for the kube-context flag // Setup shell completion for the kube-context flag
err = cmd.RegisterFlagCompletionFunc("kube-context", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err = cmd.RegisterFlagCompletionFunc("kube-context", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
cobra.CompDebugln("About to get the different kube-contexts", settings.Debug) cobra.CompDebugln("About to get the different kube-contexts", settings.Debug)
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
@ -133,9 +147,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
&clientcmd.ConfigOverrides{}).RawConfig(); err == nil { &clientcmd.ConfigOverrides{}).RawConfig(); err == nil {
comps := []string{} comps := []string{}
for name, context := range config.Contexts { for name, context := range config.Contexts {
if strings.HasPrefix(name, toComplete) { comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster))
comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster))
}
} }
return comps, cobra.ShellCompDirectiveNoFileComp return comps, cobra.ShellCompDirectiveNoFileComp
} }
@ -153,11 +165,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
flags.ParseErrorsWhitelist.UnknownFlags = true flags.ParseErrorsWhitelist.UnknownFlags = true
flags.Parse(args) flags.Parse(args)
registryClient, err := registry.NewClient( registryClient, err := newDefaultRegistryClient(false, "", "")
registry.ClientOptDebug(settings.Debug),
registry.ClientOptWriter(out),
registry.ClientOptCredentialsFile(settings.RegistryConfig),
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -169,7 +177,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
newCreateCmd(out), newCreateCmd(out),
newDependencyCmd(actionConfig, out), newDependencyCmd(actionConfig, out),
newPullCmd(actionConfig, out), newPullCmd(actionConfig, out),
newShowCmd(out), newShowCmd(actionConfig, out),
newLintCmd(out), newLintCmd(out),
newPackageCmd(out), newPackageCmd(out),
newRepoCmd(out), newRepoCmd(out),
@ -197,7 +205,6 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
newDocsCmd(out), newDocsCmd(out),
) )
// Add *experimental* subcommands
cmd.AddCommand( cmd.AddCommand(
newRegistryCmd(actionConfig, out), newRegistryCmd(actionConfig, out),
newPushCmd(actionConfig, out), newPushCmd(actionConfig, out),
@ -206,9 +213,6 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
// Find and add plugins // Find and add plugins
loadPlugins(cmd, out) loadPlugins(cmd, out)
// Check permissions on critical files
checkPerms()
// Check for expired repositories // Check for expired repositories
checkForExpiredRepos(settings.RepositoryConfig) checkForExpiredRepos(settings.RepositoryConfig)
@ -235,7 +239,7 @@ func checkForExpiredRepos(repofile string) {
} }
// parse repo file. // parse repo file.
// Ignore the error because it is okay for a repo file to be unparseable at this // Ignore the error because it is okay for a repo file to be unparsable at this
// stage. Later checks will trap the error and respond accordingly. // stage. Later checks will trap the error and respond accordingly.
repoFile, err := repo.LoadFile(repofile) repoFile, err := repo.LoadFile(repofile)
if err != nil { if err != nil {
@ -263,11 +267,71 @@ func checkForExpiredRepos(repofile string) {
} }
// When dealing with OCI-based charts, ensure that the user has func newRegistryClient(
// enabled the experimental feature gate prior to continuing certFile, keyFile, caFile string, insecureSkipTLSverify, plainHTTP bool, username, password string,
func checkOCI(ref string) error { ) (*registry.Client, error) {
if registry.IsOCI(ref) && !FeatureGateOCI.IsEnabled() { if certFile != "" && keyFile != "" || caFile != "" || insecureSkipTLSverify {
return FeatureGateOCI.Error() registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSverify, username, password)
if err != nil {
return nil, err
}
return registryClient, nil
}
registryClient, err := newDefaultRegistryClient(plainHTTP, username, password)
if err != nil {
return nil, err
}
return registryClient, nil
}
func newDefaultRegistryClient(plainHTTP bool, username, password string) (*registry.Client, error) {
opts := []registry.ClientOption{
registry.ClientOptDebug(settings.Debug),
registry.ClientOptEnableCache(true),
registry.ClientOptWriter(os.Stderr),
registry.ClientOptCredentialsFile(settings.RegistryConfig),
registry.ClientOptBasicAuth(username, password),
}
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, username, password string,
) (*registry.Client, error) {
tlsConf, err := tlsutil.NewTLSConfig(
tlsutil.WithInsecureSkipVerify(insecureSkipTLSverify),
tlsutil.WithCertKeyPairFiles(certFile, keyFile),
tlsutil.WithCAFile(caFile),
)
if err != nil {
return nil, fmt.Errorf("can't create TLS config for client: %w", err)
}
// Create a new registry client
registryClient, err := registry.NewClient(
registry.ClientOptDebug(settings.Debug),
registry.ClientOptEnableCache(true),
registry.ClientOptWriter(os.Stderr),
registry.ClientOptCredentialsFile(settings.RegistryConfig),
registry.ClientOptHTTPClient(&http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConf,
},
}),
registry.ClientOptBasicAuth(username, password),
)
if err != nil {
return nil, err
} }
return nil return registryClient, nil
} }

@ -21,9 +21,9 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"helm.sh/helm/v3/internal/test/ensure" "helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v3/pkg/helmpath/xdg" "helm.sh/helm/v4/pkg/helmpath/xdg"
) )
func TestRootCmd(t *testing.T) { func TestRootCmd(t *testing.T) {
@ -77,7 +77,7 @@ func TestRootCmd(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
defer ensure.HelmHome(t)() ensure.HelmHome(t)
for k, v := range tt.envvars { for k, v := range tt.envvars {
os.Setenv(k, v) os.Setenv(k, v)

@ -1,59 +0,0 @@
//go:build !windows
// +build !windows
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"os"
"os/user"
"path/filepath"
)
func checkPerms() {
// This function MUST NOT FAIL, as it is just a check for a common permissions problem.
// If for some reason the function hits a stopping condition, it may panic. But only if
// we can be sure that it is panicking because Helm cannot proceed.
kc := settings.KubeConfig
if kc == "" {
kc = os.Getenv("KUBECONFIG")
}
if kc == "" {
u, err := user.Current()
if err != nil {
// No idea where to find KubeConfig, so return silently. Many helm commands
// can proceed happily without a KUBECONFIG, so this is not a fatal error.
return
}
kc = filepath.Join(u.HomeDir, ".kube", "config")
}
fi, err := os.Stat(kc)
if err != nil {
// DO NOT error if no KubeConfig is found. Not all commands require one.
return
}
perm := fi.Mode().Perm()
if perm&0040 > 0 {
warning("Kubernetes configuration file is group-readable. This is insecure. Location: %s", kc)
}
if perm&0004 > 0 {
warning("Kubernetes configuration file is world-readable. This is insecure. Location: %s", kc)
}
}

@ -1,88 +0,0 @@
//go:build !windows
// +build !windows
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
)
func checkPermsStderr() (string, error) {
r, w, err := os.Pipe()
if err != nil {
return "", err
}
stderr := os.Stderr
os.Stderr = w
defer func() {
os.Stderr = stderr
}()
checkPerms()
w.Close()
var text bytes.Buffer
io.Copy(&text, r)
return text.String(), nil
}
func TestCheckPerms(t *testing.T) {
tdir, err := ioutil.TempDir("", "helmtest")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tdir)
tfile := filepath.Join(tdir, "testconfig")
fh, err := os.OpenFile(tfile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0440)
if err != nil {
t.Errorf("Failed to create temp file: %s", err)
}
tconfig := settings.KubeConfig
settings.KubeConfig = tfile
defer func() { settings.KubeConfig = tconfig }()
text, err := checkPermsStderr()
if err != nil {
t.Fatalf("could not read from stderr: %s", err)
}
expectPrefix := "WARNING: Kubernetes configuration file is group-readable. This is insecure. Location:"
if !strings.HasPrefix(text, expectPrefix) {
t.Errorf("Expected to get a warning for group perms. Got %q", text)
}
if err := fh.Chmod(0404); err != nil {
t.Errorf("Could not change mode on file: %s", err)
}
text, err = checkPermsStderr()
if err != nil {
t.Fatalf("could not read from stderr: %s", err)
}
expectPrefix = "WARNING: Kubernetes configuration file is world-readable. This is insecure. Location:"
if !strings.HasPrefix(text, expectPrefix) {
t.Errorf("Expected to get a warning for world perms. Got %q", text)
}
}

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. 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 This supports building an in-memory search index based on the contents of
multiple repositories, and then using string matching or regular expressions multiple repositories, and then using string matching or regular expressions
@ -30,7 +31,7 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo"
) )
// Result is a search result. // Result is a search result.
@ -146,11 +147,10 @@ func (i *Index) SearchLiteral(term string, threshold int) []*Result {
term = strings.ToLower(term) term = strings.ToLower(term)
buf := []*Result{} buf := []*Result{}
for k, v := range i.lines { for k, v := range i.lines {
lk := strings.ToLower(k)
lv := strings.ToLower(v) lv := strings.ToLower(v)
res := strings.Index(lv, term) res := strings.Index(lv, term)
if score := i.calcScore(res, lv); res != -1 && score < threshold { 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]}) buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]})
} }
} }

@ -20,8 +20,8 @@ import (
"strings" "strings"
"testing" "testing"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo"
) )
func TestSortScore(t *testing.T) { func TestSortScore(t *testing.T) {
@ -101,15 +101,15 @@ var indexfileEntries = map[string]repo.ChartVersions{
}, },
} }
func loadTestIndex(t *testing.T, all bool) *Index { func loadTestIndex(_ *testing.T, all bool) *Index {
i := NewIndex() i := NewIndex()
i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all) i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all)
i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{ i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{
"pinta": { "Pinta": {
{ {
URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"}, URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"},
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
Name: "pinta", Name: "Pinta",
Version: "2.0.0", Version: "2.0.0",
Description: "Two ship, version two", Description: "Two ship, version two",
}, },
@ -170,14 +170,14 @@ func TestSearchByName(t *testing.T) {
query: "pinta", query: "pinta",
expect: []*Result{ expect: []*Result{
{Name: "testing/pinta"}, {Name: "testing/pinta"},
{Name: "ztesting/pinta"}, {Name: "ztesting/Pinta"},
}, },
}, },
{ {
name: "repo-specific search for one result", name: "repo-specific search for one result",
query: "ztesting/pinta", query: "ztesting/pinta",
expect: []*Result{ expect: []*Result{
{Name: "ztesting/pinta"}, {Name: "ztesting/Pinta"},
}, },
}, },
{ {
@ -199,7 +199,15 @@ func TestSearchByName(t *testing.T) {
query: "two", query: "two",
expect: []*Result{ expect: []*Result{
{Name: "testing/pinta"}, {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", query: "TWO",
expect: []*Result{ expect: []*Result{
{Name: "testing/pinta"}, {Name: "testing/pinta"},
{Name: "ztesting/pinta"}, {Name: "ztesting/Pinta"},
}, },
}, },
{ {

@ -25,8 +25,8 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/internal/monocular" "helm.sh/helm/v4/internal/monocular"
"helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
) )
const searchHubDesc = ` const searchHubDesc = `
@ -53,6 +53,8 @@ type searchHubOptions struct {
searchEndpoint string searchEndpoint string
maxColWidth uint maxColWidth uint
outputFormat output.Format outputFormat output.Format
listRepoURL bool
failOnNoResult bool
} }
func newSearchHubCmd(out io.Writer) *cobra.Command { func newSearchHubCmd(out io.Writer) *cobra.Command {
@ -62,7 +64,7 @@ func newSearchHubCmd(out io.Writer) *cobra.Command {
Use: "hub [KEYWORD]", Use: "hub [KEYWORD]",
Short: "search for charts in the Artifact Hub or your own hub instance", Short: "search for charts in the Artifact Hub or your own hub instance",
Long: searchHubDesc, Long: searchHubDesc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
return o.run(out, args) return o.run(out, args)
}, },
} }
@ -70,6 +72,9 @@ func newSearchHubCmd(out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.StringVar(&o.searchEndpoint, "endpoint", "https://hub.helm.sh", "Hub instance to query for charts") 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.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) bindOutputFlag(cmd, &o.outputFormat)
return cmd return cmd
@ -88,22 +93,30 @@ func (o *searchHubOptions) run(out io.Writer, args []string) error {
return fmt.Errorf("unable to perform search against %q", o.searchEndpoint) return fmt.Errorf("unable to perform search against %q", o.searchEndpoint)
} }
return o.outputFormat.Write(out, newHubSearchWriter(results, o.searchEndpoint, o.maxColWidth)) return o.outputFormat.Write(out, newHubSearchWriter(results, o.searchEndpoint, o.maxColWidth, o.listRepoURL, o.failOnNoResult))
}
type hubChartRepo struct {
URL string `json:"url"`
Name string `json:"name"`
} }
type hubChartElement struct { type hubChartElement struct {
URL string `json:"url"` URL string `json:"url"`
Version string `json:"version"` Version string `json:"version"`
AppVersion string `json:"app_version"` AppVersion string `json:"app_version"`
Description string `json:"description"` Description string `json:"description"`
Repository hubChartRepo `json:"repository"`
} }
type hubSearchWriter struct { type hubSearchWriter struct {
elements []hubChartElement elements []hubChartElement
columnWidth uint columnWidth uint
listRepoURL bool
failOnNoResult bool
} }
func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint) *hubSearchWriter { func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint, listRepoURL, failOnNoResult bool) *hubSearchWriter {
var elements []hubChartElement var elements []hubChartElement
for _, r := range results { for _, r := range results {
// Backwards compatibility for Monocular // Backwards compatibility for Monocular
@ -114,13 +127,18 @@ func newHubSearchWriter(results []monocular.SearchResult, endpoint string, colum
url = r.ArtifactHub.PackageURL url = r.ArtifactHub.PackageURL
} }
elements = append(elements, hubChartElement{url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.Description}) elements = append(elements, hubChartElement{url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.Description, hubChartRepo{URL: r.Attributes.Repo.URL, Name: r.Attributes.Repo.Name}})
} }
return &hubSearchWriter{elements, columnWidth} return &hubSearchWriter{elements, columnWidth, listRepoURL, failOnNoResult}
} }
func (h *hubSearchWriter) WriteTable(out io.Writer) error { func (h *hubSearchWriter) WriteTable(out io.Writer) error {
if len(h.elements) == 0 { 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")) _, err := out.Write([]byte("No results found\n"))
if err != nil { if err != nil {
return fmt.Errorf("unable to write results: %s", err) return fmt.Errorf("unable to write results: %s", err)
@ -129,9 +147,19 @@ func (h *hubSearchWriter) WriteTable(out io.Writer) error {
} }
table := uitable.New() table := uitable.New()
table.MaxColWidth = h.columnWidth table.MaxColWidth = h.columnWidth
table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION")
if h.listRepoURL {
table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION", "REPO URL")
} else {
table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION")
}
for _, r := range h.elements { for _, r := range h.elements {
table.AddRow(r.URL, r.Version, r.AppVersion, r.Description) if h.listRepoURL {
table.AddRow(r.URL, r.Version, r.AppVersion, r.Description, r.Repository.URL)
} else {
table.AddRow(r.URL, r.Version, r.AppVersion, r.Description)
}
} }
return output.EncodeTable(out, table) return output.EncodeTable(out, table)
} }
@ -145,11 +173,16 @@ func (h *hubSearchWriter) WriteYAML(out io.Writer) error {
} }
func (h *hubSearchWriter) encodeByFormat(out io.Writer, format output.Format) error { 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 // Initialize the array so no results returns an empty array instead of null
chartList := make([]hubChartElement, 0, len(h.elements)) chartList := make([]hubChartElement, 0, len(h.elements))
for _, r := range h.elements { for _, r := range h.elements {
chartList = append(chartList, hubChartElement{r.URL, r.Version, r.AppVersion, r.Description}) chartList = append(chartList, hubChartElement{r.URL, r.Version, r.AppVersion, r.Description, r.Repository})
} }
switch format { switch format {

@ -27,12 +27,14 @@ func TestSearchHubCmd(t *testing.T) {
// Setup a mock search service // Setup a mock search service
var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}` var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, searchResult) fmt.Fprintln(w, searchResult)
})) }))
defer ts.Close() defer ts.Close()
// The expected output has the URL to the mocked search service in it // The expected output has the URL to the mocked search service in it
// Trailing spaces are necessary to preserve in "expected" as the uitable package adds
// them during printing.
var expected = fmt.Sprintf(`URL CHART VERSION APP VERSION DESCRIPTION var expected = fmt.Sprintf(`URL CHART VERSION APP VERSION DESCRIPTION
%s/charts/stable/phpmyadmin 3.0.0 4.9.0-1 phpMyAdmin is an mysql administration frontend %s/charts/stable/phpmyadmin 3.0.0 4.9.0-1 phpMyAdmin is an mysql administration frontend
%s/charts/bitnami/phpmyadmin 3.0.0 4.9.0-1 phpMyAdmin is an mysql administration frontend %s/charts/bitnami/phpmyadmin 3.0.0 4.9.0-1 phpMyAdmin is an mysql administration frontend
@ -51,6 +53,36 @@ func TestSearchHubCmd(t *testing.T) {
} }
} }
func TestSearchHubListRepoCmd(t *testing.T) {
// Setup a mock search service
var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, searchResult)
}))
defer ts.Close()
// The expected output has the URL to the mocked search service in it
// Trailing spaces are necessary to preserve in "expected" as the uitable package adds
// them during printing.
var expected = fmt.Sprintf(`URL CHART VERSION APP VERSION DESCRIPTION REPO URL
%s/charts/stable/phpmyadmin 3.0.0 4.9.0-1 phpMyAdmin is an mysql administration frontend https://charts.helm.sh/stable
%s/charts/bitnami/phpmyadmin 3.0.0 4.9.0-1 phpMyAdmin is an mysql administration frontend https://charts.bitnami.com
`, ts.URL, ts.URL)
testcmd := "search hub --list-repo-url --endpoint " + ts.URL + " maria"
storage := storageFixture()
_, out, err := executeActionCommandC(storage, testcmd)
if err != nil {
t.Errorf("unexpected error, %s", err)
}
if out != expected {
t.Error("expected and actual output did not match")
t.Log(out)
t.Log(expected)
}
}
func TestSearchHubOutputCompletion(t *testing.T) { func TestSearchHubOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "search hub") outputFlagCompletionTest(t, "search hub")
} }
@ -58,3 +90,98 @@ func TestSearchHubOutputCompletion(t *testing.T) {
func TestSearchHubFileCompletion(t *testing.T) { func TestSearchHubFileCompletion(t *testing.T) {
checkFileCompletion(t, "search hub", true) // File completion may be useful when inputting a keyword 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, _ *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)
}
})
}
}

@ -21,7 +21,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -31,10 +30,10 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/search" "helm.sh/helm/v4/cmd/helm/search"
"helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v4/pkg/repo"
) )
const searchRepoDesc = ` const searchRepoDesc = `
@ -64,14 +63,15 @@ Repositories are managed with 'helm repo' commands.
const searchMaxScore = 25 const searchMaxScore = 25
type searchRepoOptions struct { type searchRepoOptions struct {
versions bool versions bool
regexp bool regexp bool
devel bool devel bool
version string version string
maxColWidth uint maxColWidth uint
repoFile string repoFile string
repoCacheDir string repoCacheDir string
outputFormat output.Format outputFormat output.Format
failOnNoResult bool
} }
func newSearchRepoCmd(out io.Writer) *cobra.Command { func newSearchRepoCmd(out io.Writer) *cobra.Command {
@ -81,7 +81,7 @@ func newSearchRepoCmd(out io.Writer) *cobra.Command {
Use: "repo [keyword]", Use: "repo [keyword]",
Short: "search repositories for a keyword in charts", Short: "search repositories for a keyword in charts",
Long: searchRepoDesc, Long: searchRepoDesc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
o.repoFile = settings.RepositoryConfig o.repoFile = settings.RepositoryConfig
o.repoCacheDir = settings.RepositoryCache o.repoCacheDir = settings.RepositoryCache
return o.run(out, args) return o.run(out, args)
@ -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.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.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.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) bindOutputFlag(cmd, &o.outputFormat)
return cmd return cmd
@ -124,7 +126,7 @@ func (o *searchRepoOptions) run(out io.Writer, args []string) error {
return err 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() { func (o *searchRepoOptions) setupSearchedVersion() {
@ -137,7 +139,7 @@ func (o *searchRepoOptions) setupSearchedVersion() {
if o.devel { // search for releases and prereleases (alpha, beta, and release candidate releases). if o.devel { // search for releases and prereleases (alpha, beta, and release candidate releases).
debug("setting version to >0.0.0-0") debug("setting version to >0.0.0-0")
o.version = ">0.0.0-0" o.version = ">0.0.0-0"
} else { // search only for stable releases, prerelease versions will be skip } else { // search only for stable releases, prerelease versions will be skipped
debug("setting version to >0.0.0") debug("setting version to >0.0.0")
o.version = ">0.0.0" o.version = ">0.0.0"
} }
@ -205,12 +207,18 @@ type repoChartElement struct {
} }
type repoSearchWriter struct { type repoSearchWriter struct {
results []*search.Result results []*search.Result
columnWidth uint columnWidth uint
failOnNoResult bool
} }
func (r *repoSearchWriter) WriteTable(out io.Writer) error { func (r *repoSearchWriter) WriteTable(out io.Writer) error {
if len(r.results) == 0 { 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")) _, err := out.Write([]byte("No results found\n"))
if err != nil { if err != nil {
return fmt.Errorf("unable to write results: %s", err) 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 { 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 // Initialize the array so no results returns an empty array instead of null
chartList := make([]repoChartElement, 0, len(r.results)) chartList := make([]repoChartElement, 0, len(r.results))
@ -259,7 +272,7 @@ func compListChartsOfRepo(repoName string, prefix string) []string {
var charts []string var charts []string
path := filepath.Join(settings.RepositoryCache, helmpath.CacheChartsFile(repoName)) path := filepath.Join(settings.RepositoryCache, helmpath.CacheChartsFile(repoName))
content, err := ioutil.ReadFile(path) content, err := os.ReadFile(path)
if err == nil { if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(content)) scanner := bufio.NewScanner(bytes.NewReader(content))
for scanner.Scan() { for scanner.Scan() {
@ -312,8 +325,9 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
} }
repoWithSlash := fmt.Sprintf("%s/", repo) repoWithSlash := fmt.Sprintf("%s/", repo)
if strings.HasPrefix(toComplete, repoWithSlash) { if strings.HasPrefix(toComplete, repoWithSlash) {
// Must complete with charts within the specified repo // Must complete with charts within the specified repo.
completions = append(completions, compListChartsOfRepo(repo, toComplete)...) // Don't filter on toComplete to allow for shell fuzzy matching
completions = append(completions, compListChartsOfRepo(repo, "")...)
noSpace = false noSpace = false
break break
} else if strings.HasPrefix(repo, toComplete) { } else if strings.HasPrefix(repo, toComplete) {
@ -325,7 +339,7 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
cobra.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions), settings.Debug) cobra.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions), settings.Debug)
// Now handle completions for url prefixes // Now handle completions for url prefixes
for _, url := range []string{"https://\tChart URL prefix", "http://\tChart URL prefix", "file://\tChart local URL prefix"} { for _, url := range []string{"oci://\tChart OCI prefix", "https://\tChart URL prefix", "http://\tChart URL prefix", "file://\tChart local URL prefix"} {
if strings.HasPrefix(toComplete, url) { if strings.HasPrefix(toComplete, url) {
// The user already put in the full url prefix; we don't have // The user already put in the full url prefix; we don't have
// anything to add, but make sure the shell does not default // anything to add, but make sure the shell does not default

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save