Merge remote-tracking branch 'upstream/main' into asset-transparency-github-action

pull/8697/head
Joe Julian 2 years ago
commit 7aeac9c547
No known key found for this signature in database
GPG Key ID: FAB12BE0575D999B

@ -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,36 +1,14 @@
---
# This file can be removed when Helm no longer uses CircleCI on any release
# branches. Once CircleCI is turned off this file can be removed.
version: 2
jobs:
build:
working_directory: ~/helm.sh/helm
docker:
- image: circleci/golang:1.14
environment:
GOCACHE: "/tmp/go/cache"
GOLANGCI_LINT_VERSION: "1.27.0"
- image: cimg/go:1.18
steps:
- checkout
- run:
name: install test dependencies
command: .circleci/bootstrap.sh
- run:
name: test style
command: make test-style
- run:
name: test
command: make test-coverage
- deploy:
name: deploy
command: .circleci/deploy.sh
workflows:
version: 2
build:
jobs:
- build:
filters:
tags:
only: /.*/

@ -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:-}" == "master" ]]; 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"

@ -4,4 +4,8 @@ updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

@ -1,5 +1,5 @@
<!-- Thanks for sending a pull request! Here are some tips for you:
1. Make sure to read the Contributing Guide before submitting your PR: https://github.com/helm/helm/blob/master/CONTRIBUTING.md
1. Make sure to read the Contributing Guide before submitting your PR: https://github.com/helm/helm/blob/main/CONTRIBUTING.md
2. If this PR closes another issue, add 'closes #<issue number>' somewhere in the PR summary. GitHub will automatically close that issue when this PR gets merged. Alternatively, adding 'refs #<issue number>' will not close the issue, but help provide the reviewer more context.-->
**What this PR does / why we need it**:

@ -0,0 +1,36 @@
name: build-test
on:
push:
branches:
- 'main'
- 'release-**'
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # pin@v3.5.3
- name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # pin@4.1.0
with:
go-version: '1.20'
- name: Install golangci-lint
run: |
curl -sSLO https://github.com/golangci/golangci-lint/releases/download/v$GOLANGCI_LINT_VERSION/golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz
shasum -a 256 golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz | grep "^$GOLANGCI_LINT_SHA256 " > /dev/null
tar -xf golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz
sudo mv golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64/golangci-lint /usr/local/bin/golangci-lint
rm -rf golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64*
env:
GOLANGCI_LINT_VERSION: '1.51.2'
GOLANGCI_LINT_SHA256: '4de479eb9d9bc29da51aec1834e7c255b333723d38dbd56781c68e5dddc6a90b'
- name: Test style
run: make test-style
- name: Run unit tests
run: make test-coverage
- name: Test build
run: make build

@ -0,0 +1,67 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '29 6 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # pin@v3.5.3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@a09933a12a80f87b87005513f0abb1494c27a716 # pinv2.21.4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@a09933a12a80f87b87005513f0abb1494c27a716 # pinv2.21.4
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@a09933a12a80f87b87005513f0abb1494c27a716 # pinv2.21.4

@ -0,0 +1,75 @@
name: release
on:
create:
tags:
- v*
push:
branches:
- main
# Note the only differences between release and canary-release jobs are:
# - only canary passes --overwrite flag
# - the VERSION make variable passed to 'make dist checksum' is expected to
# be "canary" if the job is triggered by a push to "main" branch. If the
# job is triggered by a tag push, VERSION should be the tag ref.
jobs:
release:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # pin@v3.5.3
- name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # pin@4.1.0
with:
go-version: '1.20'
- name: Run unit tests
run: make test-coverage
- name: Build Helm Binaries
run: |
make build-cross
make dist checksum VERSION="${{ github.ref_name }}"
- name: Upload Binaries
uses: bacongobbler/azure-blob-storage-upload@50f7d898b7697e864130ea04c303ca38b5751c50 # pin@3.0.0
env:
AZURE_STORAGE_CONNECTION_STRING: "${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}"
AZURE_STORAGE_CONTAINER_NAME: "${{ secrets.AZURE_STORAGE_CONTAINER_NAME }}"
with:
source_dir: _dist
container_name: ${{ secrets.AZURE_STORAGE_CONTAINER_NAME }}
connection_string: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}
extra_args: '--pattern helm-*'
canary-release:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout source code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # pin@v3.5.3
- name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # pin@4.1.0
with:
go-version: '1.20'
- name: Run unit tests
run: make test-coverage
- name: Build Helm Binaries
run: |
make build-cross
make dist checksum VERSION="canary"
- name: Upload Binaries
uses: bacongobbler/azure-blob-storage-upload@50f7d898b7697e864130ea04c303ca38b5751c50 # pin@3.0.0
with:
source_dir: _dist
container_name: ${{ secrets.AZURE_STORAGE_CONTAINER_NAME }}
connection_string: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}
extra_args: '--pattern helm-*'
# WARNING: this will overwrite existing blobs in your blob storage
overwrite: 'true'

@ -6,10 +6,11 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
- uses: actions/stale@v3.0.14
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.'
exempt-issue-labels: 'keep+open,v4.x'
exempt-issue-labels: 'keep open,v4.x,in progress'
days-before-stale: 90
days-before-close: 30
operations-per-run: 100

1
.gitignore vendored

@ -1,4 +1,5 @@
*.exe
*.swp
.DS_Store
.coverage/
.idea/

@ -1,22 +1,19 @@
run:
timeout: 2m
timeout: 10m
linters:
disable-all: true
enable:
- deadcode
- dupl
- gofmt
- goimports
- golint
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- structcheck
- revive
- unused
- varcheck
- staticcheck
linters-settings:

@ -191,7 +191,9 @@ below.
issue to a milestone until the questions are answered.
- We attempt to do this process at least once per work day.
3. Discussion
- issues that are labeled as `feature` or `bug` should be connected to the PR that resolves it.
- Issues that are labeled `feature` or `proposal` must write a Helm Improvement Proposal (HIP).
See [Proposing an Idea](#proposing-an-idea). Smaller quality-of-life enhancements are exempt.
- Issues that are labeled as `feature` or `bug` should be connected to the PR that resolves it.
- Whoever is working on a `feature` or `bug` issue (whether a maintainer or someone from the
community), should either assign the issue to themself or make a comment in the issue saying
that they are taking it.
@ -200,9 +202,30 @@ below.
and reduce noise. Should the issue need to stay open, the `keep open` label can be added.
4. Issue closure
## Proposing an Idea
Before proposing a new idea to the Helm project, please make sure to write up a [Helm Improvement
Proposal](https://github.com/helm/community/tree/master/hips). A Helm Improvement Proposal is a
design document that describes a new feature for the Helm project. The proposal should provide a
concise technical specification and rationale for the feature.
It is also worth considering vetting your idea with the community via the
[cncf-helm](mailto:cncf-helm@lists.cncf.io) mailing list. Vetting an idea publicly before going as
far as writing a proposal is meant to save the potential author time. Many ideas have been proposed;
it's quite likely there are others in the community who may be working on a similar proposal, or a
similar proposal may have already been written.
HIPs are submitted to the [helm/community repository](https://github.com/helm/community). [HIP
1](https://github.com/helm/community/blob/master/hips/hip-0001.md) describes the process to write a
HIP as well as the review process.
After your proposal has been approved, follow the [developer's
guide](https://helm.sh/docs/community/developers/) to get started.
## How to Contribute a Patch
1. Identify or create the related issue.
1. Identify or create the related issue. If you're proposing a larger change to
Helm, see [Proposing an Idea](#proposing-an-idea).
2. Fork the desired repo; develop and test your code changes.
3. Submit a pull request, making sure to sign your work and link the related issue.
@ -232,10 +255,11 @@ Like any good open source project, we use Pull Requests (PRs) to track code chan
3. Assigning reviews
- Once a review has the `awaiting review` label, maintainers will review them as schedule
permits. The maintainer who takes the issue should self-request a review.
- Any PR with the `size/large` label requires 2 review approvals from maintainers before it can
be merged. Those with `size/medium` or `size/small` are per the judgement of the maintainers.
- PRs from a community member with the label `size/S` or larger requires 2 review approvals from
maintainers before it can be merged. Those with `size/XS` are per the judgement of the
maintainers. For more detail see the [Size Labels](#size-labels) section.
4. Reviewing/Discussion
- All reviews will be completed using Github review tool.
- All reviews will be completed using GitHub review tool.
- A "Comment" review should be used when there are questions about the code that should be
answered, but that don't involve code changes. This type of review does not count as approval.
- A "Changes Requested" review indicates that changes to the code need to be made before they
@ -313,15 +337,16 @@ makes 30 lines of changes in 1 file, but it changes key functionality, it will l
feature, but requires another 150 lines of tests to cover all cases, could be labeled as `size/S`
even though the number of lines is greater than defined below.
PRs submitted by a core maintainer, regardless of size, only requires approval from one additional
maintainer. This ensures there are at least two maintainers who are aware of any significant PRs
introduced to the codebase.
Any changes from the community labeled as `size/S` or larger should be thoroughly tested before
merging and always requires approval from 2 core maintainers. PRs submitted by a core maintainer,
regardless of size, only requires approval from one additional maintainer. This ensures there are at
least two maintainers who are aware of any significant PRs introduced to the codebase.
| Label | Description |
| ----- | ----------- |
| `size/XS` | Denotes a PR that changes 0-9 lines, ignoring generated files. Very little testing may be required depending on the change. |
| `size/S` | Denotes a PR that changes 10-29 lines, ignoring generated files. Only small amounts of manual testing may be required. |
| `size/M` | Denotes a PR that changes 30-99 lines, ignoring generated files. Manual validation should be required. |
| `size/L` | Denotes a PR that changes 100-499 lines, ignoring generated files. This should be thoroughly tested before merging and always requires 2 approvals. |
| `size/XL` | Denotes a PR that changes 500-999 lines, ignoring generated files. This should be thoroughly tested before merging and always requires 2 approvals. |
| `size/XXL` | Denotes a PR that changes 1000+ lines, ignoring generated files. This should be thoroughly tested before merging and always requires 2 approvals. |
| `size/L` | Denotes a PR that changes 100-499 lines, ignoring generated files. |
| `size/XL` | Denotes a PR that changes 500-999 lines, ignoring generated files. |
| `size/XXL` | Denotes a PR that changes 1000+ lines, ignoring generated files. |

88
KEYS

@ -852,3 +852,91 @@ ANQIfZg7P8oNxVDAX+jIsTDxjh8r+S1wsUQcTNop6JMicDbxrBRB13vYIY0Jg4+Z
hS0eN8yqaR533ire0Ur5Vif6+z4A0ifVTZ2hY96B
=nEJu
-----END PGP PUBLIC KEY BLOCK-----
pub rsa4096 2021-03-12 [SC] [expires: 2037-03-08]
4AB45F1CB0D292975C6371436E2A23D806B6E6DD
uid [ unknown] Matt Butcher <technosophos@gmail.com>
sig 3 6E2A23D806B6E6DD 2021-03-12 Matt Butcher <technosophos@gmail.com>
uid [ unknown] Matt Butcher <matt.butcher@microsoft.com>
sig 3 6E2A23D806B6E6DD 2021-03-12 Matt Butcher <technosophos@gmail.com>
uid [ unknown] Matt Butcher <mabutch@microsoft.com>
sig 3 6E2A23D806B6E6DD 2021-03-12 Matt Butcher <technosophos@gmail.com>
sub rsa4096 2021-03-12 [E] [expires: 2037-03-08]
sig 6E2A23D806B6E6DD 2021-03-12 Matt Butcher <technosophos@gmail.com>
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: GPGTools - https://gpgtools.org
mQINBGBLvGsBEADHfZXD7feUfyNQoCwmDYCmygvIGKJxGkgiyxecbGieggOGVbNy
1N0F2w/HHHW7uanlCsrB/wKnSmkNxkp5m1vfcmg+AorjshBJZCjvNZAX78yOGOZk
7UQivwPhRWvJ8fnzwTd7ls7bz7mggPT0wVuBsrHtr6mfioxxmVq5ChTHKER7uFRL
23bd11x6hurfURgDuYPrCaLyrvHmQs7CCe2pxJVLFH4kXyzNoea4jZEbOPGNLXB/
war4QJaXtk9rLqEQ6fp0iM/s7N61eEcrj18HDLj9CTUB66UMTlDKUZUV+36502Ae
I6lrrFSx8KUvK9fcpdcxXKYoaY5t6BIBUS2JK8fCrTgyBdTPQ1J7z5N4GvwYonf6
FBsQpC2aY7wBAqFEbZ8xhdB/A6gY17542OSDhcto3ovdrbLkPaPKHUDz9WRDdR1U
VKAkNeqaf6h00cyEjM/IN8+Ni+Bwz1hUrwN/9qcKkhsaJK+D2z/f+Fq08+8wHm7A
rf/azwtiTT21S/Qwmg+ISkmHJiUueuL9IIIJv0tsgxZ6MsYF9tP2NxjBcmtketTE
h/oygKhFDiK8ybSRftCatEzJuf53cfe4fNIJpacUbD/QM8tGgwrXOpAz26Flm8Ki
drw6re2mvxnDKOua7dyukq+JHR5SBEzKv8WmaNEgzEDxPdaMa6+7mLcVOQARAQAB
tCVNYXR0IEJ1dGNoZXIgPHRlY2hub3NvcGhvc0BnbWFpbC5jb20+iQI4BBMBCAAs
BQJgS7xrCRBuKiPYBrbm3QIbAwUJHhM4AAIZAQQLBwkDBRUICgIDBBYAAQIAAMZ7
D/42lpQArXi7unDfG1K5dksGWv50S8dPy93APKZkxSqmO/LxMxOSUUq6N5NSh5FO
WV3o9Za0u0IfKN+cje4ldkRGaxAEmoPLRaB26lztv9AzkaBUh6c4q/MsUiuExJMN
l9P7los6B8kCtxddq3TjTXf1FVPxT3U6Orprmh9BNsIdw/N9K0teUJjEBl5ui7i9
WqVvbbTy3I34ae2tCdN98iwHVpkfm/VYuvqtKcgzv99FcasvAWLPr+z9fG5iOx54
WthG2UCXf4k75W8Ddd5TD8n/3JaVZX8UUq7EiURRD2fFtqMce4PCDYia2MZybjio
qJOvxMGOr981JMI5uN+2gVKe+A2p9s9ittvHtnHQxVWd1O+CGFQg87+js+0BB4hi
WcYGdDPh6GhpYx38In3tBHxzIfCitvKMOvovFpV1j1kYaMCENrlaO2C2DWHALCX7
unpvrSb3gNnCFzB86+PJkwOSRcWxERdGY8soZacTDoTqUrwCraR4/KgZk6JK8jKH
t3w/a9igvwmzZuUrolAiv41zywDupl/wYOA6uUmvi8GxWCGZ5sHRuLGxm+Tk2QyA
QA6seNaun7OE4gvrTtuA/2AYAy/NVqdVdjHN4oOIFPnsoRfW+ltvWsQ2fBsyG0mW
A0JT7aicKCa8aZZ6ZQtP4zbKMYxJW4n042hiYcgrdCdumLQpTWF0dCBCdXRjaGVy
IDxtYXR0LmJ1dGNoZXJAbWljcm9zb2Z0LmNvbT6JAjUEEwEIACkFAmBLvGsJEG4q
I9gGtubdAhsDBQkeEzgABAsHCQMFFQgKAgMEFgABAgAAWkAP/0KjQDI1HyFIT5GG
j0yufkcmRZrsXSy57eUpfL1RY1OGqTnB/dS4DL6OJX1GaXOlfj3lwjiDl2Y1pHAk
oncv6n5AAXWfvWxkDJzxqyo8A6FhS+fOgoXaKBPAH5/1CgilNzABNIlRmHwJ4uAw
TFP8v20Ug6gqaW9lSH2PXtZKKf+gH6lBB4YwNnzehnIteX30PWhhZ1SUib0jJCoc
6H156wo7G6INzZepg+hqI1ly/XYg/XzL7qRvIREtALOs/7qU04+x1ny4Ys6G1ZAP
hI0sxfcy+qbSqzb5+7oYg/UwrbwIhs81HaTyQLa4FOYKGPyg1GkeJpzo9EENRgoy
u1Dmd/7S/Zbszj4kakF7INMByolvbHvl3FMLAILj6DwFxakI5kd1V9XemYPSRoLA
wzeUlzYHrK5tD1Q+EdmTGBpmVghFuN0ov/jja9tInF/ZXra4GdeCdksatbkUHP5p
xb8BCGmJQtJJ0ncxdn3zwJSl+5qFtdaTmMrc9p20QYiwKuMupHL6+hkdhwncbRux
S8x0dUm4Fn5EnEcejRiLu6Xs6cmUURZyWXEkcUW2i3+cvj+1dkp/HPkStWrBceyb
VarypHX5BhBGThdWiDT/Gl6W7uycFGm8kEUF9bGgSvly1clwRskj0cc6IZnSXmNq
/+efhKkDyQC3krStcwT2/HzvtLgDtCRNYXR0IEJ1dGNoZXIgPG1hYnV0Y2hAbWlj
cm9zb2Z0LmNvbT6JAjUEEwEIACkFAmBLvGsJEG4qI9gGtubdAhsDBQkeEzgABAsH
CQMFFQgKAgMEFgABAgAA3TIP+wSoWwwicctBVV0Mu3zX+9TOC/QT3pf95la5PgIV
fu6S97h7ePphk0ORRFe4qW5f7IM0iXWTN455h1ngnZGXn5tG3JtkUY616AnmK1fJ
MHRZRCJmeD8u5SzCCZGBlL+n3Hp6gOR7q14hhgkeg4oPiFKSF75LJos4JYEeCIYN
WyUa2yjz/glnzrA/zMeRQ+acRXj/Aa1MlwiDukxpIaHzB8U0xm+V6AgWdNzP7T8P
Daxidjgkjk3GGAK741z37avP9MFYUTd/Pq6Z2uB5xFuaB2xD5gJcvVYMBJQtYmtt
AmbzEZwYsROmkfCmS9jmlUFaMbKdAl2do/0feX7Hw29fhVT23tYD2d9Zm39CFXOm
tIb4SDcteyqeIOhQkLZgKLwJiwXkaLsHPVZlQljzvkQlW4qRGvzxyCWWr4PZovQG
ZSyFcO3XJk2hswijbhM3rQOxtOL9GJ9U+khnghLfmet5otSl0Gm1yW+ub7AynXi9
JT+kMv2QZfPP+jZjIeBLC3yItI6K/+0qI53JMswKDvQ8qnmeVj++dquSSnSozXpa
npqxrjxAhZ905UrPKqzxd9lJUegfB4khUBC/IuE7HTkFnZz/I+r6IfJ031YZK/lr
eeCQm6DMvoehR+4vgo+APdvclMmmCWd4TBTFBhtOZvLX5HfMU++YZC13AeDUmzOp
edRWuQINBGBLvGsBEADtGQcj2nLThgu9QBKN7Q4TCwywd/RTyJCZm2aq6NVs2iYP
NGd49RmHdzYbiSgOaSSIYODevDB0KFK0/D3YMjEE5oBpf94MxGDOfq/tVEVOjiOR
rwW7YaKGpxoD0q9QB+CI4+w3Dhu5Yiaiun+carXPfhxaOvoYq26heLipZ/cztgRK
16bqoAn/Kl2/yY3kfN2YRBgHFaLwkKFAKD39QxbxrCTB6YuGLhGOI+BLv47WlECi
TnSM//k80jQVEjuvoXZaFQO0/A8O7vIXF2TarVKO2I2HPlCt4q09ub6rmmqn2MGj
2gwYR1lv1vQZMVevJOe+4gwGKPCicIbp+JX2CN8n9lorS/PlYkUSNZehNhEaBKUK
yl5WFY00oGtjYKwRwStN9m3JwNPAQES9EYipGi4YGdsrTa+MtsIZQdnbaMVA9wlU
sNMyoTBjaGr79Gu4cPLISy3mNy6LRivlEeE3pxcziUj3k/6dLEUFgTfgmH3dGJ2o
c1fqF7RPJ0hvzqh6pG9lx5nkUtpG+s8FC7hDDnuqVXCS+4rPe13sEFRlM6l1YAiC
hXeApBhvpqB71ydiVR/yHua1H9b49+1eVeWzfF6XPtUSSCkwH7W1ZWx+8yUBi6zz
GUgmGNJ4m0GglCDPXsP3w7WNJoPAU15LNsi5z59bjGou3OkI5czPTKF7Q73znwAR
AQABiQI1BBgBCAApBQJgS7xrCRBuKiPYBrbm3QIbDAUJHhM4AAQLBwkDBRUICgID
BBYAAQIAACVcEACY7aIw03LMedYRsWogFn6IkpdbqRVEYP5Zjglky8MFIOQv81j7
Zg99BB4V0lyvSMSlFmom4BE+Sq6EO3uuqC7WR+7GL3p92AyIF9EJIOAg9FFH8eRn
jk1jA12Zdx40V6okWpy3C/OY6D6du17G6AJ1NExfSWtbxXknFAbsv2azQpJ0ATdK
xEPun0PGlOhsg+Bu33k7tQ2P6/4dJT8c2e8QBy/kedj3mGhrb9Ymy0VdOn12P7kA
oVl9TvvQV64f9YSToQzDjHTSP8dxiEV7a8SMD4cm/7sTLF1a7LW8lD405jxqll8a
dtj4+yY/rfSN/rDVoTDBkc6habYL0G97j70o02nZYJtukkIQvSYdYARE0OUdwb+y
SZWuTxT340LDJHUwmDpFyk6L6MTaCwlFPoi4+0FDpjdOngEMjMHe92vWT1gGhk6B
uOKbA/wFozjv87y8T6bCJ+dA1/TqhUT7UJBKJozXpOpcYapI59ZmTVu5V7WwFJvK
JlWm8DSDpOI75JRRy3DTX4UmYg/nRX5pfLPsxq2JQW/QnjPLPJ/y+5Y++b92wWrP
AirPev6SluPhLJ2mswaK3THlhOZulKO/VIEJ6g50m5Vj3hdYf6sR603yK9rP+3iu
IagTQt2SGfW3Ap0RO3Yt+w29BpZ1CZ5Ml4gAYkXz0hiiMnVRhlcLIOHoFw==
=h3+3
-----END PGP PUBLIC KEY BLOCK-----

@ -1,12 +1,16 @@
BINDIR := $(CURDIR)/bin
INSTALL_PATH ?= /usr/local/bin
DIST_DIRS := find * -type d -exec
TARGETS := darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x windows/amd64
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum 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
TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x windows/amd64
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum
BINNAME ?= helm
GOPATH = $(shell go env GOPATH)
GOX = $(GOPATH)/bin/gox
GOIMPORTS = $(GOPATH)/bin/goimports
GOBIN = $(shell go env GOBIN)
ifeq ($(GOBIN),)
GOBIN = $(shell go env GOPATH)/bin
endif
GOX = $(GOBIN)/gox
GOIMPORTS = $(GOBIN)/goimports
ARCH = $(shell uname -p)
ACCEPTANCE_DIR:=../acceptance-testing
@ -14,13 +18,16 @@ ACCEPTANCE_DIR:=../acceptance-testing
ACCEPTANCE_RUN_TESTS=.
# go option
PKG := ./...
TAGS :=
TESTS := .
TESTFLAGS :=
LDFLAGS := -w -s
GOFLAGS :=
SRC := $(shell find . -type f -name '*.go' -print)
PKG := ./...
TAGS :=
TESTS := .
TESTFLAGS :=
LDFLAGS := -w -s
GOFLAGS :=
CGO_ENABLED ?= 0
# Rebuild the binary if any of these files change
SRC := $(shell find . -type f -name '*.go' -print) go.mod go.sum
# Required for globs to work correctly
SHELL = /usr/bin/env bash
@ -49,6 +56,17 @@ endif
LDFLAGS += -X helm.sh/helm/v3/internal/version.metadata=${VERSION_METADATA}
LDFLAGS += -X helm.sh/helm/v3/internal/version.gitCommit=${GIT_COMMIT}
LDFLAGS += -X helm.sh/helm/v3/internal/version.gitTreeState=${GIT_DIRTY}
LDFLAGS += $(EXT_LDFLAGS)
# Define constants based on the client-go version
K8S_MODULES_VER=$(subst ., ,$(subst v,,$(shell go list -f '{{.Version}}' -m k8s.io/client-go)))
K8S_MODULES_MAJOR_VER=$(shell echo $$(($(firstword $(K8S_MODULES_VER)) + 1)))
K8S_MODULES_MINOR_VER=$(word 2,$(K8S_MODULES_VER))
LDFLAGS += -X helm.sh/helm/v3/pkg/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v3/pkg/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v3/pkg/chartutil.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v3/pkg/chartutil.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
.PHONY: all
all: build
@ -60,7 +78,14 @@ all: build
build: $(BINDIR)/$(BINNAME)
$(BINDIR)/$(BINNAME): $(SRC)
GO111MODULE=on go build $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/helm
GO111MODULE=on CGO_ENABLED=$(CGO_ENABLED) go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/helm
# ------------------------------------------------------------------------------
# install
.PHONY: install
install: build
@install "$(BINDIR)/$(BINNAME)" "$(INSTALL_PATH)/$(BINNAME)"
# ------------------------------------------------------------------------------
# test
@ -115,18 +140,25 @@ coverage:
format: $(GOIMPORTS)
GO111MODULE=on go list -f '{{.Dir}}' ./... | xargs $(GOIMPORTS) -w -local helm.sh/helm
# Generate golden files used in unit tests
.PHONY: gen-test-golden
gen-test-golden:
gen-test-golden: PKG = ./cmd/helm ./pkg/action
gen-test-golden: TESTFLAGS = -update
gen-test-golden: test-unit
# ------------------------------------------------------------------------------
# dependencies
# If go get is run from inside the project directory it will add the dependencies
# to the go.mod file. To avoid that we change to a directory without a go.mod file
# when downloading the following dependencies
# If go install is run from inside the project directory it will add the
# dependencies to the go.mod file. To avoid that we change to a directory
# without a go.mod file when downloading the following dependencies
$(GOX):
(cd /; GO111MODULE=on go get -u github.com/mitchellh/gox)
(cd /; GO111MODULE=on go install github.com/mitchellh/gox@latest)
$(GOIMPORTS):
(cd /; GO111MODULE=on go get -u golang.org/x/tools/cmd/goimports)
(cd /; GO111MODULE=on go install golang.org/x/tools/cmd/goimports@latest)
# ------------------------------------------------------------------------------
# release
@ -134,7 +166,7 @@ $(GOIMPORTS):
.PHONY: build-cross
build-cross: LDFLAGS += -extldflags "-static"
build-cross: $(GOX)
GO111MODULE=on CGO_ENABLED=0 $(GOX) -parallel=3 -output="_dist/{{.OS}}-{{.Arch}}/$(BINNAME)" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' ./cmd/helm
GOFLAGS="-trimpath" GO111MODULE=on CGO_ENABLED=0 $(GOX) -parallel=3 -output="_dist/{{.OS}}-{{.Arch}}/$(BINNAME)" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' ./cmd/helm
.PHONY: dist
dist:
@ -156,7 +188,7 @@ fetch-dist:
.PHONY: sign
sign:
for f in _dist/*.{gz,zip,sha256,sha256sum} ; do \
for f in $$(ls _dist/*.{gz,zip,sha256,sha256sum} 2>/dev/null) ; do \
gpg --armor --detach-sign $${f} ; \
done
@ -169,7 +201,7 @@ sign:
# removed in Helm v4.
.PHONY: checksum
checksum:
for f in _dist/*.{gz,zip} ; do \
for f in $$(ls _dist/*.{gz,zip} 2>/dev/null) ; do \
shasum -a 256 "$${f}" | sed 's/_dist\///' > "$${f}.sha256sum" ; \
shasum -a 256 "$${f}" | awk '{print $$1}' > "$${f}.sha256" ; \
done

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

@ -1,6 +1,6 @@
# Helm
[![CircleCI](https://circleci.com/gh/helm/helm.svg?style=shield)](https://circleci.com/gh/helm/helm)
[![Build Status](https://github.com/helm/helm/workflows/release/badge.svg)](https://github.com/helm/helm/actions?workflow=release)
[![Go Report Card](https://goreportcard.com/badge/github.com/helm/helm)](https://goreportcard.com/report/github.com/helm/helm)
[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/helm.sh/helm/v3)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3131/badge)](https://bestpractices.coreinfrastructure.org/projects/3131)
@ -9,7 +9,7 @@ Helm is a tool for managing Charts. Charts are packages of pre-configured Kubern
Use Helm to:
- Find and use [popular software packaged as Helm Charts](https://hub.helm.sh) to run in Kubernetes
- Find and use [popular software packaged as Helm Charts](https://artifacthub.io/packages/search?kind=0) to run in Kubernetes
- Share your own applications as Helm Charts
- Create reproducible builds of your Kubernetes applications
- Intelligently manage your Kubernetes manifest files
@ -30,7 +30,6 @@ Think of it like apt/yum/homebrew for Kubernetes.
## Install
Binary downloads of the Helm client can be found on [the Releases page](https://github.com/helm/helm/releases/latest).
Unpack the `helm` binary and add it to your PATH and you are good to go!
@ -54,7 +53,7 @@ Get started with the [Quick Start guide](https://helm.sh/docs/intro/quickstart/)
## Roadmap
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.
## Community, discussion, contribution, and support
@ -66,7 +65,11 @@ You can reach the Helm community and developers via the following channels:
- [#charts](https://kubernetes.slack.com/messages/charts)
- Mailing List:
- [Helm Mailing List](https://lists.cncf.io/g/cncf-helm)
- Developer Call: Thursdays at 9:30-10:00 Pacific. [https://zoom.us/j/696660622](https://zoom.us/j/696660622)
- 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

@ -1,49 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"io"
"github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/action"
)
const chartHelp = `
This command consists of multiple subcommands to work with the chart cache.
The subcommands can be used to push, pull, tag, list, or remove Helm charts.
`
func newChartCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "chart",
Short: "push, pull, tag, or remove Helm charts",
Long: chartHelp,
Hidden: !FeatureGateOCI.IsEnabled(),
PersistentPreRunE: checkOCIFeatureGate(),
}
cmd.AddCommand(
newChartListCmd(cfg, out),
newChartExportCmd(cfg, out),
newChartPullCmd(cfg, out),
newChartPushCmd(cfg, out),
newChartRemoveCmd(cfg, out),
newChartSaveCmd(cfg, out),
)
return cmd
}

@ -1,55 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"io"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/action"
)
const chartExportDesc = `
Export a chart stored in local registry cache.
This will create a new directory with the name of
the chart, in a format that developers can modify
and check into source control if desired.
`
func newChartExportCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewChartExport(cfg)
cmd := &cobra.Command{
Use: "export [ref]",
Short: "export a chart to directory",
Long: chartExportDesc,
Args: require.MinimumNArgs(1),
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
ref := args[0]
return client.Run(out, ref)
},
}
f := cmd.Flags()
f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.")
return cmd
}

@ -1,44 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"io"
"github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/action"
)
const chartListDesc = `
List all charts in the local registry cache.
Charts are sorted by ref name, alphabetically.
`
func newChartListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "list all saved charts",
Long: chartListDesc,
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
return action.NewChartList(cfg).Run(out)
},
}
}

@ -1,46 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"io"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/action"
)
const chartPullDesc = `
Download a chart from a remote registry.
This will store the chart in the local registry cache to be used later.
`
func newChartPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return &cobra.Command{
Use: "pull [ref]",
Short: "pull a chart from remote",
Long: chartPullDesc,
Args: require.MinimumNArgs(1),
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
ref := args[0]
return action.NewChartPull(cfg).Run(out, ref)
},
}
}

@ -1,48 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"io"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/action"
)
const chartPushDesc = `
Upload a chart to a remote registry.
Note: the ref must already exist in the local registry cache.
Must first run "helm chart save" or "helm chart pull".
`
func newChartPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return &cobra.Command{
Use: "push [ref]",
Short: "push a chart to remote",
Long: chartPushDesc,
Args: require.MinimumNArgs(1),
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
ref := args[0]
return action.NewChartPush(cfg).Run(out, ref)
},
}
}

@ -1,50 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"io"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/action"
)
const chartRemoveDesc = `
Remove a chart from the local registry cache.
Note: the chart content will still exist in the cache,
but it will no longer appear in "helm chart list".
To remove all unlinked content, please run "helm chart prune". (TODO)
`
func newChartRemoveCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return &cobra.Command{
Use: "remove [ref]",
Aliases: []string{"rm"},
Short: "remove a chart",
Long: chartRemoveDesc,
Args: require.MinimumNArgs(1),
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
ref := args[0]
return action.NewChartRemove(cfg).Run(out, ref)
},
}
}

@ -1,61 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"io"
"path/filepath"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart/loader"
)
const chartSaveDesc = `
Store a copy of chart in local registry cache.
Note: modifying the chart after this operation will
not change the item as it exists in the cache.
`
func newChartSaveCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return &cobra.Command{
Use: "save [path] [ref]",
Short: "save a chart directory",
Long: chartSaveDesc,
Args: require.MinimumNArgs(2),
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
path := args[0]
ref := args[1]
path, err := filepath.Abs(path)
if err != nil {
return err
}
ch, err := loader.Load(path)
if err != nil {
return err
}
return action.NewChartSave(cfg).Run(out, ch, ref)
},
}
}

@ -27,43 +27,61 @@ import (
)
const completionDesc = `
Generate autocompletions script for Helm for the specified shell.
Generate autocompletion scripts for Helm for the specified shell.
`
const bashCompDesc = `
Generate the autocompletion script for Helm for the bash shell.
To load completions in your current shell session:
$ source <(helm completion bash)
source <(helm completion bash)
To load completions for every new session, execute once:
Linux:
$ helm completion bash > /etc/bash_completion.d/helm
MacOS:
$ helm completion bash > /usr/local/etc/bash_completion.d/helm
- Linux:
helm completion bash > /etc/bash_completion.d/helm
- MacOS:
helm completion bash > /usr/local/etc/bash_completion.d/helm
`
const zshCompDesc = `
Generate the autocompletion script for Helm for the zsh shell.
To load completions in your current shell session:
$ source <(helm completion zsh)
source <(helm completion zsh)
To load completions for every new session, execute once:
$ helm completion zsh > "${fpath[1]}/_helm"
helm completion zsh > "${fpath[1]}/_helm"
`
const fishCompDesc = `
Generate the autocompletion script for Helm for the fish shell.
To load completions in your current shell session:
$ helm completion fish | source
helm completion fish | source
To load completions for every new session, execute once:
$ helm completion fish > ~/.config/fish/completions/helm.fish
helm completion fish > ~/.config/fish/completions/helm.fish
You will need to start a new shell for this setup to take effect.
`
const powershellCompDesc = `
Generate the autocompletion script for powershell.
To load completions in your current shell session:
PS C:\> helm completion powershell | Out-String | Invoke-Expression
To load completions for every new session, add the output of the above command
to your powershell profile.
`
const (
noDescFlagName = "no-descriptions"
noDescFlagText = "disable completion descriptions"
@ -73,57 +91,67 @@ var disableCompDescriptions bool
func newCompletionCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "completion",
Short: "generate autocompletions script for the specified shell",
Long: completionDesc,
Args: require.NoArgs,
ValidArgsFunction: noCompletions, // Disable file completion
Use: "completion",
Short: "generate autocompletion scripts for the specified shell",
Long: completionDesc,
Args: require.NoArgs,
}
bash := &cobra.Command{
Use: "bash",
Short: "generate autocompletions script for bash",
Long: bashCompDesc,
Args: require.NoArgs,
DisableFlagsInUseLine: true,
ValidArgsFunction: noCompletions,
Use: "bash",
Short: "generate autocompletion script for bash",
Long: bashCompDesc,
Args: require.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionBash(out, cmd)
},
}
bash.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
zsh := &cobra.Command{
Use: "zsh",
Short: "generate autocompletions script for zsh",
Long: zshCompDesc,
Args: require.NoArgs,
DisableFlagsInUseLine: true,
ValidArgsFunction: noCompletions,
Use: "zsh",
Short: "generate autocompletion script for zsh",
Long: zshCompDesc,
Args: require.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionZsh(out, cmd)
},
}
zsh.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
fish := &cobra.Command{
Use: "fish",
Short: "generate autocompletions script for fish",
Long: fishCompDesc,
Args: require.NoArgs,
DisableFlagsInUseLine: true,
ValidArgsFunction: noCompletions,
Use: "fish",
Short: "generate autocompletion script for fish",
Long: fishCompDesc,
Args: require.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionFish(out, cmd)
},
}
fish.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
cmd.AddCommand(bash, zsh, fish)
powershell := &cobra.Command{
Use: "powershell",
Short: "generate autocompletion script for powershell",
Long: powershellCompDesc,
Args: require.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionPowershell(out, cmd)
},
}
powershell.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
cmd.AddCommand(bash, zsh, fish, powershell)
return cmd
}
func runCompletionBash(out io.Writer, cmd *cobra.Command) error {
err := cmd.Root().GenBashCompletion(out)
err := cmd.Root().GenBashCompletionV2(out, !disableCompDescriptions)
// In case the user renamed the helm binary (e.g., to be able to run
// both helm2 and helm3), we hook the new binary name to the completion function
@ -145,154 +173,42 @@ fi
}
func runCompletionZsh(out io.Writer, cmd *cobra.Command) error {
zshInitialization := `#compdef helm
__helm_bash_source() {
alias shopt=':'
alias _expand=_bash_expand
alias _complete=_bash_comp
emulate -L sh
setopt kshglob noshglob braceexpand
source "$@"
}
__helm_type() {
# -t is not supported by zsh
if [ "$1" == "-t" ]; then
shift
# fake Bash 4 to disable "complete -o nospace". Instead
# "compopt +-o nospace" is used in the code to toggle trailing
# spaces. We don't support that, but leave trailing spaces on
# all the time
if [ "$1" = "__helm_compopt" ]; then
echo builtin
return 0
fi
fi
type "$@"
}
__helm_compgen() {
local completions w
completions=( $(compgen "$@") ) || return $?
# filter by given word as prefix
while [[ "$1" = -* && "$1" != -- ]]; do
shift
shift
done
if [[ "$1" == -- ]]; then
shift
fi
for w in "${completions[@]}"; do
if [[ "${w}" = "$1"* ]]; then
# Use printf instead of echo because it is possible that
# the value to print is -n, which would be interpreted
# as a flag to echo
printf "%s\n" "${w}"
fi
done
}
__helm_compopt() {
true # don't do anything. Not supported by bashcompinit in zsh
}
__helm_ltrim_colon_completions()
{
if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
# Remove colon-word prefix from COMPREPLY items
local colon_word=${1%${1##*:}}
local i=${#COMPREPLY[*]}
while [[ $((--i)) -ge 0 ]]; do
COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
done
fi
}
__helm_get_comp_words_by_ref() {
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[${COMP_CWORD}-1]}"
words=("${COMP_WORDS[@]}")
cword=("${COMP_CWORD[@]}")
}
__helm_filedir() {
local RET OLD_IFS w qw
__debug "_filedir $@ cur=$cur"
if [[ "$1" = \~* ]]; then
# somehow does not work. Maybe, zsh does not call this at all
eval echo "$1"
return 0
fi
OLD_IFS="$IFS"
IFS=$'\n'
if [ "$1" = "-d" ]; then
shift
RET=( $(compgen -d) )
else
RET=( $(compgen -f) )
fi
IFS="$OLD_IFS"
IFS="," __debug "RET=${RET[@]} len=${#RET[@]}"
for w in ${RET[@]}; do
if [[ ! "${w}" = "${cur}"* ]]; then
continue
fi
if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then
qw="$(__helm_quote "${w}")"
if [ -d "${w}" ]; then
COMPREPLY+=("${qw}/")
else
COMPREPLY+=("${qw}")
fi
fi
done
}
__helm_quote() {
if [[ $1 == \'* || $1 == \"* ]]; then
# Leave out first character
printf %q "${1:1}"
else
printf %q "$1"
fi
}
autoload -U +X bashcompinit && bashcompinit
# use word boundary patterns for BSD or GNU sed
LWORD='[[:<:]]'
RWORD='[[:>:]]'
if sed --help 2>&1 | grep -q 'GNU\|BusyBox'; then
LWORD='\<'
RWORD='\>'
fi
__helm_convert_bash_to_zsh() {
sed \
-e 's/declare -F/whence -w/' \
-e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \
-e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \
-e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \
-e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \
-e "s/${LWORD}_filedir${RWORD}/__helm_filedir/g" \
-e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__helm_get_comp_words_by_ref/g" \
-e "s/${LWORD}__ltrim_colon_completions${RWORD}/__helm_ltrim_colon_completions/g" \
-e "s/${LWORD}compgen${RWORD}/__helm_compgen/g" \
-e "s/${LWORD}compopt${RWORD}/__helm_compopt/g" \
-e "s/${LWORD}declare${RWORD}/builtin declare/g" \
-e "s/\\\$(type${RWORD}/\$(__helm_type/g" \
-e 's/aliashash\["\(.\{1,\}\)"\]/aliashash[\1]/g' \
-e 's/FUNCNAME/funcstack/g' \
<<'BASH_COMPLETION_EOF'
var err error
if disableCompDescriptions {
err = cmd.Root().GenZshCompletionNoDesc(out)
} else {
err = cmd.Root().GenZshCompletion(out)
}
// In case the user renamed the helm binary (e.g., to be able to run
// both helm2 and helm3), we hook the new binary name to the completion function
if binary := filepath.Base(os.Args[0]); binary != "helm" {
renamedBinaryHook := `
# Hook the command used to generate the completion script
# to the helm completion function to handle the case where
# the user renamed the helm binary
compdef _helm %[1]s
`
out.Write([]byte(zshInitialization))
fmt.Fprintf(out, renamedBinaryHook, binary)
}
runCompletionBash(out, cmd)
// Cobra doesn't source zsh completion file, explicitly doing it here
fmt.Fprintf(out, "compdef _helm helm")
zshTail := `
BASH_COMPLETION_EOF
}
__helm_bash_source <(__helm_convert_bash_to_zsh)
`
out.Write([]byte(zshTail))
return nil
return err
}
func runCompletionFish(out io.Writer, cmd *cobra.Command) error {
return cmd.Root().GenFishCompletion(out, !disableCompDescriptions)
}
func runCompletionPowershell(out io.Writer, cmd *cobra.Command) error {
if disableCompDescriptions {
return cmd.Root().GenPowerShellCompletion(out)
}
return cmd.Root().GenPowerShellCompletionWithDesc(out)
}
// Function to disable file completion
func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp

@ -29,9 +29,14 @@ import (
func checkFileCompletion(t *testing.T, cmdName string, shouldBePerformed bool) {
storage := storageFixture()
storage.Create(&release.Release{
Name: "myrelease",
Info: &release.Info{Status: release.StatusDeployed},
Chart: &chart.Chart{},
Name: "myrelease",
Info: &release.Info{Status: release.StatusDeployed},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "Myrelease-Chart",
Version: "1.2.3",
},
},
Version: 1,
})
@ -42,10 +47,10 @@ func checkFileCompletion(t *testing.T, cmdName string, shouldBePerformed bool) {
}
if !strings.Contains(out, "ShellCompDirectiveNoFileComp") != shouldBePerformed {
if shouldBePerformed {
t.Error(fmt.Sprintf("Unexpected directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName))
t.Errorf("Unexpected directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName)
} else {
t.Error(fmt.Sprintf("Did not receive directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName))
t.Errorf("Did not receive directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName)
}
t.Log(out)
}
@ -57,3 +62,32 @@ func TestCompletionFileCompletion(t *testing.T) {
checkFileCompletion(t, "completion zsh", false)
checkFileCompletion(t, "completion fish", false)
}
func checkReleaseCompletion(t *testing.T, cmdName string, multiReleasesAllowed bool) {
multiReleaseTestGolden := "output/empty_nofile_comp.txt"
if multiReleasesAllowed {
multiReleaseTestGolden = "output/release_list_repeat_comp.txt"
}
tests := []cmdTestCase{{
name: "completion for uninstall",
cmd: fmt.Sprintf("__complete %s ''", cmdName),
golden: "output/release_list_comp.txt",
rels: []*release.Release{
release.Mock(&release.MockReleaseOptions{Name: "athos"}),
release.Mock(&release.MockReleaseOptions{Name: "porthos"}),
release.Mock(&release.MockReleaseOptions{Name: "aramis"}),
},
}, {
name: "completion for uninstall repetition",
cmd: fmt.Sprintf("__complete %s porthos ''", cmdName),
golden: multiReleaseTestGolden,
rels: []*release.Release{
release.Mock(&release.MockReleaseOptions{Name: "athos"}),
release.Mock(&release.MockReleaseOptions{Name: "porthos"}),
release.Mock(&release.MockReleaseOptions{Name: "aramis"}),
},
}}
for _, test := range tests {
runTestCmd(t, []cmdTestCase{test})
}
}

@ -107,6 +107,7 @@ func (o *createOptions) run(out io.Writer) error {
return chartutil.CreateFrom(cfile, filepath.Dir(o.name), lstarter)
}
chartutil.Stderr = out
_, err := chartutil.Create(chartname, filepath.Dir(o.name))
return err
}

@ -18,7 +18,6 @@ package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -77,7 +76,7 @@ func TestCreateStarterCmd(t *testing.T) {
t.Logf("Created %s", dest)
}
tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl")
if err := ioutil.WriteFile(tplpath, []byte("test"), 0644); err != nil {
if err := os.WriteFile(tplpath, []byte("test"), 0644); err != nil {
t.Fatalf("Could not write template: %s", err)
}
@ -140,7 +139,7 @@ func TestCreateStarterAbsoluteCmd(t *testing.T) {
t.Logf("Created %s", dest)
}
tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl")
if err := ioutil.WriteFile(tplpath, []byte("test"), 0644); err != nil {
if err := os.WriteFile(tplpath, []byte("test"), 0644); err != nil {
t.Fatalf("Could not write template: %s", err)
}

@ -82,26 +82,24 @@ the contents of a chart.
This will produce an error if the chart cannot be loaded.
`
func newDependencyCmd(out io.Writer) *cobra.Command {
func newDependencyCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "dependency update|build|list",
Aliases: []string{"dep", "dependencies"},
Short: "manage a chart's dependencies",
Long: dependencyDesc,
Args: require.NoArgs,
ValidArgsFunction: noCompletions, // Disable file completion
Use: "dependency update|build|list",
Aliases: []string{"dep", "dependencies"},
Short: "manage a chart's dependencies",
Long: dependencyDesc,
Args: require.NoArgs,
}
cmd.AddCommand(newDependencyListCmd(out))
cmd.AddCommand(newDependencyUpdateCmd(out))
cmd.AddCommand(newDependencyBuildCmd(out))
cmd.AddCommand(newDependencyUpdateCmd(cfg, out))
cmd.AddCommand(newDependencyBuildCmd(cfg, out))
return cmd
}
func newDependencyListCmd(out io.Writer) *cobra.Command {
client := action.NewDependency()
cmd := &cobra.Command{
Use: "list CHART",
Aliases: []string{"ls"},
@ -116,5 +114,9 @@ func newDependencyListCmd(out io.Writer) *cobra.Command {
return client.List(chartpath, out)
},
}
f := cmd.Flags()
f.UintVar(&client.ColumnWidth, "max-col-width", 80, "maximum column width for output table")
return cmd
}

@ -41,7 +41,7 @@ If no lock file is found, 'helm dependency build' will mirror the behavior
of 'helm dependency update'.
`
func newDependencyBuildCmd(out io.Writer) *cobra.Command {
func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewDependency()
cmd := &cobra.Command{
@ -58,7 +58,9 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command {
Out: out,
ChartPath: chartpath,
Keyring: client.Keyring,
SkipUpdate: client.SkipRefresh,
Getters: getter.All(settings),
RegistryClient: cfg.RegistryClient,
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug,
@ -77,6 +79,7 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command {
f := cmd.Flags()
f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures")
f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys")
f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache")
return cmd
}

@ -22,13 +22,14 @@ import (
"strings"
"testing"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/provenance"
"helm.sh/helm/v3/pkg/repo"
"helm.sh/helm/v3/pkg/repo/repotest"
)
func TestDependencyBuildCmd(t *testing.T) {
srv, err := repotest.NewTempServer("testdata/testcharts/*.tgz")
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz")
defer srv.Stop()
if err != nil {
t.Fatal(err)
@ -37,6 +38,22 @@ func TestDependencyBuildCmd(t *testing.T) {
rootDir := srv.Root()
srv.LinkIndices()
ociSrv, err := repotest.NewOCIServer(t, srv.Root())
if err != nil {
t.Fatal(err)
}
ociChartName := "oci-depending-chart"
c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL)
if _, err := chartutil.Save(c, ociSrv.Dir); err != nil {
t.Fatal(err)
}
ociSrv.Run(t, repotest.WithDependingChart(c))
dir := func(p ...string) string {
return filepath.Join(append([]string{srv.Root()}, p...)...)
}
chartname := "depbuild"
createTestingChart(t, rootDir, chartname, srv.URL())
repoFile := filepath.Join(rootDir, "repositories.yaml")
@ -99,6 +116,38 @@ func TestDependencyBuildCmd(t *testing.T) {
if v := reqver.Version; v != "0.1.0" {
t.Errorf("mismatched versions. Expected %q, got %q", "0.1.0", v)
}
skipRefreshCmd := fmt.Sprintf("dependency build '%s' --skip-refresh --repository-config %s --repository-cache %s", filepath.Join(rootDir, chartname), repoFile, rootDir)
_, out, err = executeActionCommand(skipRefreshCmd)
// In this pass, we check --skip-refresh option becomes effective.
if err != nil {
t.Logf("Output: %s", out)
t.Fatal(err)
}
if strings.Contains(out, `update from the "test" chart repository`) {
t.Errorf("Repo did get updated\n%s", out)
}
// OCI dependencies
if err := chartutil.SaveDir(c, dir()); err != nil {
t.Fatal(err)
}
cmd = fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json",
dir(ociChartName),
dir("repositories.yaml"),
dir(),
dir())
_, out, err = executeActionCommand(cmd)
if err != nil {
t.Logf("Output: %s", out)
t.Fatal(err)
}
expect = dir(ociChartName, "charts/oci-dependent-chart-0.1.0.tgz")
if _, err := os.Stat(expect); err != nil {
t.Fatal(err)
}
}
func TestDependencyBuildCmdWithHelmV2Hash(t *testing.T) {

@ -43,7 +43,7 @@ in the Chart.yaml file, but (b) at the wrong version.
`
// newDependencyUpdateCmd creates a new dependency update command.
func newDependencyUpdateCmd(out io.Writer) *cobra.Command {
func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewDependency()
cmd := &cobra.Command{
@ -63,6 +63,7 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command {
Keyring: client.Keyring,
SkipUpdate: client.SkipRefresh,
Getters: getter.All(settings),
RegistryClient: cfg.RegistryClient,
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug,

@ -17,7 +17,6 @@ package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -33,13 +32,25 @@ import (
)
func TestDependencyUpdateCmd(t *testing.T) {
srv, err := repotest.NewTempServer("testdata/testcharts/*.tgz")
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz")
if err != nil {
t.Fatal(err)
}
defer srv.Stop()
t.Logf("Listening on directory %s", srv.Root())
ociSrv, err := repotest.NewOCIServer(t, srv.Root())
if err != nil {
t.Fatal(err)
}
ociChartName := "oci-depending-chart"
c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL)
if _, err := chartutil.Save(c, ociSrv.Dir); err != nil {
t.Fatal(err)
}
ociSrv.Run(t, repotest.WithDependingChart(c))
if err := srv.LinkIndices(); err != nil {
t.Fatal(err)
}
@ -115,13 +126,32 @@ func TestDependencyUpdateCmd(t *testing.T) {
if _, err := os.Stat(unexpected); err == nil {
t.Fatalf("Unexpected %q", unexpected)
}
// test for OCI charts
if err := chartutil.SaveDir(c, dir()); err != nil {
t.Fatal(err)
}
cmd := fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json",
dir(ociChartName),
dir("repositories.yaml"),
dir(),
dir())
_, out, err = executeActionCommand(cmd)
if err != nil {
t.Logf("Output: %s", out)
t.Fatal(err)
}
expect = dir(ociChartName, "charts/oci-dependent-chart-0.1.0.tgz")
if _, err := os.Stat(expect); err != nil {
t.Fatal(err)
}
}
func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) {
func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) {
defer resetEnv()()
defer ensure.HelmHome(t)()
srv, err := repotest.NewTempServer("testdata/testcharts/*.tgz")
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz")
if err != nil {
t.Fatal(err)
}
@ -155,7 +185,7 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) {
}
// Make sure charts dir still has dependencies
files, err := ioutil.ReadDir(filepath.Join(dir(chartname), "charts"))
files, err := os.ReadDir(filepath.Join(dir(chartname), "charts"))
if err != nil {
t.Fatal(err)
}
@ -193,6 +223,19 @@ func createTestingMetadata(name, baseURL string) *chart.Chart {
}
}
func createTestingMetadataForOCI(name, registryURL string) *chart.Chart {
return &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV2,
Name: name,
Version: "1.2.3",
Dependencies: []*chart.Dependency{
{Name: "oci-dependent-chart", Version: "0.1.0", Repository: fmt.Sprintf("oci://%s/u/ocitestuser", registryURL)},
},
},
}
}
// createTestingChart creates a basic chart that depends on reqtest-0.1.0
//
// The baseURL can be used to point to a particular repository server.

@ -16,12 +16,17 @@ limitations under the License.
package main
import (
"fmt"
"io"
"path"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"helm.sh/helm/v3/cmd/helm/require"
)
@ -38,9 +43,10 @@ It can also generate bash autocompletions.
`
type docsOptions struct {
dest string
docTypeString string
topCmd *cobra.Command
dest string
docTypeString string
topCmd *cobra.Command
generateHeaders bool
}
func newDocsCmd(out io.Writer) *cobra.Command {
@ -62,6 +68,11 @@ func newDocsCmd(out io.Writer) *cobra.Command {
f := cmd.Flags()
f.StringVar(&o.dest, "dir", "./", "directory to which documentation is written")
f.StringVar(&o.docTypeString, "type", "markdown", "the type of documentation to generate (markdown, man, bash)")
f.BoolVar(&o.generateHeaders, "generate-headers", false, "generate standard headers for markdown files")
cmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"bash", "man", "markdown"}, cobra.ShellCompDirectiveNoFileComp
})
return cmd
}
@ -69,6 +80,18 @@ func newDocsCmd(out io.Writer) *cobra.Command {
func (o *docsOptions) run(out io.Writer) error {
switch o.docTypeString {
case "markdown", "mdown", "md":
if o.generateHeaders {
standardLinks := func(s string) string { return s }
hdrFunc := func(filename string) string {
base := filepath.Base(filename)
name := strings.TrimSuffix(base, path.Ext(base))
title := cases.Title(language.Und, cases.NoLower).String(strings.Replace(name, "_", " ", -1))
return fmt.Sprintf("---\ntitle: \"%s\"\n---\n\n", title)
}
return doc.GenMarkdownTreeCustom(o.topCmd, o.dest, hdrFunc, standardLinks)
}
return doc.GenMarkdownTree(o.topCmd, o.dest)
case "man":
manHdr := &doc.GenManHeader{Title: "HELM", Section: "1"}

@ -20,6 +20,19 @@ import (
"testing"
)
func TestDocsTypeFlagCompletion(t *testing.T) {
tests := []cmdTestCase{{
name: "completion for docs --type",
cmd: "__complete docs --type ''",
golden: "output/docs-type-comp.txt",
}, {
name: "completion for docs --type, no filter",
cmd: "__complete docs --type mar",
golden: "output/docs-type-comp.txt",
}}
runTestCmd(t, tests)
}
func TestDocsFileCompletion(t *testing.T) {
checkFileCompletion(t, "docs", false)
}

@ -17,13 +17,16 @@ limitations under the License.
package main
import (
"flag"
"fmt"
"log"
"path/filepath"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"k8s.io/klog/v2"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
@ -33,18 +36,23 @@ import (
"helm.sh/helm/v3/pkg/repo"
)
const outputFlag = "output"
const postRenderFlag = "post-renderer"
const (
outputFlag = "output"
postRenderFlag = "post-renderer"
postRenderArgsFlag = "post-renderer-args"
)
func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) {
f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)")
f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)")
f.StringArrayVar(&v.JSONValues, "set-json", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)")
f.StringArrayVar(&v.LiteralValues, "set-literal", []string{}, "set a literal STRING value on the command line")
}
func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
f.StringVar(&c.Version, "version", "", "specify the exact chart version to use. If this is not specified, the latest version is used")
f.StringVar(&c.Version, "version", "", "specify a version constraint for the chart version to use. This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used")
f.BoolVar(&c.Verify, "verify", false, "verify the package before using it")
f.StringVar(&c.Keyring, "keyring", defaultKeyring(), "location of public keys used for verification")
f.StringVar(&c.RepoURL, "repo", "", "chart repository url where to locate the requested chart")
@ -53,7 +61,9 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
f.StringVar(&c.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.BoolVar(&c.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&c.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download")
f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains")
}
// bindOutputFlag will add the output flag to the given command and bind the
@ -64,12 +74,13 @@ func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) {
err := cmd.RegisterFlagCompletionFunc(outputFlag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var formatNames []string
for _, format := range output.Formats() {
if strings.HasPrefix(format, toComplete) {
formatNames = append(formatNames, format)
}
for format, desc := range output.FormatsWithDesc() {
formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc))
}
return formatNames, cobra.ShellCompDirectiveDefault
// Sort the results to get a deterministic order for the tests
sort.Strings(formatNames)
return formatNames, cobra.ShellCompDirectiveNoFileComp
})
if err != nil {
@ -105,33 +116,85 @@ func (o *outputValue) Set(s string) error {
}
func bindPostRenderFlag(cmd *cobra.Command, varRef *postrender.PostRenderer) {
cmd.Flags().Var(&postRenderer{varRef}, postRenderFlag, "the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path")
p := &postRendererOptions{varRef, "", []string{}}
cmd.Flags().Var(&postRendererString{p}, postRenderFlag, "the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path")
cmd.Flags().Var(&postRendererArgsSlice{p}, postRenderArgsFlag, "an argument to the post-renderer (can specify multiple)")
}
type postRendererOptions struct {
renderer *postrender.PostRenderer
binaryPath string
args []string
}
type postRenderer struct {
renderer *postrender.PostRenderer
type postRendererString struct {
options *postRendererOptions
}
func (p postRenderer) String() string {
return "exec"
func (p *postRendererString) String() string {
return p.options.binaryPath
}
func (p postRenderer) Type() string {
return "postrenderer"
func (p *postRendererString) Type() string {
return "postRendererString"
}
func (p postRenderer) Set(s string) error {
if s == "" {
func (p *postRendererString) Set(val string) error {
if val == "" {
return nil
}
pr, err := postrender.NewExec(s)
p.options.binaryPath = val
pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...)
if err != nil {
return err
}
*p.renderer = pr
*p.options.renderer = pr
return nil
}
type postRendererArgsSlice struct {
options *postRendererOptions
}
func (p *postRendererArgsSlice) String() string {
return "[" + strings.Join(p.options.args, ",") + "]"
}
func (p *postRendererArgsSlice) Type() string {
return "postRendererArgsSlice"
}
func (p *postRendererArgsSlice) Set(val string) error {
// a post-renderer defined by a user may accept empty arguments
p.options.args = append(p.options.args, val)
if p.options.binaryPath == "" {
return nil
}
// overwrite if already create PostRenderer by `post-renderer` flags
pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...)
if err != nil {
return err
}
*p.options.renderer = pr
return nil
}
func (p *postRendererArgsSlice) Append(val string) error {
p.options.args = append(p.options.args, val)
return nil
}
func (p *postRendererArgsSlice) Replace(val []string) error {
p.options.args = val
return nil
}
func (p *postRendererArgsSlice) GetSlice() []string {
return p.options.args
}
func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellCompDirective) {
chartInfo := strings.Split(chartRef, "/")
if len(chartInfo) != 2 {
@ -146,12 +209,44 @@ func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellC
var versions []string
if indexFile, err := repo.LoadIndexFile(path); err == nil {
for _, details := range indexFile.Entries[chartName] {
version := details.Metadata.Version
if strings.HasPrefix(version, toComplete) {
versions = append(versions, version)
appVersion := details.Metadata.AppVersion
appVersionDesc := ""
if appVersion != "" {
appVersionDesc = fmt.Sprintf("App: %s, ", appVersion)
}
created := details.Created.Format("January 2, 2006")
createdDesc := ""
if created != "" {
createdDesc = fmt.Sprintf("Created: %s ", created)
}
deprecated := ""
if details.Metadata.Deprecated {
deprecated = "(deprecated)"
}
versions = append(versions, fmt.Sprintf("%s\t%s%s%s", details.Metadata.Version, appVersionDesc, createdDesc, deprecated))
}
}
return versions, cobra.ShellCompDirectiveNoFileComp
}
// addKlogFlags adds flags from k8s.io/klog
// marks the flags as hidden to avoid polluting the help text
func addKlogFlags(fs *pflag.FlagSet) {
local := flag.NewFlagSet("klog", flag.ExitOnError)
klog.InitFlags(local)
local.VisitAll(func(fl *flag.Flag) {
fl.Name = normalize(fl.Name)
if fs.Lookup(fl.Name) != nil {
return
}
newflag := pflag.PFlagFromGoFlag(fl)
newflag.Hidden = true
fs.AddFlag(newflag)
})
}
// normalize replaces underscores with hyphens
func normalize(s string) string {
return strings.ReplaceAll(s, "_", "-")
}

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

@ -37,11 +37,10 @@ get extended information about the release, including:
func newGetCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "get",
Short: "download extended information of a named release",
Long: getHelp,
Args: require.NoArgs,
ValidArgsFunction: noCompletions, // Disable file completion
Use: "get",
Short: "download extended information of a named release",
Long: getHelp,
Args: require.NoArgs,
}
cmd.AddCommand(newGetAllCmd(cfg, out))

@ -45,7 +45,7 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
return compListReleases(toComplete, args, cfg)
},
RunE: func(cmd *cobra.Command, args []string) error {
res, err := client.Run(args[0])
@ -59,7 +59,7 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return tpl(template, data, out)
}
return output.Table.Write(out, &statusPrinter{res, true, false})
return output.Table.Write(out, &statusPrinter{res, true, false, false})
},
}

@ -42,6 +42,10 @@ func TestGetCmd(t *testing.T) {
runTestCmd(t, tests)
}
func TestGetAllCompletion(t *testing.T) {
checkReleaseCompletion(t, "get all", false)
}
func TestGetAllRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get all")
}

@ -45,7 +45,7 @@ func newGetHooksCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
return compListReleases(toComplete, args, cfg)
},
RunE: func(cmd *cobra.Command, args []string) error {
res, err := client.Run(args[0])

@ -37,6 +37,10 @@ func TestGetHooks(t *testing.T) {
runTestCmd(t, tests)
}
func TestGetHooksCompletion(t *testing.T) {
checkReleaseCompletion(t, "get hooks", false)
}
func TestGetHooksRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get hooks")
}

@ -47,7 +47,7 @@ func newGetManifestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
return compListReleases(toComplete, args, cfg)
},
RunE: func(cmd *cobra.Command, args []string) error {
res, err := client.Run(args[0])

@ -37,6 +37,10 @@ func TestGetManifest(t *testing.T) {
runTestCmd(t, tests)
}
func TestGetManifestCompletion(t *testing.T) {
checkReleaseCompletion(t, "get manifest", false)
}
func TestGetManifestRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get manifest")
}

@ -43,7 +43,7 @@ func newGetNotesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
return compListReleases(toComplete, args, cfg)
},
RunE: func(cmd *cobra.Command, args []string) error {
res, err := client.Run(args[0])

@ -37,6 +37,10 @@ func TestGetNotesCmd(t *testing.T) {
runTestCmd(t, tests)
}
func TestGetNotesCompletion(t *testing.T) {
checkReleaseCompletion(t, "get notes", false)
}
func TestGetNotesRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get notes")
}

@ -50,7 +50,7 @@ func newGetValuesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
return compListReleases(toComplete, args, cfg)
},
RunE: func(cmd *cobra.Command, args []string) error {
vals, err := client.Run(args[0])

@ -53,6 +53,10 @@ func TestGetValuesCmd(t *testing.T) {
runTestCmd(t, tests)
}
func TestGetValuesCompletion(t *testing.T) {
checkReleaseCompletion(t, "get values", false)
}
func TestGetValuesRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get values")
}

@ -17,16 +17,13 @@ limitations under the License.
package main // import "helm.sh/helm/v3/cmd/helm"
import (
"flag"
"fmt"
"io/ioutil"
"io"
"log"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"k8s.io/klog"
"sigs.k8s.io/yaml"
// Import to initialize client auth plugins.
@ -34,15 +31,12 @@ import (
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/gates"
"helm.sh/helm/v3/pkg/kube"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
)
// FeatureGateOCI is the feature gate for checking if `helm chart` and `helm registry` commands should work
const FeatureGateOCI = gates.Gate("HELM_EXPERIMENTAL_OCI")
var settings = cli.New()
func init() {
@ -61,21 +55,17 @@ func warning(format string, v ...interface{}) {
fmt.Fprintf(os.Stderr, format, v...)
}
func initKubeLogs() {
pflag.CommandLine.SetNormalizeFunc(wordSepNormalizeFunc)
gofs := flag.NewFlagSet("klog", flag.ExitOnError)
klog.InitFlags(gofs)
pflag.CommandLine.AddGoFlagSet(gofs)
pflag.CommandLine.Set("logtostderr", "true")
}
func main() {
initKubeLogs()
// Setting the name of the app for managedFields in the Kubernetes client.
// It is set here to the full name of "helm" so that renaming of helm to
// another name (e.g., helm2 or helm3) does not change the name of the
// manager as picked up by the automated name detection.
kube.ManagedFieldsManager = "helm"
actionConfig := new(action.Configuration)
cmd, err := newRootCmd(actionConfig, os.Stdout, os.Args[1:])
if err != nil {
debug("%+v", err)
warning("%+v", err)
os.Exit(1)
}
@ -101,20 +91,6 @@ func main() {
}
}
// wordSepNormalizeFunc changes all flags that contain "_" separators
func wordSepNormalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName {
return pflag.NormalizedName(strings.ReplaceAll(name, "_", "-"))
}
func checkOCIFeatureGate() func(_ *cobra.Command, _ []string) error {
return func(_ *cobra.Command, _ []string) error {
if !FeatureGateOCI.IsEnabled() {
return FeatureGateOCI.Error()
}
return nil
}
}
// This function loads releases into the memory storage if the
// environment variable is properly set.
func loadReleasesInMemory(actionConfig *action.Configuration) {
@ -130,10 +106,10 @@ func loadReleasesInMemory(actionConfig *action.Configuration) {
return
}
actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard}
actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard}
for _, path := range filePaths {
b, err := ioutil.ReadFile(path)
b, err := os.ReadFile(path)
if err != nil {
log.Fatal("Unable to read memory driver data", err)
}

@ -18,7 +18,7 @@ package main
import (
"bytes"
"io/ioutil"
"io"
"os"
"os/exec"
"runtime"
@ -60,8 +60,11 @@ func runTestCmd(t *testing.T, tests []cmdTestCase) {
}
t.Logf("running cmd (attempt %d): %s", i+1, tt.cmd)
_, out, err := executeActionCommandC(storage, tt.cmd)
if (err != nil) != tt.wantError {
t.Errorf("expected error, got '%v'", err)
if tt.wantError && err == nil {
t.Errorf("expected error, got success with the following output:\n%s", out)
}
if !tt.wantError && err != nil {
t.Errorf("expected no error, got: '%v'", err)
}
if tt.golden != "" {
test.AssertGoldenString(t, out, tt.golden)
@ -71,27 +74,6 @@ func runTestCmd(t *testing.T, tests []cmdTestCase) {
}
}
func runTestActionCmd(t *testing.T, tests []cmdTestCase) {
t.Helper()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer resetEnv()()
store := storageFixture()
for _, rel := range tt.rels {
store.Create(rel)
}
_, out, err := executeActionCommandC(store, tt.cmd)
if (err != nil) != tt.wantError {
t.Errorf("expected error, got '%v'", err)
}
if tt.golden != "" {
test.AssertGoldenString(t, out, tt.golden)
}
})
}
}
func storageFixture() *storage.Storage {
return storage.Init(driver.NewMemory())
}
@ -110,7 +92,7 @@ func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string)
actionConfig := &action.Configuration{
Releases: store,
KubeClient: &kubefake.PrintingKubeClient{Out: ioutil.Discard},
KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard},
Capabilities: chartutil.DefaultCapabilities,
Log: func(format string, v ...interface{}) {},
}

@ -20,7 +20,6 @@ import (
"fmt"
"io"
"strconv"
"strings"
"time"
"github.com/gosuri/uitable"
@ -65,7 +64,7 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
return compListReleases(toComplete, args, cfg)
},
RunE: func(cmd *cobra.Command, args []string) error {
history, err := getHistory(client, args[0])
@ -191,10 +190,9 @@ func compListRevisions(toComplete string, cfg *action.Configuration, releaseName
var revisions []string
if hist, err := client.Run(releaseName); err == nil {
for _, release := range hist {
version := strconv.Itoa(release.Version)
if strings.HasPrefix(version, toComplete) {
revisions = append(revisions, version)
}
appVersion := fmt.Sprintf("App: %s", release.Chart.Metadata.AppVersion)
chartDesc := fmt.Sprintf("Chart: %s-%s", release.Chart.Metadata.Name, release.Chart.Metadata.Version)
revisions = append(revisions, fmt.Sprintf("%s\t%s, %s", strconv.Itoa(release.Version), appVersion, chartDesc))
}
return revisions, cobra.ShellCompDirectiveNoFileComp
}

@ -95,6 +95,11 @@ func revisionFlagCompletionTest(t *testing.T, cmdName string) {
cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName),
rels: releases,
golden: "output/revision-comp.txt",
}, {
name: "completion for revision flag, no filter",
cmd: fmt.Sprintf("__complete %s musketeers --revision 1", cmdName),
rels: releases,
golden: "output/revision-comp.txt",
}, {
name: "completion for revision flag with too few args",
cmd: fmt.Sprintf("__complete %s --revision ''", cmdName),
@ -109,6 +114,10 @@ func revisionFlagCompletionTest(t *testing.T, cmdName string) {
runTestCmd(t, tests)
}
func TestHistoryCompletion(t *testing.T) {
checkReleaseCompletion(t, "history", false)
}
func TestHistoryFileCompletion(t *testing.T) {
checkFileCompletion(t, "history", false)
checkFileCompletion(t, "history myrelease", false)

@ -17,8 +17,13 @@ limitations under the License.
package main
import (
"context"
"fmt"
"io"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/pkg/errors"
@ -44,9 +49,10 @@ a path to an unpacked chart directory or a URL.
To override values in a chart, use either the '--values' flag and pass in a file
or use the '--set' flag and pass configuration from the command line, to force
a string value use '--set-string'. In case a value is large and therefore
you want not to use neither '--values' nor '--set', use '--set-file' to read the
single large value from file.
a string value use '--set-string'. You can use '--set-file' to set individual
values from a file when the value itself is too long for the command line
or is dynamically generated. You can also use '--set-json' to set json values
(scalars/objects/arrays) from the command line.
$ helm install -f myvalues.yaml myredis ./redis
@ -62,6 +68,11 @@ or
$ helm install --set-file my_script=dothings.sh myredis ./redis
or
$ helm install --set-json 'master.sidecars=[{"name":"sidecar","image":"myImage","imagePullPolicy":"Always","ports":[{"name":"portname","containerPort":1234}]}]' myredis ./redis
You can specify the '--values'/'-f' flag multiple times. The priority will be given to the
last (right-most) file specified. For example, if both myvalues.yaml and override.yaml
contained a key called 'Test', the value set in override.yaml would take precedence:
@ -74,6 +85,13 @@ set for a key called 'foo', the 'newbar' value would take precedence:
$ helm install --set foo=bar --set foo=newbar myredis ./redis
Similarly, in the following example 'foo' is set to '["four"]':
$ helm install --set-json='foo=["one", "two", "three"]' --set-json='foo=["four"]' myredis ./redis
And in the following example, 'foo' is set to '{"key1":"value1","key2":"bar"}':
$ helm install --set-json='foo={"key1":"value1","key2":"value2"}' --set-json='foo.key2="bar"' myredis ./redis
To check the generated manifests of a release without installing the chart,
the '--debug' and '--dry-run' flags can be combined.
@ -81,13 +99,14 @@ the '--debug' and '--dry-run' flags can be combined.
If --verify is set, the chart MUST have a provenance file, and the provenance
file MUST pass all verification steps.
There are five different ways you can express the chart you want to install:
There are six different ways you can express the chart you want to install:
1. By chart reference: helm install mymaria example/mariadb
2. By path to a packaged chart: helm install mynginx ./nginx-1.2.3.tgz
3. By path to an unpacked chart directory: helm install mynginx ./nginx
4. By absolute URL: helm install mynginx https://example.com/charts/nginx-1.2.3.tgz
5. By chart reference and repo url: helm install --repo https://example.com/charts/ mynginx nginx
6. By OCI registries: helm install mynginx --version 1.2.3 oci://example.com/charts/nginx
CHART REFERENCES
@ -117,12 +136,25 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return compInstall(args, toComplete, client)
},
RunE: func(_ *cobra.Command, args []string) error {
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
client.SetRegistryClient(registryClient)
// This is for the case where "" is specifically passed in as a
// value. When there is no value passed in NoOptDefVal will be used
// and it is set to client. See addInstallFlags.
if client.DryRunOption == "" {
client.DryRunOption = "none"
}
rel, err := runInstall(args, client, valueOpts, out)
if err != nil {
return err
return errors.Wrap(err, "INSTALLATION FAILED")
}
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false})
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false})
},
}
@ -135,20 +167,29 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) {
f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install")
// --dry-run options with expected outcome:
// - Not set means no dry run and server is contacted.
// - Set with no value, a value of client, or a value of true and the server is not contacted
// - Set with a value of false, none, or false and the server is contacted
// The true/false part is meant to reflect some legacy behavior while none is equal to "".
f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.")
f.Lookup("dry-run").NoOptDefVal = "client"
f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout")
f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout")
f.BoolVarP(&client.GenerateName, "generate-name", "g", false, "generate the name (and omit the NAME parameter)")
f.StringVar(&client.NameTemplate, "name-template", "", "specify template used to name the release")
f.StringVar(&client.Description, "description", "", "add a custom description")
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "run helm dependency update before installing the chart")
f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart")
f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema")
f.BoolVar(&client.Atomic, "atomic", false, "if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used")
f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed. By default, CRDs are installed if not already present")
f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent")
f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates")
addValueOptionsFlags(f, valueOpts)
addChartPathOptionsFlags(f, &client.ChartPathOptions)
@ -213,6 +254,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
// As of Helm 2.4.0, this is treated as a stopping condition:
// https://github.com/helm/helm/issues/2209
if err := action.CheckDependencies(chartRequested, req); err != nil {
err = errors.Wrap(err, "An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies")
if client.DependencyUpdate {
man := &downloader.Manager{
Out: out,
@ -223,6 +265,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug,
RegistryClient: client.GetRegistryClient(),
}
if err := man.Update(); err != nil {
return nil, err
@ -238,7 +281,28 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
}
client.Namespace = settings.Namespace()
return client.Run(chartRequested, vals)
// Validate DryRunOption member is one of the allowed values
if err := validateDryRunOptionFlag(client.DryRunOption); err != nil {
return nil, err
}
// Create context and prepare the handle of SIGTERM
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
// Set up channel on which to send signal notifications.
// We must use a buffered channel or risk missing the signal
// if we're not ready to receive when the signal is sent.
cSignal := make(chan os.Signal, 2)
signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM)
go func() {
<-cSignal
fmt.Fprintf(out, "Release %s has been cancelled.\n", args[0])
cancel()
}()
return client.RunWithContext(ctx, chartRequested, vals)
}
// checkIfInstallable validates if a chart can be installed
@ -263,3 +327,19 @@ func compInstall(args []string, toComplete string, client *action.Install) ([]st
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
func validateDryRunOptionFlag(dryRunOptionFlagValue string) error {
// Validate dry-run flag value with a set of allowed value
allowedDryRunValues := []string{"false", "true", "none", "client", "server"}
isAllowed := false
for _, v := range allowedDryRunValues {
if dryRunOptionFlagValue == v {
isAllowed = true
break
}
}
if !isAllowed {
return errors.New("Invalid dry-run flag. Flag must one of the following: false, true, none, client, server")
}
return nil
}

@ -18,10 +18,39 @@ package main
import (
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"helm.sh/helm/v3/pkg/repo/repotest"
)
func TestInstall(t *testing.T) {
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*")
if err != nil {
t.Fatal(err)
}
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) {
http.FileServer(http.Dir(srv.Root())).ServeHTTP(w, r)
}))
defer srv2.Close()
if err := srv.LinkIndices(); err != nil {
t.Fatal(err)
}
repoFile := filepath.Join(srv.Root(), "repositories.yaml")
tests := []cmdTestCase{
// Install, base case
{
@ -85,10 +114,16 @@ func TestInstall(t *testing.T) {
cmd: "install apollo testdata/testcharts/empty --wait",
golden: "output/install-with-wait.txt",
},
// Install, with wait-for-jobs
{
name: "install with wait-for-jobs",
cmd: "install apollo testdata/testcharts/empty --wait --wait-for-jobs",
golden: "output/install-with-wait-for-jobs.txt",
},
// Install, using the name-template
{
name: "install with name-template",
cmd: "install testdata/testcharts/empty --name-template '{{upper \"foobar\"}}'",
cmd: "install testdata/testcharts/empty --name-template '{{ \"foobar\"}}'",
golden: "output/install-name-template.txt",
},
// Install, perform chart verification along the way.
@ -134,7 +169,7 @@ func TestInstall(t *testing.T) {
name: "install library chart",
cmd: "install libchart testdata/testcharts/lib-chart",
wantError: true,
golden: "output/template-lib-chart.txt",
golden: "output/install-lib-chart.txt",
},
// Install, chart with bad type
{
@ -201,9 +236,25 @@ func TestInstall(t *testing.T) {
name: "install chart with only crds",
cmd: "install crd-test testdata/testcharts/chart-with-only-crds --namespace default",
},
// Verify the user/pass works
{
name: "basic install with credentials",
cmd: "install aeneas reqtest --namespace default --repo " + srv.URL() + " --username username --password password",
golden: "output/install.txt",
},
{
name: "basic install with credentials",
cmd: "install aeneas reqtest --namespace default --repo " + srv2.URL + " --username username --password password --pass-credentials",
golden: "output/install.txt",
},
{
name: "basic install with credentials and no repo",
cmd: fmt.Sprintf("install aeneas test/reqtest --username username --password password --repository-config %s --repository-cache %s", repoFile, srv.Root()),
golden: "output/install.txt",
},
}
runTestActionCmd(t, tests)
runTestCmd(t, tests)
}
func TestInstallOutputCompletion(t *testing.T) {
@ -224,6 +275,10 @@ func TestInstallVersionCompletion(t *testing.T) {
name: "completion for install version flag with generate-name",
cmd: fmt.Sprintf("%s __complete install --generate-name testing/alpine --version ''", repoSetup),
golden: "output/version-comp.txt",
}, {
name: "completion for install version flag, no filter",
cmd: fmt.Sprintf("%s __complete install releasename testing/alpine --version 0.3", repoSetup),
golden: "output/version-comp.txt",
}, {
name: "completion for install version flag too few args",
cmd: fmt.Sprintf("%s __complete install testing/alpine --version ''", repoSetup),

@ -29,6 +29,7 @@ import (
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/values"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/lint/support"
)
var longLintHelp = `
@ -76,12 +77,23 @@ func newLintCmd(out io.Writer) *cobra.Command {
var message strings.Builder
failed := 0
errorsOrWarnings := 0
for _, path := range paths {
fmt.Fprintf(&message, "==> Linting %s\n", path)
result := client.Run([]string{path}, vals)
// If there is no errors/warnings and quiet flag is set
// go to the next chart
hasWarningsOrErrors := action.HasWarningsOrErrors(result)
if hasWarningsOrErrors {
errorsOrWarnings++
}
if client.Quiet && !hasWarningsOrErrors {
continue
}
fmt.Fprintf(&message, "==> Linting %s\n", path)
// All the Errors that are generated by a chart
// that failed a lint will be included in the
// results.Messages so we only need to print
@ -93,7 +105,9 @@ func newLintCmd(out io.Writer) *cobra.Command {
}
for _, msg := range result.Messages {
fmt.Fprintf(&message, "%s\n", msg)
if !client.Quiet || msg.Severity > support.InfoSev {
fmt.Fprintf(&message, "%s\n", msg)
}
}
if len(result.Errors) != 0 {
@ -112,7 +126,9 @@ func newLintCmd(out io.Writer) *cobra.Command {
if failed > 0 {
return errors.New(summary)
}
fmt.Fprintln(out, summary)
if !client.Quiet || errorsOrWarnings > 0 {
fmt.Fprintln(out, summary)
}
return nil
},
}
@ -120,6 +136,7 @@ func newLintCmd(out io.Writer) *cobra.Command {
f := cmd.Flags()
f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings")
f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
f.BoolVar(&client.Quiet, "quiet", false, "print only warnings and errors")
addValueOptionsFlags(f, valueOpts)
return cmd

@ -37,6 +37,32 @@ func TestLintCmdWithSubchartsFlag(t *testing.T) {
runTestCmd(t, tests)
}
func TestLintCmdWithQuietFlag(t *testing.T) {
testChart1 := "testdata/testcharts/alpine"
testChart2 := "testdata/testcharts/chart-bad-requirements"
tests := []cmdTestCase{{
name: "lint good chart using --quiet flag",
cmd: fmt.Sprintf("lint --quiet %s", testChart1),
golden: "output/lint-quiet.txt",
}, {
name: "lint two charts, one with error using --quiet flag",
cmd: fmt.Sprintf("lint --quiet %s %s", testChart1, testChart2),
golden: "output/lint-quiet-with-error.txt",
wantError: true,
}, {
name: "lint chart with warning using --quiet flag",
cmd: "lint --quiet testdata/testcharts/chart-with-only-crds",
golden: "output/lint-quiet-with-warning.txt",
}, {
name: "lint non-existent chart using --quiet flag",
cmd: "lint --quiet thischartdoesntexist/",
golden: "",
wantError: true,
}}
runTestCmd(t, tests)
}
func TestLintFileCompletion(t *testing.T) {
checkFileCompletion(t, "lint", true)
checkFileCompletion(t, "lint mypath", true) // Multiple paths can be given

@ -46,8 +46,8 @@ regular expressions (Perl compatible) that are applied to the list of releases.
Only items that match the filter will be returned.
$ helm list --filter 'ara[a-z]+'
NAME UPDATED CHART
maudlin-arachnid Mon May 9 16:07:08 2016 alpine-0.1.0
NAME UPDATED CHART
maudlin-arachnid 2020-06-18 14:17:46.125134977 +0000 UTC alpine-0.1.0
If no results are found, 'helm list' will exit 0, but with no output (or in
the case of no '-q' flag, only headers).
@ -83,8 +83,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
if client.Short {
names := make([]string, 0)
names := make([]string, 0, len(results))
for _, res := range results {
names = append(names, res.Name)
}
@ -103,17 +102,17 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
fmt.Fprintln(out, res.Name)
}
return nil
default:
return outfmt.Write(out, newReleaseListWriter(results))
}
}
return outfmt.Write(out, newReleaseListWriter(results))
return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat, client.NoHeaders))
},
}
f := cmd.Flags()
f.BoolVarP(&client.Short, "short", "q", false, "output short (quiet) listing format")
f.BoolVarP(&client.NoHeaders, "no-headers", "", false, "don't print headers when using the default output format")
f.StringVar(&client.TimeFormat, "time-format", "", `format time using golang time formatter. Example: --time-format "2006-01-02 15:04:05Z0700"`)
f.BoolVarP(&client.ByDate, "date", "d", false, "sort by release date")
f.BoolVarP(&client.SortReverse, "reverse", "r", false, "reverse the sort order")
f.BoolVarP(&client.All, "all", "a", false, "show all releases without any filter applied")
@ -125,7 +124,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.Pending, "pending", false, "show pending releases")
f.BoolVarP(&client.AllNamespaces, "all-namespaces", "A", false, "list releases across all namespaces")
f.IntVarP(&client.Limit, "max", "m", 256, "maximum number of releases to fetch")
f.IntVar(&client.Offset, "offset", 0, "next release name in the list, used to offset from start value")
f.IntVar(&client.Offset, "offset", 0, "next release index in the list, used to offset from start value")
f.StringVarP(&client.Filter, "filter", "f", "", "a regular expression (Perl compatible). Any releases that match the expression will be included in the results")
f.StringVarP(&client.Selector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Works only for secret(default) and configmap storage backends.")
bindOutputFlag(cmd, &outfmt)
@ -144,10 +143,11 @@ type releaseElement struct {
}
type releaseListWriter struct {
releases []releaseElement
releases []releaseElement
noHeaders bool
}
func newReleaseListWriter(releases []*release.Release) *releaseListWriter {
func newReleaseListWriter(releases []*release.Release, timeFormat string, noHeaders bool) *releaseListWriter {
// Initialize the array so no results returns an empty array instead of null
elements := make([]releaseElement, 0, len(releases))
for _, r := range releases {
@ -156,22 +156,30 @@ func newReleaseListWriter(releases []*release.Release) *releaseListWriter {
Namespace: r.Namespace,
Revision: strconv.Itoa(r.Version),
Status: r.Info.Status.String(),
Chart: fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version),
AppVersion: r.Chart.Metadata.AppVersion,
Chart: formatChartname(r.Chart),
AppVersion: formatAppVersion(r.Chart),
}
t := "-"
if tspb := r.Info.LastDeployed; !tspb.IsZero() {
t = tspb.String()
if timeFormat != "" {
t = tspb.Format(timeFormat)
} else {
t = tspb.String()
}
}
element.Updated = t
elements = append(elements, element)
}
return &releaseListWriter{elements}
return &releaseListWriter{elements, noHeaders}
}
func (r *releaseListWriter) WriteTable(out io.Writer) error {
table := uitable.New()
table.AddRow("NAME", "NAMESPACE", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION")
if !r.noHeaders {
table.AddRow("NAME", "NAMESPACE", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION")
}
for _, r := range r.releases {
table.AddRow(r.Name, r.Namespace, r.Revision, r.Updated, r.Status, r.Chart, r.AppVersion)
}
@ -186,24 +194,57 @@ func (r *releaseListWriter) WriteYAML(out io.Writer) error {
return output.EncodeYAML(out, r.releases)
}
// Returns all releases from 'releases', except those with names matching 'ignoredReleases'
func filterReleases(releases []*release.Release, ignoredReleaseNames []string) []*release.Release {
// if ignoredReleaseNames is nil, just return releases
if ignoredReleaseNames == nil {
return releases
}
var filteredReleases []*release.Release
for _, rel := range releases {
found := false
for _, ignoredName := range ignoredReleaseNames {
if rel.Name == ignoredName {
found = true
break
}
}
if !found {
filteredReleases = append(filteredReleases, rel)
}
}
return filteredReleases
}
// Provide dynamic auto-completion for release names
func compListReleases(toComplete string, cfg *action.Configuration) ([]string, cobra.ShellCompDirective) {
func compListReleases(toComplete string, ignoredReleaseNames []string, cfg *action.Configuration) ([]string, cobra.ShellCompDirective) {
cobra.CompDebugln(fmt.Sprintf("compListReleases with toComplete %s", toComplete), settings.Debug)
client := action.NewList(cfg)
client.All = true
client.Limit = 0
client.Filter = fmt.Sprintf("^%s", toComplete)
// Do not filter so as to get the entire list of releases.
// This will allow zsh and fish to match completion choices
// on other criteria then prefix. For example:
// helm status ingress<TAB>
// can match
// helm status nginx-ingress
//
// client.Filter = fmt.Sprintf("^%s", toComplete)
client.SetStateMask()
results, err := client.Run()
releases, err := client.Run()
if err != nil {
return nil, cobra.ShellCompDirectiveDefault
}
var choices []string
for _, res := range results {
choices = append(choices, res.Name)
filteredReleases := filterReleases(releases, ignoredReleaseNames)
for _, rel := range filteredReleases {
choices = append(choices,
fmt.Sprintf("%s\t%s-%s -> %s", rel.Name, rel.Chart.Metadata.Name, rel.Chart.Metadata.Version, rel.Info.Status.String()))
}
return choices, cobra.ShellCompDirectiveNoFileComp

@ -148,6 +148,11 @@ func TestListCmd(t *testing.T) {
cmd: "list",
golden: "output/list.txt",
rels: releaseFixture,
}, {
name: "list without headers",
cmd: "list --no-headers",
golden: "output/list-no-headers.txt",
rels: releaseFixture,
}, {
name: "list all releases",
cmd: "list --all",

@ -19,7 +19,6 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
@ -59,7 +58,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
found, err := plugin.FindPlugins(settings.PluginsDirectory)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err)
fmt.Fprintf(os.Stderr, "failed to load plugins: %s\n", err)
return
}
@ -154,7 +153,7 @@ func callPluginExecutable(pluginName string, main string, argv []string, out io.
func manuallyProcessArgs(args []string) ([]string, []string) {
known := []string{}
unknown := []string{}
kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--registry-config", "--repository-cache", "--repository-config"}
kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--kube-as-user", "--kube-as-group", "--kube-ca-file", "--registry-config", "--repository-cache", "--repository-config", "--insecure-skip-tls-verify", "--tls-server-name"}
knownArg := func(a string) bool {
for _, pre := range kvargs {
if strings.HasPrefix(a, pre+"=") {
@ -311,9 +310,9 @@ func addPluginCommands(plugin *plugin.Plugin, baseCmd *cobra.Command, cmds *plug
// loadFile takes a yaml file at the given path, parses it and returns a pluginCommand object
func loadFile(path string) (*pluginCommand, error) {
cmds := new(pluginCommand)
b, err := ioutil.ReadFile(path)
b, err := os.ReadFile(path)
if err != nil {
return cmds, errors.New(fmt.Sprintf("File (%s) not provided by plugin. No plugin auto-completion possible.", path))
return cmds, fmt.Errorf("file (%s) not provided by plugin. No plugin auto-completion possible", path)
}
err = yaml.Unmarshal(b, cmds)

@ -19,7 +19,6 @@ package main
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
@ -48,7 +47,7 @@ If '--keyring' is not specified, Helm usually defaults to the public keyring
unless your environment is otherwise configured.
`
func newPackageCmd(out io.Writer) *cobra.Command {
func newPackageCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewPackage()
valueOpts := &values.Options{}
@ -87,11 +86,12 @@ func newPackageCmd(out io.Writer) *cobra.Command {
if client.DependencyUpdate {
downloadManager := &downloader.Manager{
Out: ioutil.Discard,
Out: io.Discard,
ChartPath: path,
Keyring: client.Keyring,
Getters: p,
Debug: settings.Debug,
RegistryClient: cfg.RegistryClient,
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
}
@ -114,6 +114,7 @@ func newPackageCmd(out io.Writer) *cobra.Command {
f.BoolVar(&client.Sign, "sign", false, "use a PGP private key to sign this package")
f.StringVar(&client.Key, "key", "", "name of the key to use when signing. Used if --sign is true")
f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "location of a public keyring")
f.StringVar(&client.PassphraseFile, "passphrase-file", "", `location of a file which contains the passphrase for the signing key. Use "-" in order to read from stdin.`)
f.StringVar(&client.Version, "version", "", "set the version on the chart to this semver version")
f.StringVar(&client.AppVersion, "app-version", "", "set the appVersion on the chart to this version")
f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.")

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

@ -32,10 +32,9 @@ Manage client-side Helm plugins.
func newPluginCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "plugin",
Short: "install, list, or uninstall Helm plugins",
Long: pluginHelp,
ValidArgsFunction: noCompletions, // Disable file completion
Use: "plugin",
Short: "install, list, or uninstall Helm plugins",
Long: pluginHelp,
}
cmd.AddCommand(
newPluginInstallCmd(out),

@ -19,6 +19,7 @@ import (
"fmt"
"io"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
@ -81,7 +82,7 @@ func (o *pluginInstallOptions) run(out io.Writer) error {
debug("loading plugin from %s", i.Path())
p, err := plugin.LoadDir(i.Path())
if err != nil {
return err
return errors.Wrap(err, "plugin is installed but unusable")
}
if err := runHook(p, plugin.Install); err != nil {

@ -18,7 +18,6 @@ package main
import (
"fmt"
"io"
"strings"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
@ -51,15 +50,38 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
return cmd
}
// Returns all plugins from plugins, except those with names matching ignoredPluginNames
func filterPlugins(plugins []*plugin.Plugin, ignoredPluginNames []string) []*plugin.Plugin {
// if ignoredPluginNames is nil, just return plugins
if ignoredPluginNames == nil {
return plugins
}
var filteredPlugins []*plugin.Plugin
for _, plugin := range plugins {
found := false
for _, ignoredName := range ignoredPluginNames {
if plugin.Metadata.Name == ignoredName {
found = true
break
}
}
if !found {
filteredPlugins = append(filteredPlugins, plugin)
}
}
return filteredPlugins
}
// Provide dynamic auto-completion for plugin names
func compListPlugins(toComplete string) []string {
func compListPlugins(toComplete string, ignoredPluginNames []string) []string {
var pNames []string
plugins, err := plugin.FindPlugins(settings.PluginsDirectory)
if err == nil {
for _, p := range plugins {
if strings.HasPrefix(p.Metadata.Name, toComplete) {
pNames = append(pNames, p.Metadata.Name)
}
if err == nil && len(plugins) > 0 {
filteredPlugins := filterPlugins(plugins, ignoredPluginNames)
for _, p := range filteredPlugins {
pNames = append(pNames, fmt.Sprintf("%s\t%s", p.Metadata.Name, p.Metadata.Usage))
}
}
return pNames

@ -37,6 +37,9 @@ func TestManuallyProcessArgs(t *testing.T) {
"--kubeconfig", "/home/foo",
"--kube-context=test1",
"--kube-context", "test1",
"--kube-as-user", "pikachu",
"--kube-as-group", "teatime",
"--kube-as-group", "admins",
"-n=test2",
"-n", "test2",
"--namespace=test2",
@ -51,6 +54,9 @@ func TestManuallyProcessArgs(t *testing.T) {
"--kubeconfig", "/home/foo",
"--kube-context=test1",
"--kube-context", "test1",
"--kube-as-user", "pikachu",
"--kube-as-group", "teatime",
"--kube-as-group", "admins",
"-n=test2",
"-n", "test2",
"--namespace=test2",
@ -271,11 +277,6 @@ func TestPluginDynamicCompletion(t *testing.T) {
cmd: "__complete echo -n mynamespace ''",
golden: "output/plugin_echo_no_directive.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin bad directive",
cmd: "__complete echo ''",
golden: "output/plugin_echo_bad_directive.txt",
rels: []*release.Release{},
}}
for _, test := range tests {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
@ -299,6 +300,60 @@ func TestLoadPlugins_HelmNoPlugins(t *testing.T) {
}
}
func TestPluginCmdsCompletion(t *testing.T) {
tests := []cmdTestCase{{
name: "completion for plugin update",
cmd: "__complete plugin update ''",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin update, no filter",
cmd: "__complete plugin update full",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin update repetition",
cmd: "__complete plugin update args ''",
golden: "output/plugin_repeat_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin uninstall",
cmd: "__complete plugin uninstall ''",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin uninstall, no filter",
cmd: "__complete plugin uninstall full",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin uninstall repetition",
cmd: "__complete plugin uninstall args ''",
golden: "output/plugin_repeat_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin list",
cmd: "__complete plugin list ''",
golden: "output/empty_nofile_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin install no args",
cmd: "__complete plugin install ''",
golden: "output/empty_default_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin install one arg",
cmd: "__complete plugin list /tmp ''",
golden: "output/empty_nofile_comp.txt",
rels: []*release.Release{},
}, {}}
for _, test := range tests {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
runTestCmd(t, []cmdTestCase{test})
}
}
func TestPluginFileCompletion(t *testing.T) {
checkFileCompletion(t, "plugin", false)
}

@ -39,10 +39,7 @@ func newPluginUninstallCmd(out io.Writer) *cobra.Command {
Aliases: []string{"rm", "remove"},
Short: "uninstall one or more Helm plugins",
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return compListPlugins(toComplete), cobra.ShellCompDirectiveNoFileComp
return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return o.complete(args)

@ -40,10 +40,7 @@ func newPluginUpdateCmd(out io.Writer) *cobra.Command {
Aliases: []string{"up"},
Short: "update one or more Helm plugins",
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return compListPlugins(toComplete), cobra.ShellCompDirectiveNoFileComp
return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp
},
PreRunE: func(cmd *cobra.Command, args []string) error {
return o.complete(args)

@ -42,8 +42,8 @@ file, and MUST pass the verification process. Failure in any part of this will
result in an error, and the chart will not be saved locally.
`
func newPullCmd(out io.Writer) *cobra.Command {
client := action.NewPull()
func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewPullWithOpts(action.WithConfig(cfg))
cmd := &cobra.Command{
Use: "pull [chart URL | repo/chartname] [...]",
@ -64,6 +64,13 @@ func newPullCmd(out io.Writer) *cobra.Command {
client.Version = ">0.0.0-0"
}
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
client.SetRegistryClient(registryClient)
for i := 0; i < len(args); i++ {
output, err := client.Run(args[i])
if err != nil {
@ -80,7 +87,7 @@ func newPullCmd(out io.Writer) *cobra.Command {
f.BoolVar(&client.Untar, "untar", false, "if set to true, will untar the chart after downloading it")
f.BoolVar(&client.VerifyLater, "prov", false, "fetch the provenance file, but don't perform verification")
f.StringVar(&client.UntarDir, "untardir", ".", "if untar is specified, this flag specifies the name of the directory into which the chart is expanded")
f.StringVarP(&client.DestDir, "destination", "d", ".", "location to write the chart. If this and tardir are specified, tardir is appended to this")
f.StringVarP(&client.DestDir, "destination", "d", ".", "location to write the chart. If this and untardir are specified, untardir is appended to this")
addChartPathOptionsFlags(f, &client.ChartPathOptions)
err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {

@ -18,6 +18,8 @@ package main
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
@ -26,12 +28,18 @@ import (
)
func TestPullCmd(t *testing.T) {
srv, err := repotest.NewTempServer("testdata/testcharts/*.tgz*")
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*")
if err != nil {
t.Fatal(err)
}
defer srv.Stop()
ociSrv, err := repotest.NewOCIServer(t, srv.Root())
if err != nil {
t.Fatal(err)
}
ociSrv.Run(t)
if err := srv.LinkIndices(); err != nil {
t.Fatal(err)
}
@ -139,23 +147,70 @@ func TestPullCmd(t *testing.T) {
failExpect: "Failed to fetch chart version",
wantError: true,
},
{
name: "Fetch OCI Chart",
args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0", ociSrv.RegistryURL),
expectFile: "./oci-dependent-chart-0.1.0.tgz",
},
{
name: "Fetch OCI Chart with untar",
args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar", ociSrv.RegistryURL),
expectFile: "./oci-dependent-chart",
expectDir: true,
},
{
name: "Fetch OCI Chart with untar and untardir",
args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar --untardir ocitest2", ociSrv.RegistryURL),
expectFile: "./ocitest2",
expectDir: true,
},
{
name: "OCI Fetch untar when dir with same name existed",
args: fmt.Sprintf("oci-test-chart oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar --untardir ocitest2 --untar --untardir ocitest2", ociSrv.RegistryURL),
wantError: true,
wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "ocitest2")),
},
{
name: "Fail fetching non-existent OCI chart",
args: fmt.Sprintf("oci://%s/u/ocitestuser/nosuchthing --version 0.1.0", ociSrv.RegistryURL),
failExpect: "Failed to fetch",
wantError: true,
},
{
name: "Fail fetching OCI chart without version specified",
args: fmt.Sprintf("oci://%s/u/ocitestuser/nosuchthing", ociSrv.RegistryURL),
wantErrorMsg: "Error: --version flag is explicitly required for OCI registries",
wantError: true,
},
{
name: "Fail fetching OCI chart without version specified",
args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0", ociSrv.RegistryURL),
wantErrorMsg: "Error: --version flag is explicitly required for OCI registries",
wantError: true,
},
{
name: "Fail fetching OCI chart without version specified",
args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0 --version 0.1.0", ociSrv.RegistryURL),
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
outdir := srv.Root()
cmd := fmt.Sprintf("fetch %s -d '%s' --repository-config %s --repository-cache %s ",
cmd := fmt.Sprintf("fetch %s -d '%s' --repository-config %s --repository-cache %s --registry-config %s",
tt.args,
outdir,
filepath.Join(outdir, "repositories.yaml"),
outdir,
filepath.Join(outdir, "config.json"),
)
// Create file or Dir before helm pull --untar, see: https://github.com/helm/helm/issues/7182
if tt.existFile != "" {
file := filepath.Join(outdir, tt.existFile)
_, err := os.Create(file)
if err != nil {
t.Fatal("err")
t.Fatal(err)
}
}
if tt.existDir != "" {
@ -196,6 +251,115 @@ func TestPullCmd(t *testing.T) {
}
}
func TestPullWithCredentialsCmd(t *testing.T) {
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*")
if err != nil {
t.Fatal(err)
}
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) {
http.FileServer(http.Dir(srv.Root())).ServeHTTP(w, r)
}))
defer srv2.Close()
if err := srv.LinkIndices(); err != nil {
t.Fatal(err)
}
// all flags will get "-d outdir" appended.
tests := []struct {
name string
args string
existFile string
existDir string
wantError bool
wantErrorMsg string
expectFile string
expectDir bool
}{
{
name: "Chart fetch using repo URL",
expectFile: "./signtest-0.1.0.tgz",
args: "signtest --repo " + srv.URL() + " --username username --password password",
},
{
name: "Fail fetching non-existent chart on repo URL",
args: "someChart --repo " + srv.URL() + " --username username --password password",
wantError: true,
},
{
name: "Specific version chart fetch using repo URL",
expectFile: "./signtest-0.1.0.tgz",
args: "signtest --version=0.1.0 --repo " + srv.URL() + " --username username --password password",
},
{
name: "Specific version chart fetch using repo URL",
args: "signtest --version=0.2.0 --repo " + srv.URL() + " --username username --password password",
wantError: true,
},
{
name: "Chart located on different domain with credentials passed",
args: "reqtest --repo " + srv2.URL + " --username username --password password --pass-credentials",
expectFile: "./reqtest-0.1.0.tgz",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
outdir := srv.Root()
cmd := fmt.Sprintf("pull %s -d '%s' --repository-config %s --repository-cache %s --registry-config %s",
tt.args,
outdir,
filepath.Join(outdir, "repositories.yaml"),
outdir,
filepath.Join(outdir, "config.json"),
)
// Create file or Dir before helm pull --untar, see: https://github.com/helm/helm/issues/7182
if tt.existFile != "" {
file := filepath.Join(outdir, tt.existFile)
_, err := os.Create(file)
if err != nil {
t.Fatal(err)
}
}
if tt.existDir != "" {
file := filepath.Join(outdir, tt.existDir)
err := os.Mkdir(file, 0755)
if err != nil {
t.Fatal(err)
}
}
_, _, err := executeActionCommand(cmd)
if err != nil {
if tt.wantError {
if tt.wantErrorMsg != "" && tt.wantErrorMsg == err.Error() {
t.Fatalf("Actual error %s, not equal to expected error %s", err, tt.wantErrorMsg)
}
return
}
t.Fatalf("%q reported error: %s", tt.name, err)
}
ef := filepath.Join(outdir, tt.expectFile)
fi, err := os.Stat(ef)
if err != nil {
t.Errorf("%q: expected a file at %s. %s", tt.name, ef, err)
}
if fi.IsDir() != tt.expectDir {
t.Errorf("%q: expected directory=%t, but it's not.", tt.name, tt.expectDir)
}
})
}
}
func TestPullVersionCompletion(t *testing.T) {
repoFile := "testdata/helmhome/helm/repositories.yaml"
repoCache := "testdata/helmhome/helm/repository"
@ -206,6 +370,10 @@ func TestPullVersionCompletion(t *testing.T) {
name: "completion for pull version flag",
cmd: fmt.Sprintf("%s __complete pull testing/alpine --version ''", repoSetup),
golden: "output/version-comp.txt",
}, {
name: "completion for pull version flag, no filter",
cmd: fmt.Sprintf("%s __complete pull testing/alpine --version 0.3", repoSetup),
golden: "output/version-comp.txt",
}, {
name: "completion for pull version flag too few args",
cmd: fmt.Sprintf("%s __complete pull --version ''", repoSetup),

@ -0,0 +1,101 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"io"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/pusher"
)
const pushDesc = `
Upload a chart to a registry.
If the chart has an associated provenance file,
it will also be uploaded.
`
type registryPushOptions struct {
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
plainHTTP bool
}
func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
o := &registryPushOptions{}
cmd := &cobra.Command{
Use: "push [chart] [remote]",
Short: "push a chart to remote",
Long: pushDesc,
Args: require.MinimumNArgs(2),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
// Do file completion for the chart file to push
return nil, cobra.ShellCompDirectiveDefault
}
if len(args) == 1 {
providers := []pusher.Provider(pusher.All(settings))
var comps []string
for _, p := range providers {
for _, scheme := range p.Schemes {
comps = append(comps, fmt.Sprintf("%s://", scheme))
}
}
return comps, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
registryClient, err := newRegistryClient(o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSverify, o.plainHTTP)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
cfg.RegistryClient = registryClient
chartRef := args[0]
remote := args[1]
client := action.NewPushWithOpts(action.WithPushConfig(cfg),
action.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile),
action.WithInsecureSkipTLSVerify(o.insecureSkipTLSverify),
action.WithPlainHTTP(o.plainHTTP),
action.WithPushOptWriter(out))
client.Settings = settings
output, err := client.Run(chartRef, remote)
if err != nil {
return err
}
fmt.Fprint(out, output)
return nil
},
}
f := cmd.Flags()
f.StringVar(&o.certFile, "cert-file", "", "identify registry client using this SSL certificate file")
f.StringVar(&o.keyFile, "key-file", "", "identify registry client using this SSL key file")
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart upload")
f.BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the chart upload")
return cmd
}

@ -14,16 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConstants(t *testing.T) {
knownMediaTypes := KnownMediaTypes()
assert.Contains(t, knownMediaTypes, HelmChartConfigMediaType)
assert.Contains(t, knownMediaTypes, HelmChartContentLayerMediaType)
func TestPushFileCompletion(t *testing.T) {
checkFileCompletion(t, "push", true)
checkFileCompletion(t, "push package.tgz", false)
checkFileCompletion(t, "push package.tgz oci://localhost:5000", false)
}

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

@ -21,11 +21,10 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/docker/docker/pkg/term"
"github.com/moby/term"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
@ -36,45 +35,61 @@ const registryLoginDesc = `
Authenticate to a remote registry.
`
type registryLoginOptions struct {
username string
password string
passwordFromStdinOpt bool
certFile string
keyFile string
caFile string
insecure bool
}
func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
var usernameOpt, passwordOpt string
var passwordFromStdinOpt, insecureOpt bool
o := &registryLoginOptions{}
cmd := &cobra.Command{
Use: "login [host]",
Short: "login to a registry",
Long: registryLoginDesc,
Args: require.MinimumNArgs(1),
Hidden: !FeatureGateOCI.IsEnabled(),
Use: "login [host]",
Short: "login to a registry",
Long: registryLoginDesc,
Args: require.MinimumNArgs(1),
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
hostname := args[0]
username, password, err := getUsernamePassword(usernameOpt, passwordOpt, passwordFromStdinOpt)
username, password, err := getUsernamePassword(o.username, o.password, o.passwordFromStdinOpt)
if err != nil {
return err
}
return action.NewRegistryLogin(cfg).Run(out, hostname, username, password, insecureOpt)
return action.NewRegistryLogin(cfg).Run(out, hostname, username, password,
action.WithCertFile(o.certFile),
action.WithKeyFile(o.keyFile),
action.WithCAFile(o.caFile),
action.WithInsecure(o.insecure))
},
}
f := cmd.Flags()
f.StringVarP(&usernameOpt, "username", "u", "", "registry username")
f.StringVarP(&passwordOpt, "password", "p", "", "registry password or identity token")
f.BoolVarP(&passwordFromStdinOpt, "password-stdin", "", false, "read password or identity token from stdin")
f.BoolVarP(&insecureOpt, "insecure", "", false, "allow connections to TLS registry without certs")
f.StringVarP(&o.username, "username", "u", "", "registry username")
f.StringVarP(&o.password, "password", "p", "", "registry password or identity token")
f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read password or identity token from stdin")
f.BoolVarP(&o.insecure, "insecure", "", false, "allow connections to TLS registry without certs")
f.StringVar(&o.certFile, "cert-file", "", "identify registry client using this SSL certificate file")
f.StringVar(&o.keyFile, "key-file", "", "identify registry client using this SSL key file")
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
return cmd
}
// Adapted from https://github.com/deislabs/oras
// Adapted from https://github.com/oras-project/oras
func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) {
var err error
username := usernameOpt
password := passwordOpt
if passwordFromStdinOpt {
passwordFromStdin, err := ioutil.ReadAll(os.Stdin)
passwordFromStdin, err := io.ReadAll(os.Stdin)
if err != nil {
return "", "", err
}
@ -104,13 +119,13 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd
}
}
} else {
fmt.Fprintln(os.Stderr, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
warning("Using --password via the CLI is insecure. Use --password-stdin.")
}
return username, password, nil
}
// Copied/adapted from https://github.com/deislabs/oras
// Copied/adapted from https://github.com/oras-project/oras
func readLine(prompt string, silent bool) (string, error) {
fmt.Print(prompt)
if silent {

@ -14,15 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package registry // import "helm.sh/helm/v3/internal/experimental/registry"
package main
import (
"github.com/deislabs/oras/pkg/auth"
"testing"
)
type (
// Authorizer handles registry auth operations
Authorizer struct {
auth.Client
}
)
func TestRegistryLoginFileCompletion(t *testing.T) {
checkFileCompletion(t, "registry login", false)
}

@ -31,11 +31,11 @@ Remove credentials stored for a remote registry.
func newRegistryLogoutCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return &cobra.Command{
Use: "logout [host]",
Short: "logout from a registry",
Long: registryLogoutDesc,
Args: require.MinimumNArgs(1),
Hidden: !FeatureGateOCI.IsEnabled(),
Use: "logout [host]",
Short: "logout from a registry",
Long: registryLogoutDesc,
Args: require.MinimumNArgs(1),
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
hostname := args[0]
return action.NewRegistryLogout(cfg).Run(out, hostname)

@ -14,15 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package registry // import "helm.sh/helm/v3/internal/experimental/registry"
package main
import (
"github.com/containerd/containerd/remotes"
"testing"
)
type (
// Resolver provides remotes based on a locator
Resolver struct {
remotes.Resolver
}
)
func TestRegistryLogoutFileCompletion(t *testing.T) {
checkFileCompletion(t, "registry logout", false)
}

@ -19,6 +19,8 @@ package main
import (
"fmt"
"io"
"regexp"
"strings"
"time"
"github.com/spf13/cobra"
@ -39,6 +41,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
client := action.NewReleaseTesting(cfg)
var outfmt = output.Table
var outputLogs bool
var filter []string
cmd := &cobra.Command{
Use: "test [RELEASE]",
@ -49,10 +52,18 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
return compListReleases(toComplete, args, cfg)
},
RunE: func(cmd *cobra.Command, args []string) error {
client.Namespace = settings.Namespace()
notName := regexp.MustCompile(`^!\s?name=`)
for _, f := range filter {
if strings.HasPrefix(f, "name=") {
client.Filters[action.IncludeNameFilter] = append(client.Filters[action.IncludeNameFilter], strings.TrimPrefix(f, "name="))
} else if notName.MatchString(f) {
client.Filters[action.ExcludeNameFilter] = append(client.Filters[action.ExcludeNameFilter], notName.ReplaceAllLiteralString(f, ""))
}
}
rel, runErr := client.Run(args[0])
// We only return an error if we weren't even able to get the
// release, otherwise we keep going so we can print status and logs
@ -61,7 +72,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
return runErr
}
if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}); err != nil {
if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false}); err != nil {
return err
}
@ -80,6 +91,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
f := cmd.Flags()
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&outputLogs, "logs", false, "dump the logs from test pods (this runs after all tests are complete, but before any cleanup)")
f.StringSliceVar(&filter, "filter", []string{}, "specify tests by attribute (currently \"name\") using attribute=value syntax or '!attribute=value' to exclude a test (can specify multiple or separate values with commas: name=test1,name=test2)")
return cmd
}

@ -20,6 +20,10 @@ import (
"testing"
)
func TestReleaseTestingCompletion(t *testing.T) {
checkReleaseCompletion(t, "test", false)
}
func TestReleaseTestingFileCompletion(t *testing.T) {
checkFileCompletion(t, "test", false)
checkFileCompletion(t, "test myrelease", false)

@ -34,11 +34,10 @@ It can be used to add, remove, list, and index chart repositories.
func newRepoCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "repo add|remove|list|index|update [ARGS]",
Short: "add, list, remove, update, and index chart repositories",
Long: repoHelm,
Args: require.NoArgs,
ValidArgsFunction: noCompletions, // Disable file completion
Use: "repo add|remove|list|index|update [ARGS]",
Short: "add, list, remove, update, and index chart repositories",
Long: repoHelm,
Args: require.NoArgs,
}
cmd.AddCommand(newRepoAddCmd(out))

@ -20,7 +20,6 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -29,7 +28,7 @@ import (
"github.com/gofrs/flock"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/term"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/cmd/helm/require"
@ -37,12 +36,21 @@ import (
"helm.sh/helm/v3/pkg/repo"
)
// Repositories that have been permanently deleted and no longer work
var deprecatedRepos = map[string]string{
"//kubernetes-charts.storage.googleapis.com": "https://charts.helm.sh/stable",
"//kubernetes-charts-incubator.storage.googleapis.com": "https://charts.helm.sh/incubator",
}
type repoAddOptions struct {
name string
url string
username string
password string
noUpdate bool
name string
url string
username string
password string
passwordFromStdinOpt bool
passCredentialsAll bool
forceUpdate bool
allowDeprecatedRepos bool
certFile string
keyFile string
@ -51,6 +59,9 @@ type repoAddOptions struct {
repoFile string
repoCache string
// Deprecated, but cannot be removed until Helm 4
deprecatedNoUpdate bool
}
func newRepoAddCmd(out io.Writer) *cobra.Command {
@ -74,24 +85,44 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
f := cmd.Flags()
f.StringVar(&o.username, "username", "", "chart repository username")
f.StringVar(&o.password, "password", "", "chart repository password")
f.BoolVar(&o.noUpdate, "no-update", false, "raise error if repo is already registered")
f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read chart repository password from stdin")
f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists")
f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.")
f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository")
f.BoolVar(&o.allowDeprecatedRepos, "allow-deprecated-repos", false, "by default, this command will not allow adding official repos that have been permanently deleted. This disables that behavior")
f.BoolVar(&o.passCredentialsAll, "pass-credentials", false, "pass credentials to all domains")
return cmd
}
func (o *repoAddOptions) run(out io.Writer) error {
//Ensure the file directory exists as it is required for file locking
// Block deprecated repos
if !o.allowDeprecatedRepos {
for oldURL, newURL := range deprecatedRepos {
if strings.Contains(o.url, oldURL) {
return fmt.Errorf("repo %q is no longer available; try %q instead", o.url, newURL)
}
}
}
// Ensure the file directory exists as it is required for file locking
err := os.MkdirAll(filepath.Dir(o.repoFile), os.ModePerm)
if err != nil && !os.IsExist(err) {
return err
}
// Acquire a file lock for process synchronization
fileLock := flock.New(strings.Replace(o.repoFile, filepath.Ext(o.repoFile), ".lock", 1))
repoFileExt := filepath.Ext(o.repoFile)
var lockPath string
if len(repoFileExt) > 0 && len(repoFileExt) < len(o.repoFile) {
lockPath = strings.TrimSuffix(o.repoFile, repoFileExt) + ".lock"
} else {
lockPath = o.repoFile + ".lock"
}
fileLock := flock.New(lockPath)
lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
locked, err := fileLock.TryLockContext(lockCtx, time.Second)
@ -102,7 +133,7 @@ func (o *repoAddOptions) run(out io.Writer) error {
return err
}
b, err := ioutil.ReadFile(o.repoFile)
b, err := os.ReadFile(o.repoFile)
if err != nil && !os.IsNotExist(err) {
return err
}
@ -112,19 +143,25 @@ func (o *repoAddOptions) run(out io.Writer) error {
return err
}
if o.noUpdate && f.Has(o.name) {
return errors.Errorf("repository name (%s) already exists, please specify a different name", o.name)
}
if o.username != "" && o.password == "" {
fd := int(os.Stdin.Fd())
fmt.Fprint(out, "Password: ")
password, err := terminal.ReadPassword(fd)
fmt.Fprintln(out)
if err != nil {
return err
if o.passwordFromStdinOpt {
passwordFromStdin, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
password := strings.TrimSuffix(string(passwordFromStdin), "\n")
password = strings.TrimSuffix(password, "\r")
o.password = password
} else {
fd := int(os.Stdin.Fd())
fmt.Fprint(out, "Password: ")
password, err := term.ReadPassword(fd)
fmt.Fprintln(out)
if err != nil {
return err
}
o.password = string(password)
}
o.password = string(password)
}
c := repo.Entry{
@ -132,12 +169,35 @@ func (o *repoAddOptions) run(out io.Writer) error {
URL: o.url,
Username: o.username,
Password: o.password,
PassCredentialsAll: o.passCredentialsAll,
CertFile: o.certFile,
KeyFile: o.keyFile,
CAFile: o.caFile,
InsecureSkipTLSverify: o.insecureSkipTLSverify,
}
// Check if the repo name is legal
if strings.Contains(o.name, "/") {
return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", o.name)
}
// If the repo exists do one of two things:
// 1. If the configuration for the name is the same continue without error
// 2. When the config is different require --force-update
if !o.forceUpdate && f.Has(o.name) {
existing := f.Get(o.name)
if c != *existing {
// The input coming in for the name is different from what is already
// configured. Return an error.
return errors.Errorf("repository name (%s) already exists, please specify a different name", o.name)
}
// The add is idempotent so do nothing
fmt.Fprintf(out, "%q already exists with the same configuration, skipping\n", o.name)
return nil
}
r, err := repo.NewChartRepository(&c, getter.All(settings))
if err != nil {
return err
@ -152,7 +212,7 @@ func (o *repoAddOptions) run(out io.Writer) error {
f.Update(&c)
if err := f.WriteFile(o.repoFile, 0644); err != nil {
if err := f.WriteFile(o.repoFile, 0600); err != nil {
return err
}
fmt.Fprintf(out, "%q has been added to your repositories\n", o.name)

@ -18,9 +18,10 @@ package main
import (
"fmt"
"io/ioutil"
"io"
"os"
"path/filepath"
"strings"
"sync"
"testing"
@ -34,26 +35,54 @@ import (
)
func TestRepoAddCmd(t *testing.T) {
srv, err := repotest.NewTempServer("testdata/testserver/*.*")
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer srv.Stop()
tmpdir := ensure.TempDir(t)
// A second test server is setup to verify URL changing
srv2, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer srv2.Stop()
tmpdir := filepath.Join(ensure.TempDir(t), "path-component.yaml/data")
err = os.MkdirAll(tmpdir, 0777)
if err != nil {
t.Fatal(err)
}
repoFile := filepath.Join(tmpdir, "repositories.yaml")
tests := []cmdTestCase{{
name: "add a repository",
cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s", srv.URL(), repoFile, tmpdir),
golden: "output/repo-add.txt",
}}
tests := []cmdTestCase{
{
name: "add a repository",
cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s", srv.URL(), repoFile, tmpdir),
golden: "output/repo-add.txt",
},
{
name: "add repository second time",
cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s", srv.URL(), repoFile, tmpdir),
golden: "output/repo-add2.txt",
},
{
name: "add repository different url",
cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s", srv2.URL(), repoFile, tmpdir),
wantError: true,
},
{
name: "add repository second time",
cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s --force-update", srv2.URL(), repoFile, tmpdir),
golden: "output/repo-add.txt",
},
}
runTestCmd(t, tests)
}
func TestRepoAdd(t *testing.T) {
ts, err := repotest.NewTempServer("testdata/testserver/*.*")
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
@ -65,14 +94,15 @@ func TestRepoAdd(t *testing.T) {
const testRepoName = "test-name"
o := &repoAddOptions{
name: testRepoName,
url: ts.URL(),
noUpdate: true,
repoFile: repoFile,
name: testRepoName,
url: ts.URL(),
forceUpdate: false,
deprecatedNoUpdate: true,
repoFile: repoFile,
}
os.Setenv(xdg.CacheHomeEnvVar, rootDir)
if err := o.run(ioutil.Discard); err != nil {
if err := o.run(io.Discard); err != nil {
t.Error(err)
}
@ -94,17 +124,50 @@ func TestRepoAdd(t *testing.T) {
t.Errorf("Error cache charts file was not created for repository %s", testRepoName)
}
o.noUpdate = false
o.forceUpdate = true
if err := o.run(ioutil.Discard); err != nil {
if err := o.run(io.Discard); err != nil {
t.Errorf("Repository was not updated: %s", err)
}
if err := o.run(ioutil.Discard); err != nil {
if err := o.run(io.Discard); err != nil {
t.Errorf("Duplicate repository name was added")
}
}
func TestRepoAddCheckLegalName(t *testing.T) {
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
defer resetEnv()()
const testRepoName = "test-hub/test-name"
rootDir := ensure.TempDir(t)
repoFile := filepath.Join(ensure.TempDir(t), "repositories.yaml")
o := &repoAddOptions{
name: testRepoName,
url: ts.URL(),
forceUpdate: false,
deprecatedNoUpdate: true,
repoFile: repoFile,
}
os.Setenv(xdg.CacheHomeEnvVar, rootDir)
wantErrorMsg := fmt.Sprintf("repository name (%s) contains '/', please specify a different name without '/'", testRepoName)
if err := o.run(io.Discard); err != nil {
if wantErrorMsg != err.Error() {
t.Fatalf("Actual error %s, not equal to expected error %s", err, wantErrorMsg)
}
} else {
t.Fatalf("expect reported an error.")
}
}
func TestRepoAddConcurrentGoRoutines(t *testing.T) {
const testName = "test-name"
repoFile := filepath.Join(ensure.TempDir(t), "repositories.yaml")
@ -117,8 +180,20 @@ func TestRepoAddConcurrentDirNotExist(t *testing.T) {
repoAddConcurrent(t, testName, repoFile)
}
func TestRepoAddConcurrentNoFileExtension(t *testing.T) {
const testName = "test-name-3"
repoFile := filepath.Join(ensure.TempDir(t), "repositories")
repoAddConcurrent(t, testName, repoFile)
}
func TestRepoAddConcurrentHiddenFile(t *testing.T) {
const testName = "test-name-4"
repoFile := filepath.Join(ensure.TempDir(t), ".repositories")
repoAddConcurrent(t, testName, repoFile)
}
func repoAddConcurrent(t *testing.T, testName, repoFile string) {
ts, err := repotest.NewTempServer("testdata/testserver/*.*")
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
@ -130,19 +205,20 @@ func repoAddConcurrent(t *testing.T, testName, repoFile string) {
go func(name string) {
defer wg.Done()
o := &repoAddOptions{
name: name,
url: ts.URL(),
noUpdate: true,
repoFile: repoFile,
name: name,
url: ts.URL(),
deprecatedNoUpdate: true,
forceUpdate: false,
repoFile: repoFile,
}
if err := o.run(ioutil.Discard); err != nil {
if err := o.run(io.Discard); err != nil {
t.Error(err)
}
}(fmt.Sprintf("%s-%d", testName, i))
}
wg.Wait()
b, err := ioutil.ReadFile(repoFile)
b, err := os.ReadFile(repoFile)
if err != nil {
t.Error(err)
}
@ -166,3 +242,33 @@ func TestRepoAddFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo add reponame", false)
checkFileCompletion(t, "repo add reponame https://example.com", false)
}
func TestRepoAddWithPasswordFromStdin(t *testing.T) {
srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/testserver/*.*")
defer srv.Stop()
defer resetEnv()()
in, err := os.Open("testdata/password")
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
tmpdir := ensure.TempDir(t)
repoFile := filepath.Join(tmpdir, "repositories.yaml")
store := storageFixture()
const testName = "test-name"
const username = "username"
cmd := fmt.Sprintf("repo add %s %s --repository-config %s --repository-cache %s --username %s --password-stdin", testName, srv.URL(), repoFile, tmpdir, username)
var result string
_, result, err = executeActionCommandStdinC(store, in, cmd)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
if !strings.Contains(result, fmt.Sprintf("\"%s\" has been added to your repositories", testName)) {
t.Errorf("Repo was not successfully added. Output: %s", result)
}
}

@ -17,8 +17,8 @@ limitations under the License.
package main
import (
"fmt"
"io"
"strings"
"github.com/gosuri/uitable"
"github.com/pkg/errors"
@ -38,8 +38,8 @@ func newRepoListCmd(out io.Writer) *cobra.Command {
Args: require.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
f, err := repo.LoadFile(settings.RepositoryConfig)
if isNotExist(err) || (len(f.Repositories) == 0 && !(outfmt == output.JSON || outfmt == output.YAML)) {
f, _ := repo.LoadFile(settings.RepositoryConfig)
if len(f.Repositories) == 0 && !(outfmt == output.JSON || outfmt == output.YAML) {
return errors.New("no repositories to show")
}
@ -130,9 +130,7 @@ func compListRepos(prefix string, ignoredRepoNames []string) []string {
if err == nil && len(f.Repositories) > 0 {
filteredRepos := filterRepos(f.Repositories, ignoredRepoNames)
for _, repo := range filteredRepos {
if strings.HasPrefix(repo.Name, prefix) {
rNames = append(rNames, repo.Name)
}
rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL))
}
}
return rNames

@ -67,7 +67,7 @@ func (o *repoRemoveOptions) run(out io.Writer) error {
if !r.Remove(name) {
return errors.Errorf("no repo named %q found", name)
}
if err := r.WriteFile(o.repoFile, 0644); err != nil {
if err := r.WriteFile(o.repoFile, 0600); err != nil {
return err
}

@ -18,6 +18,7 @@ package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
@ -30,7 +31,7 @@ import (
)
func TestRepoRemove(t *testing.T) {
ts, err := repotest.NewTempServer("testdata/testserver/*.*")
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
@ -161,6 +162,55 @@ func testCacheFiles(t *testing.T, cacheIndexFile string, cacheChartsFile string,
}
}
func TestRepoRemoveCompletion(t *testing.T) {
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
rootDir := ensure.TempDir(t)
repoFile := filepath.Join(rootDir, "repositories.yaml")
repoCache := filepath.Join(rootDir, "cache/")
var testRepoNames = []string{"foo", "bar", "baz"}
// Add test repos
for _, repoName := range testRepoNames {
o := &repoAddOptions{
name: repoName,
url: ts.URL(),
repoFile: repoFile,
}
if err := o.run(os.Stderr); err != nil {
t.Error(err)
}
}
repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache)
// In the following tests, we turn off descriptions for completions by using __completeNoDesc.
// We have to do this because the description will contain the port used by the webserver,
// and that port changes each time we run the test.
tests := []cmdTestCase{{
name: "completion for repo remove",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove ''", repoSetup),
golden: "output/repo_list_comp.txt",
}, {
name: "completion for repo remove, no filter",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove fo", repoSetup),
golden: "output/repo_list_comp.txt",
}, {
name: "completion for repo remove repetition",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove foo ''", repoSetup),
golden: "output/repo_repeat_comp.txt",
}}
for _, test := range tests {
runTestCmd(t, []cmdTestCase{test})
}
}
func TestRepoRemoveFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo remove", false)
checkFileCompletion(t, "repo remove repo1", false)

@ -32,70 +32,136 @@ import (
const updateDesc = `
Update gets the latest information about charts from the respective chart repositories.
Information is cached locally, where it is used by commands like 'helm search'.
You can optionally specify a list of repositories you want to update.
$ helm repo update <repo_name> ...
To update all the repositories, use 'helm repo update'.
`
var errNoRepositories = errors.New("no repositories found. You must add one before updating")
type repoUpdateOptions struct {
update func([]*repo.ChartRepository, io.Writer)
repoFile string
repoCache string
update func([]*repo.ChartRepository, io.Writer, bool) error
repoFile string
repoCache string
names []string
failOnRepoUpdateFail bool
}
func newRepoUpdateCmd(out io.Writer) *cobra.Command {
o := &repoUpdateOptions{update: updateCharts}
cmd := &cobra.Command{
Use: "update",
Aliases: []string{"up"},
Short: "update information of available charts locally from chart repositories",
Long: updateDesc,
Args: require.NoArgs,
ValidArgsFunction: noCompletions,
Use: "update [REPO1 [REPO2 ...]]",
Aliases: []string{"up"},
Short: "update information of available charts locally from chart repositories",
Long: updateDesc,
Args: require.MinimumNArgs(0),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compListRepos(toComplete, args), cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
o.repoFile = settings.RepositoryConfig
o.repoCache = settings.RepositoryCache
o.names = args
return o.run(out)
},
}
f := cmd.Flags()
// Adding this flag for Helm 3 as stop gap functionality for https://github.com/helm/helm/issues/10016.
// This should be deprecated in Helm 4 by update to the behaviour of `helm repo update` command.
f.BoolVar(&o.failOnRepoUpdateFail, "fail-on-repo-update-fail", false, "update fails if any of the repository updates fail")
return cmd
}
func (o *repoUpdateOptions) run(out io.Writer) error {
f, err := repo.LoadFile(o.repoFile)
if isNotExist(err) || len(f.Repositories) == 0 {
switch {
case isNotExist(err):
return errNoRepositories
case err != nil:
return errors.Wrapf(err, "failed loading file: %s", o.repoFile)
case len(f.Repositories) == 0:
return errNoRepositories
}
var repos []*repo.ChartRepository
for _, cfg := range f.Repositories {
r, err := repo.NewChartRepository(cfg, getter.All(settings))
if err != nil {
updateAllRepos := len(o.names) == 0
if !updateAllRepos {
// Fail early if the user specified an invalid repo to update
if err := checkRequestedRepos(o.names, f.Repositories); err != nil {
return err
}
if o.repoCache != "" {
r.CachePath = o.repoCache
}
for _, cfg := range f.Repositories {
if updateAllRepos || isRepoRequested(cfg.Name, o.names) {
r, err := repo.NewChartRepository(cfg, getter.All(settings))
if err != nil {
return err
}
if o.repoCache != "" {
r.CachePath = o.repoCache
}
repos = append(repos, r)
}
repos = append(repos, r)
}
o.update(repos, out)
return nil
return o.update(repos, out, o.failOnRepoUpdateFail)
}
func updateCharts(repos []*repo.ChartRepository, out io.Writer) {
func updateCharts(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup
var repoFailList []string
for _, re := range repos {
wg.Add(1)
go func(re *repo.ChartRepository) {
defer wg.Done()
if _, err := re.DownloadIndexFile(); err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err)
repoFailList = append(repoFailList, re.Config.URL)
} else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name)
}
}(re)
}
wg.Wait()
if len(repoFailList) > 0 && failOnRepoUpdateFail {
return fmt.Errorf("Failed to update the following repositories: %s",
repoFailList)
}
fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
return nil
}
func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) error {
for _, requestedRepo := range requestedRepos {
found := false
for _, repo := range validRepos {
if requestedRepo == repo.Name {
found = true
break
}
}
if !found {
return errors.Errorf("no repositories found matching '%s'. Nothing will be updated", requestedRepo)
}
}
return nil
}
func isRepoRequested(repoName string, requestedRepos []string) bool {
for _, requestedRepo := range requestedRepos {
if repoName == requestedRepo {
return true
}
}
return false
}

@ -34,10 +34,11 @@ func TestUpdateCmd(t *testing.T) {
var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer) {
updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos {
fmt.Fprintln(out, re.Config.Name)
}
return nil
}
o := &repoUpdateOptions{
update: updater,
@ -47,27 +48,82 @@ func TestUpdateCmd(t *testing.T) {
t.Fatal(err)
}
if got := out.String(); !strings.Contains(got, "charts") {
t.Errorf("Expected 'charts' got %q", got)
if got := out.String(); !strings.Contains(got, "charts") ||
!strings.Contains(got, "firstexample") ||
!strings.Contains(got, "secondexample") {
t.Errorf("Expected 'charts', 'firstexample' and 'secondexample' but got %q", got)
}
}
func TestUpdateCustomCacheCmd(t *testing.T) {
func TestUpdateCmdMultiple(t *testing.T) {
var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos {
fmt.Fprintln(out, re.Config.Name)
}
return nil
}
o := &repoUpdateOptions{
update: updater,
repoFile: "testdata/repositories.yaml",
names: []string{"firstexample", "charts"},
}
if err := o.run(&out); err != nil {
t.Fatal(err)
}
if got := out.String(); !strings.Contains(got, "charts") ||
!strings.Contains(got, "firstexample") ||
strings.Contains(got, "secondexample") {
t.Errorf("Expected 'charts' and 'firstexample' but not 'secondexample' but got %q", got)
}
}
func TestUpdateCmdInvalid(t *testing.T) {
var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos {
fmt.Fprintln(out, re.Config.Name)
}
return nil
}
o := &repoUpdateOptions{
update: updater,
repoFile: "testdata/repositories.yaml",
names: []string{"firstexample", "invalid"},
}
if err := o.run(&out); err == nil {
t.Fatal("expected error but did not get one")
}
}
func TestUpdateCustomCacheCmd(t *testing.T) {
rootDir := ensure.TempDir(t)
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/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
o := &repoUpdateOptions{
update: updateCharts,
repoFile: "testdata/repositories.yaml",
repoFile: filepath.Join(ts.Root(), "repositories.yaml"),
repoCache: cachePath,
}
if err := o.run(&out); err != nil {
b := io.Discard
if err := o.run(b); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(cachePath, "charts-index.yaml")); err != nil {
t.Fatalf("error finding created index file in custom cache: %#v", err)
if _, err := os.Stat(filepath.Join(cachePath, "test-index.yaml")); err != nil {
t.Fatalf("error finding created index file in custom cache: %v", err)
}
}
@ -75,7 +131,7 @@ func TestUpdateCharts(t *testing.T) {
defer resetEnv()()
defer ensure.HelmHome(t)()
ts, err := repotest.NewTempServer("testdata/testserver/*.*")
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
@ -90,7 +146,7 @@ func TestUpdateCharts(t *testing.T) {
}
b := bytes.NewBuffer(nil)
updateCharts([]*repo.ChartRepository{r}, b)
updateCharts([]*repo.ChartRepository{r}, b, false)
got := b.String()
if strings.Contains(got, "Unable to get an update") {
@ -103,4 +159,81 @@ func TestUpdateCharts(t *testing.T) {
func TestRepoUpdateFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo update", false)
checkFileCompletion(t, "repo update repo1", false)
}
func TestUpdateChartsFail(t *testing.T) {
defer resetEnv()()
defer ensure.HelmHome(t)()
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
var invalidURL = ts.URL() + "55"
r, err := repo.NewChartRepository(&repo.Entry{
Name: "charts",
URL: invalidURL,
}, getter.All(settings))
if err != nil {
t.Error(err)
}
b := bytes.NewBuffer(nil)
if err := updateCharts([]*repo.ChartRepository{r}, b, false); err != nil {
t.Error("Repo update should not return error if update of repository fails")
}
got := b.String()
if !strings.Contains(got, "Unable to get an update") {
t.Errorf("Repo should have failed update but instead got: %q", got)
}
if !strings.Contains(got, "Update Complete.") {
t.Error("Update was not successful")
}
}
func TestUpdateChartsFailWithError(t *testing.T) {
defer resetEnv()()
defer ensure.HelmHome(t)()
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
var invalidURL = ts.URL() + "55"
r, err := repo.NewChartRepository(&repo.Entry{
Name: "charts",
URL: invalidURL,
}, getter.All(settings))
if err != nil {
t.Error(err)
}
b := bytes.NewBuffer(nil)
err = updateCharts([]*repo.ChartRepository{r}, b, true)
if err == nil {
t.Error("Repo update should return error because update of repository fails and 'fail-on-repo-update-fail' flag set")
return
}
var expectedErr = "Failed to update the following repositories"
var receivedErr = err.Error()
if !strings.Contains(receivedErr, expectedErr) {
t.Errorf("Expected error (%s) but got (%s) instead", expectedErr, receivedErr)
}
if !strings.Contains(receivedErr, invalidURL) {
t.Errorf("Expected invalid URL (%s) in error message but got (%s) instead", invalidURL, receivedErr)
}
got := b.String()
if !strings.Contains(got, "Unable to get an update") {
t.Errorf("Repo should have failed update but instead got: %q", got)
}
if strings.Contains(got, "Update Complete.") {
t.Error("Update was not successful and should return error message because 'fail-on-repo-update-fail' flag set")
}
}

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

@ -48,7 +48,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Args: require.MinimumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return compListReleases(toComplete, cfg)
return compListReleases(toComplete, args, cfg)
}
if len(args) == 1 {
@ -82,6 +82,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during rollback")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout")
f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout")
f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this rollback when rollback fails")
f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit")

@ -54,6 +54,11 @@ func TestRollbackCmd(t *testing.T) {
cmd: "rollback funny-honey 1 --wait",
golden: "output/rollback-wait.txt",
rels: rels,
}, {
name: "rollback a release with wait-for-jobs",
cmd: "rollback funny-honey 1 --wait --wait-for-jobs",
golden: "output/rollback-wait-for-jobs.txt",
rels: rels,
}, {
name: "rollback a release without revision",
cmd: "rollback funny-honey",

@ -21,6 +21,7 @@ import (
"fmt"
"io"
"log"
"os"
"strings"
"github.com/spf13/cobra"
@ -28,8 +29,9 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/repo"
)
var globalUsage = `The Kubernetes package manager
@ -43,16 +45,31 @@ Common actions for Helm:
Environment variables:
| Name | Description |
|------------------------------------|-----------------------------------------------------------------------------------|
| $HELM_CACHE_HOME | set an alternative location for storing cached files. |
| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. |
| $HELM_DATA_HOME | set an alternative location for storing Helm data. |
| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, postgres |
| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. |
| $HELM_MAX_HISTORY | set the maximum number of helm release history. |
| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. |
| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") |
| Name | Description |
|------------------------------------|---------------------------------------------------------------------------------------------------|
| $HELM_CACHE_HOME | set an alternative location for storing cached files. |
| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. |
| $HELM_DATA_HOME | set an alternative location for storing Helm data. |
| $HELM_DEBUG | indicate whether or not Helm is running in Debug mode |
| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. |
| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. |
| $HELM_MAX_HISTORY | set the maximum number of helm release history. |
| $HELM_NAMESPACE | set the namespace used for the helm operations. |
| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. |
| $HELM_PLUGINS | set the path to the plugins directory |
| $HELM_REGISTRY_CONFIG | set the path to the registry config file. |
| $HELM_REPOSITORY_CACHE | set the path to the repository cache directory |
| $HELM_REPOSITORY_CONFIG | set the path to the repositories file. |
| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") |
| $HELM_KUBEAPISERVER | set the Kubernetes API Server Endpoint for authentication |
| $HELM_KUBECAFILE | set the Kubernetes certificate authority file. |
| $HELM_KUBEASGROUPS | set the Groups to use for impersonation using a comma-separated list. |
| $HELM_KUBEASUSER | set the Username to impersonate for the operation. |
| $HELM_KUBECONTEXT | set the name of the kubeconfig context. |
| $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. |
| $HELM_KUBEINSECURE_SKIP_TLS_VERIFY | indicate if the Kubernetes API server's certificate validation should be skipped (insecure) |
| $HELM_KUBETLS_SERVER_NAME | set the server name used to validate the Kubernetes API server certificate |
| $HELM_BURST_LIMIT | set the default burst limit in the case the server contains many CRDs (default 100, -1 to disable)|
Helm stores cache, configuration, and data based on the following configuration order:
@ -75,18 +92,16 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
Short: "The Helm package manager for Kubernetes.",
Long: globalUsage,
SilenceUsage: true,
// This breaks completion for 'helm help <TAB>'
// The Cobra release following 1.0 will fix this
//ValidArgsFunction: noCompletions, // Disable file completion
}
flags := cmd.PersistentFlags()
settings.AddFlags(flags)
addKlogFlags(flags)
// Setup shell completion for the namespace flag
err := cmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if client, err := actionConfig.KubernetesClientSet(); err == nil {
// Choose a long enough timeout that the user notices somethings 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
to := int64(3)
cobra.CompDebugln(fmt.Sprintf("About to call kube client for namespaces with timeout of: %d", to), settings.Debug)
@ -94,9 +109,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
nsNames := []string{}
if namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{TimeoutSeconds: &to}); err == nil {
for _, ns := range namespaces.Items {
if strings.HasPrefix(ns.Name, toComplete) {
nsNames = append(nsNames, ns.Name)
}
nsNames = append(nsNames, ns.Name)
}
return nsNames, cobra.ShellCompDirectiveNoFileComp
}
@ -119,13 +132,11 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
if config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
loadingRules,
&clientcmd.ConfigOverrides{}).RawConfig(); err == nil {
ctxs := []string{}
for name := range config.Contexts {
if strings.HasPrefix(name, toComplete) {
ctxs = append(ctxs, name)
}
comps := []string{}
for name, context := range config.Contexts {
comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster))
}
return ctxs, cobra.ShellCompDirectiveNoFileComp
return comps, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
})
@ -141,15 +152,21 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
flags.ParseErrorsWhitelist.UnknownFlags = true
flags.Parse(args)
registryClient, err := newDefaultRegistryClient(false)
if err != nil {
return nil, err
}
actionConfig.RegistryClient = registryClient
// Add subcommands
cmd.AddCommand(
// chart commands
newCreateCmd(out),
newDependencyCmd(out),
newPullCmd(out),
newShowCmd(out),
newDependencyCmd(actionConfig, out),
newPullCmd(actionConfig, out),
newShowCmd(actionConfig, out),
newLintCmd(out),
newPackageCmd(out),
newPackageCmd(actionConfig, out),
newRepoCmd(out),
newSearchCmd(out),
newVerifyCmd(out),
@ -175,23 +192,112 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
newDocsCmd(out),
)
// Add *experimental* subcommands
registryClient, err := registry.NewClient(
registry.ClientOptDebug(settings.Debug),
registry.ClientOptWriter(out),
registry.ClientOptCredentialsFile(settings.RegistryConfig),
)
if err != nil {
return nil, err
}
actionConfig.RegistryClient = registryClient
cmd.AddCommand(
newRegistryCmd(actionConfig, out),
newChartCmd(actionConfig, out),
newPushCmd(actionConfig, out),
)
// Find and add plugins
loadPlugins(cmd, out)
// Check permissions on critical files
checkPerms()
// Check for expired repositories
checkForExpiredRepos(settings.RepositoryConfig)
return cmd, nil
}
func checkForExpiredRepos(repofile string) {
expiredRepos := []struct {
name string
old string
new string
}{
{
name: "stable",
old: "kubernetes-charts.storage.googleapis.com",
new: "https://charts.helm.sh/stable",
},
{
name: "incubator",
old: "kubernetes-charts-incubator.storage.googleapis.com",
new: "https://charts.helm.sh/incubator",
},
}
// parse repo file.
// Ignore the error because it is okay for a repo file to be unparseable at this
// stage. Later checks will trap the error and respond accordingly.
repoFile, err := repo.LoadFile(repofile)
if err != nil {
return
}
for _, exp := range expiredRepos {
r := repoFile.Get(exp.name)
if r == nil {
return
}
if url := r.URL; strings.Contains(url, exp.old) {
fmt.Fprintf(
os.Stderr,
"WARNING: %q is deprecated for %q and will be deleted Nov. 13, 2020.\nWARNING: You should switch to %q via:\nWARNING: helm repo add %q %q --force-update\n",
exp.old,
exp.name,
exp.new,
exp.name,
exp.new,
)
}
}
}
func newRegistryClient(certFile, keyFile, caFile string, insecureSkipTLSverify, plainHTTP bool) (*registry.Client, error) {
if certFile != "" && keyFile != "" || caFile != "" || insecureSkipTLSverify {
registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSverify)
if err != nil {
return nil, err
}
return registryClient, nil
}
registryClient, err := newDefaultRegistryClient(plainHTTP)
if err != nil {
return nil, err
}
return registryClient, nil
}
func newDefaultRegistryClient(plainHTTP bool) (*registry.Client, error) {
opts := []registry.ClientOption{
registry.ClientOptDebug(settings.Debug),
registry.ClientOptEnableCache(true),
registry.ClientOptWriter(os.Stderr),
registry.ClientOptCredentialsFile(settings.RegistryConfig),
}
if plainHTTP {
opts = append(opts, registry.ClientOptPlainHTTP())
}
// Create a new registry client
registryClient, err := registry.NewClient(opts...)
if err != nil {
return nil, err
}
return registryClient, nil
}
func newRegistryClientWithTLS(certFile, keyFile, caFile string, insecureSkipTLSverify bool) (*registry.Client, error) {
// Create a new registry client
registryClient, err := registry.NewRegistryClientWithTLS(os.Stderr, certFile, keyFile, caFile, insecureSkipTLSverify,
settings.RegistryConfig, settings.Debug,
)
if err != nil {
return nil, err
}
return registryClient, nil
}

@ -0,0 +1,58 @@
//go: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)
}
}

@ -0,0 +1,82 @@
//go: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"
"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 := t.TempDir()
tfile := filepath.Join(tdir, "testconfig")
fh, err := os.OpenFile(tfile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0440)
if err != nil {
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,25 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package action
package main
import (
"io"
)
// ChartList performs a chart list operation.
type ChartList struct {
cfg *Configuration
}
// NewChartList creates a new ChartList object with the given configuration.
func NewChartList(cfg *Configuration) *ChartList {
return &ChartList{
cfg: cfg,
}
}
// Run executes the chart list operation
func (a *ChartList) Run(out io.Writer) error {
return a.cfg.RegistryClient.PrintChartTable()
func checkPerms() {
// Not yet implemented on Windows. If you know how to do a comprehensive perms
// check on Windows, contributions welcomed!
}

@ -24,17 +24,16 @@ import (
const searchDesc = `
Search provides the ability to search for Helm charts in the various places
they can be stored including the Helm Hub and repositories you have added. Use
search subcommands to search different locations for charts.
they can be stored including the Artifact Hub and repositories you have added.
Use search subcommands to search different locations for charts.
`
func newSearchCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "search [keyword]",
Short: "search for a keyword in charts",
Long: searchDesc,
ValidArgsFunction: noCompletions, // Disable file completion
Use: "search [keyword]",
Short: "search for a keyword in charts",
Long: searchDesc,
}
cmd.AddCommand(newSearchHubCmd(out))

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*Package search provides client-side repository searching.
/*
Package search provides client-side repository searching.
This supports building an in-memory search index based on the contents of
multiple repositories, and then using string matching or regular expressions
@ -146,11 +147,10 @@ func (i *Index) SearchLiteral(term string, threshold int) []*Result {
term = strings.ToLower(term)
buf := []*Result{}
for k, v := range i.lines {
lk := strings.ToLower(k)
lv := strings.ToLower(v)
res := strings.Index(lv, term)
if score := i.calcScore(res, lv); res != -1 && score < threshold {
parts := strings.Split(lk, verSep) // Remove version, if it is there.
parts := strings.Split(k, verSep) // Remove version, if it is there.
buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]})
}
}

@ -105,11 +105,11 @@ func loadTestIndex(t *testing.T, all bool) *Index {
i := NewIndex()
i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all)
i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{
"pinta": {
"Pinta": {
{
URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"},
Metadata: &chart.Metadata{
Name: "pinta",
Name: "Pinta",
Version: "2.0.0",
Description: "Two ship, version two",
},
@ -170,14 +170,14 @@ func TestSearchByName(t *testing.T) {
query: "pinta",
expect: []*Result{
{Name: "testing/pinta"},
{Name: "ztesting/pinta"},
{Name: "ztesting/Pinta"},
},
},
{
name: "repo-specific search for one result",
query: "ztesting/pinta",
expect: []*Result{
{Name: "ztesting/pinta"},
{Name: "ztesting/Pinta"},
},
},
{
@ -199,7 +199,15 @@ func TestSearchByName(t *testing.T) {
query: "two",
expect: []*Result{
{Name: "testing/pinta"},
{Name: "ztesting/pinta"},
{Name: "ztesting/Pinta"},
},
},
{
name: "search mixedCase and result should be mixedCase too",
query: "pinta",
expect: []*Result{
{Name: "testing/pinta"},
{Name: "ztesting/Pinta"},
},
},
{
@ -207,7 +215,7 @@ func TestSearchByName(t *testing.T) {
query: "TWO",
expect: []*Result{
{Name: "testing/pinta"},
{Name: "ztesting/pinta"},
{Name: "ztesting/Pinta"},
},
},
{

@ -30,29 +30,38 @@ import (
)
const searchHubDesc = `
Search the Helm Hub or an instance of Monocular for Helm charts.
The Helm Hub provides a centralized search for publicly available distributed
charts. It is maintained by the Helm project. It can be visited at
https://hub.helm.sh
Monocular is a web-based application that enables the search and discovery of
charts from multiple Helm Chart repositories. It is the codebase that powers the
Helm Hub. You can find it at https://github.com/helm/monocular
Search for Helm charts in the Artifact Hub or your own hub instance.
Artifact Hub is a web-based application that enables finding, installing, and
publishing packages and configurations for CNCF projects, including publicly
available distributed charts Helm charts. It is a Cloud Native Computing
Foundation sandbox project. You can browse the hub at https://artifacthub.io/
The [KEYWORD] argument accepts either a keyword string, or quoted string of rich
query options. For rich query options documentation, see
https://artifacthub.github.io/hub/api/?urls.primaryName=Monocular%20compatible%20search%20API#/Monocular/get_api_chartsvc_v1_charts_search
Previous versions of Helm used an instance of Monocular as the default
'endpoint', so for backwards compatibility Artifact Hub is compatible with the
Monocular search API. Similarly, when setting the 'endpoint' flag, the specified
endpoint must also be implement a Monocular compatible search API endpoint.
Note that when specifying a Monocular instance as the 'endpoint', rich queries
are not supported. For API details, see https://github.com/helm/monocular
`
type searchHubOptions struct {
searchEndpoint string
maxColWidth uint
outputFormat output.Format
listRepoURL bool
}
func newSearchHubCmd(out io.Writer) *cobra.Command {
o := &searchHubOptions{}
cmd := &cobra.Command{
Use: "hub [keyword]",
Short: "search for charts in the Helm Hub or an instance of Monocular",
Use: "hub [KEYWORD]",
Short: "search for charts in the Artifact Hub or your own hub instance",
Long: searchHubDesc,
RunE: func(cmd *cobra.Command, args []string) error {
return o.run(out, args)
@ -60,8 +69,10 @@ func newSearchHubCmd(out io.Writer) *cobra.Command {
}
f := cmd.Flags()
f.StringVar(&o.searchEndpoint, "endpoint", "https://hub.helm.sh", "monocular 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.BoolVar(&o.listRepoURL, "list-repo-url", false, "print charts repository URL")
bindOutputFlag(cmd, &o.outputFormat)
return cmd
@ -80,28 +91,42 @@ func (o *searchHubOptions) run(out io.Writer, args []string) error {
return fmt.Errorf("unable to perform search against %q", o.searchEndpoint)
}
return o.outputFormat.Write(out, newHubSearchWriter(results, o.searchEndpoint, o.maxColWidth))
return o.outputFormat.Write(out, newHubSearchWriter(results, o.searchEndpoint, o.maxColWidth, o.listRepoURL))
}
type hubChartRepo struct {
URL string `json:"url"`
Name string `json:"name"`
}
type hubChartElement struct {
URL string `json:"url"`
Version string `json:"version"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
URL string `json:"url"`
Version string `json:"version"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
Repository hubChartRepo `json:"repository"`
}
type hubSearchWriter struct {
elements []hubChartElement
columnWidth uint
listRepoURL bool
}
func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint) *hubSearchWriter {
func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint, listRepoURL bool) *hubSearchWriter {
var elements []hubChartElement
for _, r := range results {
// Backwards compatibility for Monocular
url := endpoint + "/charts/" + r.ID
elements = append(elements, hubChartElement{url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.Description})
// Check for artifactHub compatibility
if r.ArtifactHub.PackageURL != "" {
url = r.ArtifactHub.PackageURL
}
elements = append(elements, hubChartElement{url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.Description, hubChartRepo{URL: r.Attributes.Repo.URL, Name: r.Attributes.Repo.Name}})
}
return &hubSearchWriter{elements, columnWidth}
return &hubSearchWriter{elements, columnWidth, listRepoURL}
}
func (h *hubSearchWriter) WriteTable(out io.Writer) error {
@ -114,9 +139,19 @@ func (h *hubSearchWriter) WriteTable(out io.Writer) error {
}
table := uitable.New()
table.MaxColWidth = h.columnWidth
table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION")
if h.listRepoURL {
table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION", "REPO URL")
} else {
table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION")
}
for _, r := range h.elements {
table.AddRow(r.URL, r.Version, r.AppVersion, r.Description)
if h.listRepoURL {
table.AddRow(r.URL, r.Version, r.AppVersion, r.Description, r.Repository.URL)
} else {
table.AddRow(r.URL, r.Version, r.AppVersion, r.Description)
}
}
return output.EncodeTable(out, table)
}
@ -134,7 +169,7 @@ func (h *hubSearchWriter) encodeByFormat(out io.Writer, format output.Format) er
chartList := make([]hubChartElement, 0, len(h.elements))
for _, r := range h.elements {
chartList = append(chartList, hubChartElement{r.URL, r.Version, r.AppVersion, r.Description})
chartList = append(chartList, hubChartElement{r.URL, r.Version, r.AppVersion, r.Description, r.Repository})
}
switch format {

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

@ -21,7 +21,6 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -144,7 +143,7 @@ func (o *searchRepoOptions) setupSearchedVersion() {
}
func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Result, error) {
if len(o.version) == 0 {
if o.version == "" {
return res, nil
}
@ -156,15 +155,18 @@ func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Res
data := res[:0]
foundNames := map[string]bool{}
for _, r := range res {
if _, found := foundNames[r.Name]; found {
// if not returning all versions and already have found a result,
// you're done!
if !o.versions && foundNames[r.Name] {
continue
}
v, err := semver.NewVersion(r.Chart.Version)
if err != nil || constraint.Check(v) {
if err != nil {
continue
}
if constraint.Check(v) {
data = append(data, r)
if !o.versions {
foundNames[r.Name] = true // If user hasn't requested all versions, only show the latest that matches
}
foundNames[r.Name] = true
}
}
@ -184,7 +186,8 @@ func (o *searchRepoOptions) buildIndex() (*search.Index, error) {
f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n))
ind, err := repo.LoadIndexFile(f)
if err != nil {
fmt.Fprintf(os.Stderr, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n)
warning("Repo %q is corrupt or missing. Try 'helm repo update'.", n)
warning("%s", err)
continue
}
@ -255,7 +258,7 @@ func compListChartsOfRepo(repoName string, prefix string) []string {
var charts []string
path := filepath.Join(settings.RepositoryCache, helmpath.CacheChartsFile(repoName))
content, err := ioutil.ReadFile(path)
content, err := os.ReadFile(path)
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(content))
for scanner.Scan() {
@ -298,23 +301,31 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
// First check completions for repos
repos := compListRepos("", nil)
for _, repo := range repos {
for _, repoInfo := range repos {
// Split name from description
repoInfo := strings.Split(repoInfo, "\t")
repo := repoInfo[0]
repoDesc := ""
if len(repoInfo) > 1 {
repoDesc = repoInfo[1]
}
repoWithSlash := fmt.Sprintf("%s/", repo)
if strings.HasPrefix(toComplete, repoWithSlash) {
// Must complete with charts within the specified repo
completions = append(completions, compListChartsOfRepo(repo, toComplete)...)
// Must complete with charts within the specified repo.
// Don't filter on toComplete to allow for shell fuzzy matching
completions = append(completions, compListChartsOfRepo(repo, "")...)
noSpace = false
break
} else if strings.HasPrefix(repo, toComplete) {
// Must complete the repo name
completions = append(completions, repoWithSlash)
// Must complete the repo name with the slash, followed by the description
completions = append(completions, fmt.Sprintf("%s\t%s", repoWithSlash, repoDesc))
noSpace = true
}
}
cobra.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions), settings.Debug)
// Now handle completions for url prefixes
for _, url := range []string{"https://", "http://", "file://"} {
for _, url := range []string{"oci://\tChart OCI prefix", "https://\tChart URL prefix", "http://\tChart URL prefix", "file://\tChart local URL prefix"} {
if strings.HasPrefix(toComplete, url) {
// The user already put in the full url prefix; we don't have
// anything to add, but make sure the shell does not default
@ -337,7 +348,7 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
// listing the entire content of the current directory which will
// be too many choices for the user to find the real repos)
if includeFiles && len(completions) > 0 && len(toComplete) > 0 {
if files, err := ioutil.ReadDir("."); err == nil {
if files, err := os.ReadDir("."); err == nil {
for _, file := range files {
if strings.HasPrefix(file.Name(), toComplete) {
// We are completing a file prefix
@ -351,7 +362,7 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
// If the user didn't provide any input to completion,
// we provide a hint that a path can also be used
if includeFiles && len(toComplete) == 0 {
completions = append(completions, "./", "/")
completions = append(completions, "./\tRelative path prefix to local chart", "/\tAbsolute path prefix to local chart")
}
cobra.CompDebugln(fmt.Sprintf("Completions after checking empty input: %v", completions), settings.Debug)
@ -361,9 +372,6 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
}
if noSpace {
directive = directive | cobra.ShellCompDirectiveNoSpace
// The cobra.ShellCompDirective flags do not work for zsh right now.
// We handle it ourselves instead.
completions = compEnforceNoSpace(completions)
}
if !includeFiles {
// If we should not include files in the completions,
@ -372,19 +380,3 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
}
return completions, directive
}
// This function prevents the shell from adding a space after
// a completion by adding a second, fake completion.
// It is only needed for zsh, but we cannot tell which shell
// is being used here, so we do the fake completion all the time;
// there are no real downsides to doing this for bash as well.
func compEnforceNoSpace(completions []string) []string {
// To prevent the shell from adding space after the completion,
// we trick it by pretending there is a second, longer match.
// We only do this if there is a single choice for completion.
if len(completions) == 1 {
completions = append(completions, completions[0]+".")
cobra.CompDebugln(fmt.Sprintf("compEnforceNoSpace: completions now are %v", completions), settings.Debug)
}
return completions
}

@ -89,5 +89,5 @@ func TestSearchRepoOutputCompletion(t *testing.T) {
}
func TestSearchRepoFileCompletion(t *testing.T) {
checkFileCompletion(t, "search repo", true) // File completion may be useful when inputing a keyword
checkFileCompletion(t, "search repo", true) // File completion may be useful when inputting a keyword
}

@ -33,7 +33,7 @@ This command consists of multiple subcommands to display information about a cha
const showAllDesc = `
This command inspects a chart (directory, file, or URL) and displays all its content
(values.yaml, Charts.yaml, README)
(values.yaml, Chart.yaml, README)
`
const showValuesDesc = `
@ -43,7 +43,7 @@ of the values.yaml file
const showChartDesc = `
This command inspects a chart (directory, file, or URL) and displays the contents
of the Charts.yaml file
of the Chart.yaml file
`
const readmeChartDesc = `
@ -51,8 +51,13 @@ This command inspects a chart (directory, file, or URL) and displays the content
of the README file
`
func newShowCmd(out io.Writer) *cobra.Command {
client := action.NewShow(action.ShowAll)
const showCRDsDesc = `
This command inspects a chart (directory, file, or URL) and displays the contents
of the CustomResourceDefinition files
`
func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewShowWithConfig(action.ShowAll, cfg)
showCommand := &cobra.Command{
Use: "show",
@ -79,6 +84,10 @@ func newShowCmd(out io.Writer) *cobra.Command {
ValidArgsFunction: validArgsFunc,
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowAll
err := addRegistryClient(client)
if err != nil {
return err
}
output, err := runShow(args, client)
if err != nil {
return err
@ -96,6 +105,10 @@ func newShowCmd(out io.Writer) *cobra.Command {
ValidArgsFunction: validArgsFunc,
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowValues
err := addRegistryClient(client)
if err != nil {
return err
}
output, err := runShow(args, client)
if err != nil {
return err
@ -113,6 +126,10 @@ func newShowCmd(out io.Writer) *cobra.Command {
ValidArgsFunction: validArgsFunc,
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowChart
err := addRegistryClient(client)
if err != nil {
return err
}
output, err := runShow(args, client)
if err != nil {
return err
@ -130,6 +147,31 @@ func newShowCmd(out io.Writer) *cobra.Command {
ValidArgsFunction: validArgsFunc,
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowReadme
err := addRegistryClient(client)
if err != nil {
return err
}
output, err := runShow(args, client)
if err != nil {
return err
}
fmt.Fprint(out, output)
return nil
},
}
crdsSubCmd := &cobra.Command{
Use: "crds [CHART]",
Short: "show the chart's CRDs",
Long: showCRDsDesc,
Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc,
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowCRDs
err := addRegistryClient(client)
if err != nil {
return err
}
output, err := runShow(args, client)
if err != nil {
return err
@ -139,7 +181,7 @@ func newShowCmd(out io.Writer) *cobra.Command {
},
}
cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd}
cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd, crdsSubCmd}
for _, subCmd := range cmds {
addShowFlags(subCmd, client)
showCommand.AddCommand(subCmd)
@ -182,3 +224,13 @@ func runShow(args []string, client *action.Show) (string, error) {
}
return client.Run(cp)
}
func addRegistryClient(client *action.Show) error {
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP)
if err != nil {
return fmt.Errorf("missing registry client: %w", err)
}
client.SetRegistryClient(registryClient)
return nil
}

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

Loading…
Cancel
Save