Merge branch 'main' into helm-test-output-format

pull/9677/head
Simon Alling 4 years ago
commit ad1a8a7d8d

@ -5,7 +5,7 @@ jobs:
build: build:
working_directory: ~/helm.sh/helm working_directory: ~/helm.sh/helm
docker: docker:
- image: circleci/golang:1.16 - image: circleci/golang:1.17
auth: auth:
username: $DOCKER_USER username: $DOCKER_USER
@ -26,6 +26,9 @@ jobs:
- run: - run:
name: test name: test
command: make test-coverage command: make test-coverage
- run:
name: test build
command: make
- deploy: - deploy:
name: deploy name: deploy
command: .circleci/deploy.sh command: .circleci/deploy.sh

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

@ -1,5 +1,5 @@
run: run:
timeout: 2m timeout: 10m
linters: linters:
disable-all: true disable-all: true

@ -113,7 +113,7 @@ test-coverage:
.PHONY: test-style .PHONY: test-style
test-style: test-style:
GO111MODULE=on golangci-lint run --timeout 5m0s GO111MODULE=on golangci-lint run
@scripts/validate-license.sh @scripts/validate-license.sh
.PHONY: test-acceptance .PHONY: test-acceptance

@ -1,16 +1,19 @@
maintainers: maintainers:
- adamreese - adamreese
- bacongobbler - bacongobbler
- fibonacci1729
- hickeyma - hickeyma
- jdolitsky - jdolitsky
- marckhouzam - marckhouzam
- mattfarina - mattfarina
- prydonius - prydonius
- scottrigby
- SlickNik - SlickNik
- technosophos - technosophos
- viglesiasce triage:
- yxxhero
- zonggen
emeritus: emeritus:
- fibonacci1729
- jascott1 - jascott1
- michelleN - michelleN
- migmartri - migmartri
@ -19,3 +22,4 @@ emeritus:
- seh - seh
- thomastaylor312 - thomastaylor312
- vaikas-google - vaikas-google
- viglesiasce

@ -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)
},
}
}

@ -33,37 +33,55 @@ const bashCompDesc = `
Generate the autocompletion script for Helm for the bash shell. Generate the autocompletion script for Helm for the bash shell.
To load completions in your current shell session: 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: To load completions for every new session, execute once:
Linux: - Linux:
$ helm completion bash > /etc/bash_completion.d/helm
MacOS: helm completion bash > /etc/bash_completion.d/helm
$ helm completion bash > /usr/local/etc/bash_completion.d/helm
- MacOS:
helm completion bash > /usr/local/etc/bash_completion.d/helm
` `
const zshCompDesc = ` const zshCompDesc = `
Generate the autocompletion script for Helm for the zsh shell. Generate the autocompletion script for Helm for the zsh shell.
To load completions in your current shell session: 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: To load completions for every new session, execute once:
$ helm completion zsh > "${fpath[1]}/_helm"
helm completion zsh > "${fpath[1]}/_helm"
` `
const fishCompDesc = ` const fishCompDesc = `
Generate the autocompletion script for Helm for the fish shell. Generate the autocompletion script for Helm for the fish shell.
To load completions in your current shell session: 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: 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. 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 ( const (
noDescFlagName = "no-descriptions" noDescFlagName = "no-descriptions"
noDescFlagText = "disable completion descriptions" noDescFlagText = "disable completion descriptions"
@ -80,16 +98,16 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
} }
bash := &cobra.Command{ bash := &cobra.Command{
Use: "bash", Use: "bash",
Short: "generate autocompletion script for bash", Short: "generate autocompletion script for bash",
Long: bashCompDesc, Long: bashCompDesc,
Args: require.NoArgs, Args: require.NoArgs,
DisableFlagsInUseLine: true, ValidArgsFunction: noCompletions,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionBash(out, cmd) return runCompletionBash(out, cmd)
}, },
} }
bash.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
zsh := &cobra.Command{ zsh := &cobra.Command{
Use: "zsh", Use: "zsh",
@ -115,13 +133,25 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
} }
fish.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText) 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 return cmd
} }
func runCompletionBash(out io.Writer, cmd *cobra.Command) error { 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 // 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 // both helm2 and helm3), we hook the new binary name to the completion function
@ -172,6 +202,13 @@ func runCompletionFish(out io.Writer, cmd *cobra.Command) error {
return cmd.Root().GenFishCompletion(out, !disableCompDescriptions) 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 // Function to disable file completion
func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp

@ -100,7 +100,6 @@ func newDependencyCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
func newDependencyListCmd(out io.Writer) *cobra.Command { func newDependencyListCmd(out io.Writer) *cobra.Command {
client := action.NewDependency() client := action.NewDependency()
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list CHART", Use: "list CHART",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
@ -115,5 +114,9 @@ func newDependencyListCmd(out io.Writer) *cobra.Command {
return client.List(chartpath, out) return client.List(chartpath, out)
}, },
} }
f := cmd.Flags()
f.UintVar(&client.ColumnWidth, "max-col-width", 80, "maximum column width for output table")
return cmd return cmd
} }

@ -45,7 +45,7 @@ func TestDependencyBuildCmd(t *testing.T) {
ociChartName := "oci-depending-chart" ociChartName := "oci-depending-chart"
c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL) c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL)
if err := chartutil.SaveDir(c, ociSrv.Dir); err != nil { if _, err := chartutil.Save(c, ociSrv.Dir); err != nil {
t.Fatal(err) t.Fatal(err)
} }
ociSrv.Run(t, repotest.WithDependingChart(c)) ociSrv.Run(t, repotest.WithDependingChart(c))
@ -136,6 +136,9 @@ func TestDependencyBuildCmd(t *testing.T) {
} }
// OCI dependencies // 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", cmd = fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json",
dir(ociChartName), dir(ociChartName),
dir("repositories.yaml"), dir("repositories.yaml"),

@ -17,7 +17,6 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -47,7 +46,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
ociChartName := "oci-depending-chart" ociChartName := "oci-depending-chart"
c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL) c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL)
if err := chartutil.SaveDir(c, ociSrv.Dir); err != nil { if _, err := chartutil.Save(c, ociSrv.Dir); err != nil {
t.Fatal(err) t.Fatal(err)
} }
ociSrv.Run(t, repotest.WithDependingChart(c)) ociSrv.Run(t, repotest.WithDependingChart(c))
@ -134,6 +133,9 @@ func TestDependencyUpdateCmd(t *testing.T) {
} }
// test for OCI charts // 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", cmd := fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json",
dir(ociChartName), dir(ociChartName),
dir("repositories.yaml"), dir("repositories.yaml"),
@ -188,7 +190,7 @@ func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) {
} }
// Make sure charts dir still has dependencies // 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -57,6 +57,7 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file") f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.BoolVar(&c.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download") f.BoolVar(&c.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains")
} }
// bindOutputFlag will add the output flag to the given command and bind the // bindOutputFlag will add the output flag to the given command and bind the

@ -32,6 +32,7 @@ import (
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/gates" "helm.sh/helm/v3/pkg/gates"
"helm.sh/helm/v3/pkg/kube"
kubefake "helm.sh/helm/v3/pkg/kube/fake" kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver" "helm.sh/helm/v3/pkg/storage/driver"
@ -59,6 +60,12 @@ func warning(format string, v ...interface{}) {
} }
func main() { func main() {
// Setting the name of the app for managedFields in the Kubernetes client.
// It is set here to the full name of "helm" so that renaming of helm to
// another name (e.g., helm2 or helm3) does not change the name of the
// manager as picked up by the automated name detection.
kube.ManagedFieldsManager = "helm"
actionConfig := new(action.Configuration) actionConfig := new(action.Configuration)
cmd, err := newRootCmd(actionConfig, os.Stdout, os.Args[1:]) cmd, err := newRootCmd(actionConfig, os.Stdout, os.Args[1:])
if err != nil { if err != nil {

@ -17,8 +17,13 @@ limitations under the License.
package main package main
import ( import (
"context"
"fmt"
"io" "io"
"log" "log"
"os"
"os/signal"
"syscall"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -119,7 +124,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
rel, err := runInstall(args, client, valueOpts, out) rel, err := runInstall(args, client, valueOpts, out)
if err != nil { 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})
@ -145,7 +150,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
f.StringVar(&client.NameTemplate, "name-template", "", "specify template used to name the release") f.StringVar(&client.NameTemplate, "name-template", "", "specify template used to name the release")
f.StringVar(&client.Description, "description", "", "add a custom description") 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.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.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.Atomic, "atomic", false, "if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used")
f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed. By default, CRDs are installed if not already present") f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed. By default, CRDs are installed if not already present")
@ -182,6 +187,10 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
} }
client.ReleaseName = name client.ReleaseName = name
if err := checkOCI(chart); err != nil {
return nil, err
}
cp, err := client.ChartPathOptions.LocateChart(chart, settings) cp, err := client.ChartPathOptions.LocateChart(chart, settings)
if err != nil { if err != nil {
return nil, err return nil, err
@ -214,6 +223,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
// As of Helm 2.4.0, this is treated as a stopping condition: // As of Helm 2.4.0, this is treated as a stopping condition:
// https://github.com/helm/helm/issues/2209 // https://github.com/helm/helm/issues/2209
if err := action.CheckDependencies(chartRequested, req); err != nil { 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 { if client.DependencyUpdate {
man := &downloader.Manager{ man := &downloader.Manager{
Out: out, Out: out,
@ -239,7 +249,23 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
} }
client.Namespace = settings.Namespace() client.Namespace = settings.Namespace()
return client.Run(chartRequested, vals)
// Create context and prepare the handle of SIGTERM
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
// Set up channel on which to send signal notifications.
// We must use a buffered channel or risk missing the signal
// if we're not ready to receive when the signal is sent.
cSignal := make(chan os.Signal, 2)
signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM)
go func() {
<-cSignal
fmt.Fprintf(out, "Release %s has been cancelled.\n", args[0])
cancel()
}()
return client.RunWithContext(ctx, chartRequested, vals)
} }
// checkIfInstallable validates if a chart can be installed // checkIfInstallable validates if a chart can be installed

@ -18,10 +18,39 @@ package main
import ( import (
"fmt" "fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"testing" "testing"
"helm.sh/helm/v3/pkg/repo/repotest"
) )
func TestInstall(t *testing.T) { 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{ tests := []cmdTestCase{
// Install, base case // Install, base case
{ {
@ -140,7 +169,7 @@ func TestInstall(t *testing.T) {
name: "install library chart", name: "install library chart",
cmd: "install libchart testdata/testcharts/lib-chart", cmd: "install libchart testdata/testcharts/lib-chart",
wantError: true, wantError: true,
golden: "output/template-lib-chart.txt", golden: "output/install-lib-chart.txt",
}, },
// Install, chart with bad type // Install, chart with bad type
{ {
@ -207,6 +236,22 @@ func TestInstall(t *testing.T) {
name: "install chart with only crds", name: "install chart with only crds",
cmd: "install crd-test testdata/testcharts/chart-with-only-crds --namespace default", 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) runTestActionCmd(t, tests)

@ -157,8 +157,8 @@ func newReleaseListWriter(releases []*release.Release, timeFormat string) *relea
Namespace: r.Namespace, Namespace: r.Namespace,
Revision: strconv.Itoa(r.Version), Revision: strconv.Itoa(r.Version),
Status: r.Info.Status.String(), Status: r.Info.Status.String(),
Chart: fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version), Chart: formatChartname(r.Chart),
AppVersion: r.Chart.Metadata.AppVersion, AppVersion: formatAppVersion(r.Chart),
} }
t := "-" t := "-"

@ -277,11 +277,6 @@ func TestPluginDynamicCompletion(t *testing.T) {
cmd: "__complete echo -n mynamespace ''", cmd: "__complete echo -n mynamespace ''",
golden: "output/plugin_echo_no_directive.txt", golden: "output/plugin_echo_no_directive.txt",
rels: []*release.Release{}, 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 { for _, test := range tests {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins" settings.PluginsDirectory = "testdata/helmhome/helm/plugins"

@ -20,7 +20,6 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -65,10 +64,8 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.Version = ">0.0.0-0" client.Version = ">0.0.0-0"
} }
if strings.HasPrefix(args[0], "oci://") { if err := checkOCI(args[0]); err != nil {
if !FeatureGateOCI.IsEnabled() { return err
return FeatureGateOCI.Error()
}
} }
for i := 0; i < len(args); i++ { for i := 0; i < len(args); i++ {

@ -18,6 +18,8 @@ package main
import ( import (
"fmt" "fmt"
"net/http"
"net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -250,6 +252,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) { func TestPullVersionCompletion(t *testing.T) {
repoFile := "testdata/helmhome/helm/repositories.yaml" repoFile := "testdata/helmhome/helm/repositories.yaml"
repoCache := "testdata/helmhome/helm/repository" repoCache := "testdata/helmhome/helm/repository"

@ -0,0 +1,61 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"io"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
experimental "helm.sh/helm/v3/internal/experimental/action"
"helm.sh/helm/v3/pkg/action"
)
const pushDesc = `
Upload a chart to a registry.
If the chart has an associated provenance file,
it will also be uploaded.
`
func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := experimental.NewPushWithOpts(experimental.WithPushConfig(cfg))
cmd := &cobra.Command{
Use: "push [chart] [remote]",
Short: "push a chart to remote",
Long: pushDesc,
Hidden: !FeatureGateOCI.IsEnabled(),
PersistentPreRunE: checkOCIFeatureGate(),
Args: require.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
chartRef := args[0]
remote := args[1]
client.Settings = settings
output, err := client.Run(chartRef, remote)
if err != nil {
return err
}
fmt.Fprint(out, output)
return nil
},
}
return cmd
}

@ -25,10 +25,11 @@ import (
"os" "os"
"strings" "strings"
"github.com/docker/docker/pkg/term" "github.com/docker/docker/pkg/term" //nolint
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/cmd/helm/require"
experimental "helm.sh/helm/v3/internal/experimental/action"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/action"
) )
@ -54,7 +55,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
return err return err
} }
return action.NewRegistryLogin(cfg).Run(out, hostname, username, password, insecureOpt) return experimental.NewRegistryLogin(cfg).Run(out, hostname, username, password, insecureOpt)
}, },
} }
@ -67,7 +68,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
return cmd 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) { func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) {
var err error var err error
username := usernameOpt username := usernameOpt
@ -110,7 +111,7 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd
return username, password, nil 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) { func readLine(prompt string, silent bool) (string, error) {
fmt.Print(prompt) fmt.Print(prompt)
if silent { if silent {

@ -22,6 +22,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/cmd/helm/require"
experimental "helm.sh/helm/v3/internal/experimental/action"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/action"
) )
@ -38,7 +39,7 @@ func newRegistryLogoutCmd(cfg *action.Configuration, out io.Writer) *cobra.Comma
Hidden: !FeatureGateOCI.IsEnabled(), Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
hostname := args[0] hostname := args[0]
return action.NewRegistryLogout(cfg).Run(out, hostname) return experimental.NewRegistryLogout(cfg).Run(out, hostname)
}, },
} }
} }

@ -48,6 +48,8 @@ type repoAddOptions struct {
url string url string
username string username string
password string password string
passwordFromStdinOpt bool
passCredentialsAll bool
forceUpdate bool forceUpdate bool
allowDeprecatedRepos bool allowDeprecatedRepos bool
@ -84,6 +86,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.StringVar(&o.username, "username", "", "chart repository username") f.StringVar(&o.username, "username", "", "chart repository username")
f.StringVar(&o.password, "password", "", "chart repository password") f.StringVar(&o.password, "password", "", "chart repository password")
f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read chart repository password from stdin")
f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists") f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists")
f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.") f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.")
f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
@ -91,6 +94,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository") 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.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 return cmd
} }
@ -112,7 +116,14 @@ func (o *repoAddOptions) run(out io.Writer) error {
} }
// Acquire a file lock for process synchronization // 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.Replace(o.repoFile, repoFileExt, ".lock", 1)
} else {
lockPath = o.repoFile + ".lock"
}
fileLock := flock.New(lockPath)
lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
locked, err := fileLock.TryLockContext(lockCtx, time.Second) locked, err := fileLock.TryLockContext(lockCtx, time.Second)
@ -134,14 +145,24 @@ func (o *repoAddOptions) run(out io.Writer) error {
} }
if o.username != "" && o.password == "" { if o.username != "" && o.password == "" {
fd := int(os.Stdin.Fd()) if o.passwordFromStdinOpt {
fmt.Fprint(out, "Password: ") passwordFromStdin, err := io.ReadAll(os.Stdin)
password, err := term.ReadPassword(fd) if err != nil {
fmt.Fprintln(out) return err
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{ c := repo.Entry{
@ -149,6 +170,7 @@ func (o *repoAddOptions) run(out io.Writer) error {
URL: o.url, URL: o.url,
Username: o.username, Username: o.username,
Password: o.password, Password: o.password,
PassCredentialsAll: o.passCredentialsAll,
CertFile: o.certFile, CertFile: o.certFile,
KeyFile: o.keyFile, KeyFile: o.keyFile,
CAFile: o.caFile, CAFile: o.caFile,

@ -21,6 +21,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"testing" "testing"
@ -142,6 +143,18 @@ func TestRepoAddConcurrentDirNotExist(t *testing.T) {
repoAddConcurrent(t, testName, repoFile) 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) { func repoAddConcurrent(t *testing.T, testName, repoFile string) {
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil { if err != nil {
@ -192,3 +205,33 @@ func TestRepoAddFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo add reponame", false) checkFileCompletion(t, "repo add reponame", false)
checkFileCompletion(t, "repo add reponame https://example.com", 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,6 +17,7 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"io" "io"
"strings" "strings"
@ -131,7 +132,7 @@ func compListRepos(prefix string, ignoredRepoNames []string) []string {
filteredRepos := filterRepos(f.Repositories, ignoredRepoNames) filteredRepos := filterRepos(f.Repositories, ignoredRepoNames)
for _, repo := range filteredRepos { for _, repo := range filteredRepos {
if strings.HasPrefix(repo.Name, prefix) { if strings.HasPrefix(repo.Name, prefix) {
rNames = append(rNames, repo.Name) rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL))
} }
} }
} }

@ -18,6 +18,7 @@ package main
import ( import (
"bytes" "bytes"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -161,6 +162,51 @@ 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 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) { func TestRepoRemoveFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo remove", false) checkFileCompletion(t, "repo remove", false)
checkFileCompletion(t, "repo remove repo1", false) checkFileCompletion(t, "repo remove repo1", false)

@ -32,32 +32,48 @@ import (
const updateDesc = ` const updateDesc = `
Update gets the latest information about charts from the respective chart repositories. 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'. 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") var errNoRepositories = errors.New("no repositories found. You must add one before updating")
type repoUpdateOptions struct { type repoUpdateOptions struct {
update func([]*repo.ChartRepository, io.Writer) update func([]*repo.ChartRepository, io.Writer, bool) error
repoFile string repoFile string
repoCache string repoCache string
names []string
failOnRepoUpdateFail bool
} }
func newRepoUpdateCmd(out io.Writer) *cobra.Command { func newRepoUpdateCmd(out io.Writer) *cobra.Command {
o := &repoUpdateOptions{update: updateCharts} o := &repoUpdateOptions{update: updateCharts}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "update", Use: "update [REPO1 [REPO2 ...]]",
Aliases: []string{"up"}, Aliases: []string{"up"},
Short: "update information of available charts locally from chart repositories", Short: "update information of available charts locally from chart repositories",
Long: updateDesc, Long: updateDesc,
Args: require.NoArgs, Args: require.MinimumNArgs(0),
ValidArgsFunction: noCompletions, 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 { RunE: func(cmd *cobra.Command, args []string) error {
o.repoFile = settings.RepositoryConfig o.repoFile = settings.RepositoryConfig
o.repoCache = settings.RepositoryCache o.repoCache = settings.RepositoryCache
o.names = args
return o.run(out) 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 return cmd
} }
@ -73,35 +89,79 @@ func (o *repoUpdateOptions) run(out io.Writer) error {
} }
var repos []*repo.ChartRepository var repos []*repo.ChartRepository
for _, cfg := range f.Repositories { updateAllRepos := len(o.names) == 0
r, err := repo.NewChartRepository(cfg, getter.All(settings))
if err != nil { if !updateAllRepos {
// Fail early if the user specified an invalid repo to update
if err := checkRequestedRepos(o.names, f.Repositories); err != nil {
return err 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 o.update(repos, out, o.failOnRepoUpdateFail)
return nil
} }
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...") fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup var wg sync.WaitGroup
var repoFailList []string
for _, re := range repos { for _, re := range repos {
wg.Add(1) wg.Add(1)
go func(re *repo.ChartRepository) { go func(re *repo.ChartRepository) {
defer wg.Done() defer wg.Done()
if _, err := re.DownloadIndexFile(); err != nil { if _, err := re.DownloadIndexFile(); err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err) 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 { } else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name) fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name)
} }
}(re) }(re)
} }
wg.Wait() wg.Wait()
if len(repoFailList) > 0 && failOnRepoUpdateFail {
return errors.New(fmt.Sprintf("Failed to update the following repositories: %s",
repoFailList))
}
fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") 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
} }

@ -35,10 +35,11 @@ func TestUpdateCmd(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test. // Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently. // The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer) { updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos { for _, re := range repos {
fmt.Fprintln(out, re.Config.Name) fmt.Fprintln(out, re.Config.Name)
} }
return nil
} }
o := &repoUpdateOptions{ o := &repoUpdateOptions{
update: updater, update: updater,
@ -48,8 +49,56 @@ func TestUpdateCmd(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if got := out.String(); !strings.Contains(got, "charts") { if got := out.String(); !strings.Contains(got, "charts") ||
t.Errorf("Expected 'charts' got %q", got) !strings.Contains(got, "firstexample") ||
!strings.Contains(got, "secondexample") {
t.Errorf("Expected 'charts', 'firstexample' and 'secondexample' but got %q", got)
}
}
func TestUpdateCmdMultiple(t *testing.T) {
var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos {
fmt.Fprintln(out, re.Config.Name)
}
return nil
}
o := &repoUpdateOptions{
update: updater,
repoFile: "testdata/repositories.yaml",
names: []string{"firstexample", "charts"},
}
if err := o.run(&out); err != nil {
t.Fatal(err)
}
if got := out.String(); !strings.Contains(got, "charts") ||
!strings.Contains(got, "firstexample") ||
strings.Contains(got, "secondexample") {
t.Errorf("Expected 'charts' and 'firstexample' but not 'secondexample' but got %q", got)
}
}
func TestUpdateCmdInvalid(t *testing.T) {
var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos {
fmt.Fprintln(out, re.Config.Name)
}
return nil
}
o := &repoUpdateOptions{
update: updater,
repoFile: "testdata/repositories.yaml",
names: []string{"firstexample", "invalid"},
}
if err := o.run(&out); err == nil {
t.Fatal("expected error but did not get one")
} }
} }
@ -98,7 +147,7 @@ func TestUpdateCharts(t *testing.T) {
} }
b := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)
updateCharts([]*repo.ChartRepository{r}, b) updateCharts([]*repo.ChartRepository{r}, b, false)
got := b.String() got := b.String()
if strings.Contains(got, "Unable to get an update") { if strings.Contains(got, "Unable to get an update") {
@ -111,4 +160,81 @@ func TestUpdateCharts(t *testing.T) {
func TestRepoUpdateFileCompletion(t *testing.T) { func TestRepoUpdateFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo update", false) 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")
}
} }

@ -51,7 +51,7 @@ Environment variables:
| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. | | $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. |
| $HELM_DATA_HOME | set an alternative location for storing Helm data. | | $HELM_DATA_HOME | set an alternative location for storing Helm data. |
| $HELM_DEBUG | indicate whether or not Helm is running in Debug mode | | $HELM_DEBUG | indicate whether or not Helm is running in Debug mode |
| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, postgres | | $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. |
| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. | | $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. |
| $HELM_MAX_HISTORY | set the maximum number of helm release history. | | $HELM_MAX_HISTORY | set the maximum number of helm release history. |
| $HELM_NAMESPACE | set the namespace used for the helm operations. | | $HELM_NAMESPACE | set the namespace used for the helm operations. |
@ -200,7 +200,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
// Add *experimental* subcommands // Add *experimental* subcommands
cmd.AddCommand( cmd.AddCommand(
newRegistryCmd(actionConfig, out), newRegistryCmd(actionConfig, out),
newChartCmd(actionConfig, out), newPushCmd(actionConfig, out),
) )
// Find and add plugins // Find and add plugins
@ -262,3 +262,12 @@ func checkForExpiredRepos(repofile string) {
} }
} }
// When dealing with OCI-based charts, ensure that the user has
// enabled the experimental feature gate prior to continuing
func checkOCI(ref string) error {
if registry.IsOCI(ref) && !FeatureGateOCI.IsEnabled() {
return FeatureGateOCI.Error()
}
return nil
}

@ -1,3 +1,4 @@
//go:build !windows
// +build !windows // +build !windows
/* /*

@ -1,3 +1,4 @@
//go:build !windows
// +build !windows // +build !windows
/* /*

@ -22,6 +22,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -301,7 +302,14 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
// First check completions for repos // First check completions for repos
repos := compListRepos("", nil) 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) repoWithSlash := fmt.Sprintf("%s/", repo)
if strings.HasPrefix(toComplete, repoWithSlash) { if strings.HasPrefix(toComplete, repoWithSlash) {
// Must complete with charts within the specified repo // Must complete with charts within the specified repo
@ -309,15 +317,15 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
noSpace = false noSpace = false
break break
} else if strings.HasPrefix(repo, toComplete) { } else if strings.HasPrefix(repo, toComplete) {
// Must complete the repo name // Must complete the repo name with the slash, followed by the description
completions = append(completions, repoWithSlash) completions = append(completions, fmt.Sprintf("%s\t%s", repoWithSlash, repoDesc))
noSpace = true noSpace = true
} }
} }
cobra.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions), settings.Debug) cobra.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions), settings.Debug)
// Now handle completions for url prefixes // Now handle completions for url prefixes
for _, url := range []string{"https://", "http://", "file://"} { for _, url := range []string{"https://\tChart URL prefix", "http://\tChart URL prefix", "file://\tChart local URL prefix"} {
if strings.HasPrefix(toComplete, url) { if strings.HasPrefix(toComplete, url) {
// The user already put in the full url prefix; we don't have // The user already put in the full url prefix; we don't have
// anything to add, but make sure the shell does not default // anything to add, but make sure the shell does not default
@ -340,7 +348,7 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
// listing the entire content of the current directory which will // listing the entire content of the current directory which will
// be too many choices for the user to find the real repos) // be too many choices for the user to find the real repos)
if includeFiles && len(completions) > 0 && len(toComplete) > 0 { 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 { for _, file := range files {
if strings.HasPrefix(file.Name(), toComplete) { if strings.HasPrefix(file.Name(), toComplete) {
// We are completing a file prefix // We are completing a file prefix
@ -354,7 +362,7 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
// If the user didn't provide any input to completion, // If the user didn't provide any input to completion,
// we provide a hint that a path can also be used // we provide a hint that a path can also be used
if includeFiles && len(toComplete) == 0 { 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) cobra.CompDebugln(fmt.Sprintf("Completions after checking empty input: %v", completions), settings.Debug)

@ -33,7 +33,7 @@ This command consists of multiple subcommands to display information about a cha
const showAllDesc = ` const showAllDesc = `
This command inspects a chart (directory, file, or URL) and displays all its content 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 = ` const showValuesDesc = `
@ -43,7 +43,7 @@ of the values.yaml file
const showChartDesc = ` const showChartDesc = `
This command inspects a chart (directory, file, or URL) and displays the contents 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 = ` const readmeChartDesc = `
@ -51,6 +51,11 @@ This command inspects a chart (directory, file, or URL) and displays the content
of the README file of the README file
` `
const showCRDsDesc = `
This command inspects a chart (directory, file, or URL) and displays the contents
of the CustomResourceDefintion files
`
func newShowCmd(out io.Writer) *cobra.Command { func newShowCmd(out io.Writer) *cobra.Command {
client := action.NewShow(action.ShowAll) client := action.NewShow(action.ShowAll)
@ -139,7 +144,24 @@ func newShowCmd(out io.Writer) *cobra.Command {
}, },
} }
cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd} 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
output, err := runShow(args, client)
if err != nil {
return err
}
fmt.Fprint(out, output)
return nil
},
}
cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd, crdsSubCmd}
for _, subCmd := range cmds { for _, subCmd := range cmds {
addShowFlags(subCmd, client) addShowFlags(subCmd, client)
showCommand.AddCommand(subCmd) showCommand.AddCommand(subCmd)
@ -176,6 +198,10 @@ func runShow(args []string, client *action.Show) (string, error) {
client.Version = ">0.0.0-0" client.Version = ">0.0.0-0"
} }
if err := checkOCI(args[0]); err != nil {
return "", err
}
cp, err := client.ChartPathOptions.LocateChart(args[0], settings) cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
if err != nil { if err != nil {
return "", err return "", err

@ -145,3 +145,7 @@ func TestShowReadmeFileCompletion(t *testing.T) {
func TestShowValuesFileCompletion(t *testing.T) { func TestShowValuesFileCompletion(t *testing.T) {
checkFileCompletion(t, "show values", true) checkFileCompletion(t, "show values", true)
} }
func TestShowCRDsFileCompletion(t *testing.T) {
checkFileCompletion(t, "show crds", true)
}

@ -52,6 +52,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
var skipTests bool var skipTests bool
client := action.NewInstall(cfg) client := action.NewInstall(cfg)
valueOpts := &values.Options{} valueOpts := &values.Options{}
var kubeVersion string
var extraAPIs []string var extraAPIs []string
var showFiles []string var showFiles []string
@ -64,6 +65,14 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return compInstall(args, toComplete, client) return compInstall(args, toComplete, client)
}, },
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
if kubeVersion != "" {
parsedKubeVersion, err := chartutil.ParseKubeVersion(kubeVersion)
if err != nil {
return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err)
}
client.KubeVersion = parsedKubeVersion
}
client.DryRun = true client.DryRun = true
client.ReleaseName = "RELEASE-NAME" client.ReleaseName = "RELEASE-NAME"
client.Replace = true // Skip the name check client.Replace = true // Skip the name check
@ -171,6 +180,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&includeCrds, "include-crds", false, "include CRDs in the templated output") f.BoolVar(&includeCrds, "include-crds", false, "include CRDs in the templated output")
f.BoolVar(&skipTests, "skip-tests", false, "skip tests from templated output") f.BoolVar(&skipTests, "skip-tests", false, "skip tests from templated output")
f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall") f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall")
f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")
f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions") f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions")
f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.") f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
bindPostRenderFlag(cmd, &client.PostRenderer) bindPostRenderFlag(cmd, &client.PostRenderer)

@ -62,7 +62,7 @@ func TestTemplateCmd(t *testing.T) {
name: "check chart bad type", name: "check chart bad type",
cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-bad-type"), cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-bad-type"),
wantError: true, wantError: true,
golden: "output/install-chart-bad-type.txt", golden: "output/template-chart-bad-type.txt",
}, },
{ {
name: "check chart with dependency which is an app chart acting as a library chart", name: "check chart with dependency which is an app chart acting as a library chart",
@ -74,6 +74,11 @@ func TestTemplateCmd(t *testing.T) {
cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-with-template-lib-archive-dep"), cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-with-template-lib-archive-dep"),
golden: "output/template-chart-with-template-lib-archive-dep.txt", golden: "output/template-chart-with-template-lib-archive-dep.txt",
}, },
{
name: "check kube version",
cmd: fmt.Sprintf("template --kube-version 1.16.0 '%s'", chartPath),
golden: "output/template-with-kube-version.txt",
},
{ {
name: "check kube api versions", name: "check kube api versions",
cmd: fmt.Sprintf("template --api-versions helm.k8s.io/test '%s'", chartPath), cmd: fmt.Sprintf("template --api-versions helm.k8s.io/test '%s'", chartPath),

@ -7,8 +7,7 @@ echo "Args received: ${@}"
# Final printout is the optional completion directive of the form :<directive> # Final printout is the optional completion directive of the form :<directive>
if [ "$HELM_NAMESPACE" = "default" ]; then if [ "$HELM_NAMESPACE" = "default" ]; then
# Output an invalid directive, which should be ignored echo ":0"
echo ":2222"
# else # else
# Don't include the directive, to test it is really optional # Don't include the directive, to test it is really optional
fi fi

@ -1 +1 @@
Error: validation: chart.metadata.type must be application or library Error: INSTALLATION FAILED: validation: chart.metadata.type must be application or library

@ -0,0 +1 @@
Error: INSTALLATION FAILED: validation: chart.metadata.type must be application or library

@ -1,6 +0,0 @@
echo plugin.complete was called
Namespace: default
Num args received: 1
Args received:
:0
Completion ended with directive: ShellCompDirectiveDefault

@ -0,0 +1,5 @@
foo
bar
baz
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -0,0 +1,4 @@
bar
baz
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -1,4 +1,4 @@
Error: values don't meet the specifications of the schema(s) in the following chart(s): Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s):
empty: empty:
- age: Must be greater than or equal to 0 - age: Must be greater than or equal to 0

@ -1,4 +1,4 @@
Error: values don't meet the specifications of the schema(s) in the following chart(s): Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s):
empty: empty:
- (root): employmentInfo is required - (root): employmentInfo is required
- age: Must be greater than or equal to 0 - age: Must be greater than or equal to 0

@ -1,4 +1,4 @@
Error: values don't meet the specifications of the schema(s) in the following chart(s): Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s):
subchart-with-schema: subchart-with-schema:
- age: Must be greater than or equal to 0 - age: Must be greater than or equal to 0

@ -1,4 +1,4 @@
Error: values don't meet the specifications of the schema(s) in the following chart(s): Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s):
chart-without-schema: chart-without-schema:
- (root): lastname is required - (root): lastname is required
subchart-with-schema: subchart-with-schema:

@ -0,0 +1 @@
Error: validation: chart.metadata.type must be application or library

@ -10,7 +10,6 @@ metadata:
kube-version/major: "1" kube-version/major: "1"
kube-version/minor: "20" kube-version/minor: "20"
kube-version/version: "v1.20.0" kube-version/version: "v1.20.0"
kube-api-version/test: v1
spec: spec:
type: ClusterIP type: ClusterIP
ports: ports:

@ -10,7 +10,6 @@ metadata:
kube-version/major: "1" kube-version/major: "1"
kube-version/minor: "20" kube-version/minor: "20"
kube-version/version: "v1.20.0" kube-version/version: "v1.20.0"
kube-api-version/test: v1
spec: spec:
type: ClusterIP type: ClusterIP
ports: ports:

@ -74,7 +74,6 @@ metadata:
kube-version/major: "1" kube-version/major: "1"
kube-version/minor: "20" kube-version/minor: "20"
kube-version/version: "v1.20.0" kube-version/version: "v1.20.0"
kube-api-version/test: v1
spec: spec:
type: ClusterIP type: ClusterIP
ports: ports:

@ -91,7 +91,6 @@ metadata:
kube-version/major: "1" kube-version/major: "1"
kube-version/minor: "20" kube-version/minor: "20"
kube-version/version: "v1.20.0" kube-version/version: "v1.20.0"
kube-api-version/test: v1
spec: spec:
type: ClusterIP type: ClusterIP
ports: ports:

@ -0,0 +1,114 @@
---
# Source: subchart/templates/subdir/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: subchart-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: subchart-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: subchart-role
subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subcharta
labels:
helm.sh/chart: "subcharta-0.1.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subchartb
labels:
helm.sh/chart: "subchartb-0.1.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subchart
labels:
helm.sh/chart: "subchart-0.1.0"
app.kubernetes.io/instance: "RELEASE-NAME"
kube-version/major: "1"
kube-version/minor: "16"
kube-version/version: "v1.16.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: nginx
selector:
app.kubernetes.io/name: subchart
---
# Source: subchart/templates/tests/test-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: "RELEASE-NAME-testconfig"
annotations:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
kind: Pod
metadata:
name: "RELEASE-NAME-test"
annotations:
"helm.sh/hook": test
spec:
containers:
- name: test
image: "alpine:latest"
envFrom:
- configMapRef:
name: "RELEASE-NAME-testconfig"
command:
- echo
- "$message"
restartPolicy: Never

@ -0,0 +1 @@
release "aeneas" uninstalled

@ -0,0 +1,9 @@
Release "funny-bunny" has been upgraded. Happy Helming!
NAME: funny-bunny
LAST DEPLOYED: Fri Sep 2 22:04:05 1977
NAMESPACE: default
STATUS: deployed
REVISION: 3
TEST SUITE: None
NOTES:
PARENT NOTES

@ -1 +1 @@
Error: found in Chart.yaml, but missing in charts/ directory: reqsubchart2 Error: An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: found in Chart.yaml, but missing in charts/ directory: reqsubchart2

@ -1 +1 @@
version.BuildInfo{Version:"v3.5", GitCommit:"", GitTreeState:"", GoVersion:""} version.BuildInfo{Version:"v3.7", GitCommit:"", GitTreeState:"", GoVersion:""}

@ -1 +1 @@
version.BuildInfo{Version:"v3.5", GitCommit:"", GitTreeState:"", GoVersion:""} version.BuildInfo{Version:"v3.7", GitCommit:"", GitTreeState:"", GoVersion:""}

@ -1 +1 @@
Version: v3.5 Version: v3.7

@ -1 +1 @@
version.BuildInfo{Version:"v3.5", GitCommit:"", GitTreeState:"", GoVersion:""} version.BuildInfo{Version:"v3.7", GitCommit:"", GitTreeState:"", GoVersion:""}

@ -0,0 +1 @@
password

@ -2,3 +2,8 @@ apiVersion: v1
repositories: repositories:
- name: charts - name: charts
url: "https://charts.helm.sh/stable" url: "https://charts.helm.sh/stable"
- name: firstexample
url: "http://firstexample.com"
- name: secondexample
url: "http://secondexample.com"

@ -0,0 +1,6 @@
dependencies:
- name: subchart-with-notes
repository: file://../chart-with-subchart-notes/charts/subchart-with-notes
version: 0.0.1
digest: sha256:8ca45f73ae3f6170a09b64a967006e98e13cd91eb51e5ab0599bb87296c7df0a
generated: "2021-05-02T15:07:22.1099921+02:00"

@ -71,6 +71,7 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.DryRun, "dry-run", false, "simulate a uninstall") f.BoolVar(&client.DryRun, "dry-run", false, "simulate a uninstall")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation")
f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history") f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history")
f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all the resources are deleted before returning. It will wait for as long as --timeout")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.StringVar(&client.Description, "description", "", "add a custom description") f.StringVar(&client.Description, "description", "", "add a custom description")

@ -57,6 +57,12 @@ func TestUninstall(t *testing.T) {
golden: "output/uninstall-keep-history.txt", golden: "output/uninstall-keep-history.txt",
rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
}, },
{
name: "wait",
cmd: "uninstall aeneas --wait",
golden: "output/uninstall-wait.txt",
rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})},
},
{ {
name: "uninstall without release", name: "uninstall without release",
cmd: "uninstall", cmd: "uninstall",

@ -17,9 +17,13 @@ limitations under the License.
package main package main
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"log" "log"
"os"
"os/signal"
"syscall"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -30,6 +34,7 @@ import (
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/cli/output"
"helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/cli/values"
"helm.sh/helm/v3/pkg/downloader"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/storage/driver" "helm.sh/helm/v3/pkg/storage/driver"
) )
@ -82,6 +87,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if err := checkOCI(args[1]); err != nil {
return err
}
client.Namespace = settings.Namespace() client.Namespace = settings.Namespace()
// Fixes #7002 - Support reading values from STDIN for `upgrade` command // Fixes #7002 - Support reading values from STDIN for `upgrade` command
@ -132,7 +141,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return err return err
} }
vals, err := valueOpts.MergeValues(getter.All(settings)) p := getter.All(settings)
vals, err := valueOpts.MergeValues(p)
if err != nil { if err != nil {
return err return err
} }
@ -144,7 +154,28 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
if req := ch.Metadata.Dependencies; req != nil { if req := ch.Metadata.Dependencies; req != nil {
if err := action.CheckDependencies(ch, req); err != nil { if err := action.CheckDependencies(ch, req); err != nil {
return err 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,
ChartPath: chartPath,
Keyring: client.ChartPathOptions.Keyring,
SkipUpdate: false,
Getters: p,
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug,
}
if err := man.Update(); err != nil {
return err
}
// Reload the chart with the updated Chart.lock file.
if ch, err = loader.Load(chartPath); err != nil {
return errors.Wrap(err, "failed reloading chart after repo update")
}
} else {
return err
}
} }
} }
@ -152,7 +183,22 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
warning("This chart is deprecated") warning("This chart is deprecated")
} }
rel, err := client.Run(args[0], ch, vals) // Create context and prepare the handle of SIGTERM
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
// Set up channel on which to send signal notifications.
// We must use a buffered channel or risk missing the signal
// if we're not ready to receive when the signal is sent.
cSignal := make(chan os.Signal, 2)
signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM)
go func() {
<-cSignal
fmt.Fprintf(out, "Release %s has been cancelled.\n", args[0])
cancel()
}()
rel, err := client.RunWithContext(ctx, args[0], ch, vals)
if err != nil { if err != nil {
return errors.Wrap(err, "UPGRADE FAILED") return errors.Wrap(err, "UPGRADE FAILED")
} }
@ -186,6 +232,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this upgrade when upgrade fails") f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this upgrade when upgrade fails")
f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent") f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent")
f.StringVar(&client.Description, "description", "", "add a custom description") f.StringVar(&client.Description, "description", "", "add a custom description")
f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart")
addChartPathOptionsFlags(f, &client.ChartPathOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions)
addValueOptionsFlags(f, valueOpts) addValueOptionsFlags(f, valueOpts)
bindOutputFlag(cmd, &outfmt) bindOutputFlag(cmd, &outfmt)

@ -32,6 +32,7 @@ import (
) )
func TestUpgradeCmd(t *testing.T) { func TestUpgradeCmd(t *testing.T) {
tmpChart := ensure.TempDir(t) tmpChart := ensure.TempDir(t)
cfile := &chart.Chart{ cfile := &chart.Chart{
Metadata: &chart.Metadata{ Metadata: &chart.Metadata{
@ -79,6 +80,7 @@ func TestUpgradeCmd(t *testing.T) {
missingDepsPath := "testdata/testcharts/chart-missing-deps" missingDepsPath := "testdata/testcharts/chart-missing-deps"
badDepsPath := "testdata/testcharts/chart-bad-requirements" badDepsPath := "testdata/testcharts/chart-bad-requirements"
presentDepsPath := "testdata/testcharts/chart-with-subchart-update"
relWithStatusMock := func(n string, v int, ch *chart.Chart, status release.Status) *release.Release { relWithStatusMock := func(n string, v int, ch *chart.Chart, status release.Status) *release.Release {
return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch, Status: status}) return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch, Status: status})
@ -149,6 +151,12 @@ func TestUpgradeCmd(t *testing.T) {
golden: "output/upgrade-with-bad-dependencies.txt", golden: "output/upgrade-with-bad-dependencies.txt",
wantError: true, wantError: true,
}, },
{
name: "upgrade a release with resolving missing dependencies",
cmd: fmt.Sprintf("upgrade --dependency-update funny-bunny %s", presentDepsPath),
golden: "output/upgrade-with-dependency-update.txt",
rels: []*release.Release{relMock("funny-bunny", 2, ch2)},
},
{ {
name: "upgrade a non-existent release", name: "upgrade a non-existent release",
cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath),

@ -48,6 +48,8 @@ the template:
- .GitCommit is the git commit - .GitCommit is the git commit
- .GitTreeState is the state of the git tree when Helm was built - .GitTreeState is the state of the git tree when Helm was built
- .GoVersion contains the version of Go that Helm was compiled with - .GoVersion contains the version of Go that Helm was compiled with
For example, --template='Version: {{.Version}}' outputs 'Version: v3.2.1'.
` `
type versionOptions struct { type versionOptions struct {

@ -3,52 +3,45 @@ module helm.sh/helm/v3
go 1.16 go 1.16
require ( require (
github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/toml v0.4.1
github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/Masterminds/goutils v1.1.1
github.com/Masterminds/semver/v3 v3.1.1 github.com/Masterminds/semver/v3 v3.1.1
github.com/Masterminds/sprig/v3 v3.2.2 github.com/Masterminds/sprig/v3 v3.2.2
github.com/Masterminds/squirrel v1.5.0 github.com/Masterminds/squirrel v1.5.1
github.com/Masterminds/vcs v1.13.1 github.com/Masterminds/vcs v1.13.1
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/containerd/containerd v1.4.4 github.com/containerd/containerd v1.5.8
github.com/cyphar/filepath-securejoin v0.2.2 github.com/cyphar/filepath-securejoin v0.2.3
github.com/deislabs/oras v0.10.0 github.com/distribution/distribution/v3 v3.0.0-20211118083504-a29a3c99a684
github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker v20.10.11+incompatible
github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce github.com/evanphx/json-patch v4.11.0+incompatible
github.com/docker/go-units v0.4.0
github.com/evanphx/json-patch v4.9.0+incompatible
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.8.0 github.com/gofrs/flock v0.8.1
github.com/gosuri/uitable v0.0.4 github.com/gosuri/uitable v0.0.4
github.com/jmoiron/sqlx v1.3.1 github.com/jmoiron/sqlx v1.3.4
github.com/lib/pq v1.10.0 github.com/lib/pq v1.10.4
github.com/mattn/go-shellwords v1.0.11 github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/copystructure v1.1.1 github.com/mitchellh/copystructure v1.2.0
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.2
github.com/opencontainers/image-spec v1.0.1
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.1.3 github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad github.com/ziutek/mymysql v1.5.4 // indirect
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
k8s.io/api v0.20.4 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
k8s.io/apiextensions-apiserver v0.20.4 k8s.io/api v0.22.4
k8s.io/apimachinery v0.20.4 k8s.io/apiextensions-apiserver v0.22.4
k8s.io/apiserver v0.20.4 k8s.io/apimachinery v0.22.4
k8s.io/cli-runtime v0.20.4 k8s.io/apiserver v0.22.4
k8s.io/client-go v0.20.4 k8s.io/cli-runtime v0.22.4
k8s.io/klog/v2 v2.8.0 k8s.io/client-go v0.22.4
k8s.io/kubectl v0.20.4 k8s.io/klog/v2 v2.30.0
sigs.k8s.io/yaml v1.2.0 k8s.io/kubectl v0.22.4
) oras.land/oras-go v1.0.0
sigs.k8s.io/yaml v1.3.0
replace (
github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d
github.com/docker/docker => github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
) )

1003
go.sum

File diff suppressed because it is too large Load Diff

@ -0,0 +1,71 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"strings"
"helm.sh/helm/v3/internal/experimental/pusher"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/internal/experimental/uploader"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
)
// Push is the action for uploading a chart.
//
// It provides the implementation of 'helm push'.
type Push struct {
Settings *cli.EnvSettings
cfg *action.Configuration
}
// PushOpt is a type of function that sets options for a push action.
type PushOpt func(*Push)
// WithPushConfig sets the cfg field on the push configuration object.
func WithPushConfig(cfg *action.Configuration) PushOpt {
return func(p *Push) {
p.cfg = cfg
}
}
// NewPushWithOpts creates a new push, with configuration options.
func NewPushWithOpts(opts ...PushOpt) *Push {
p := &Push{}
for _, fn := range opts {
fn(p)
}
return p
}
// Run executes 'helm push' against the given chart archive.
func (p *Push) Run(chartRef string, remote string) (string, error) {
var out strings.Builder
c := uploader.ChartUploader{
Out: &out,
Pushers: pusher.All(p.Settings),
Options: []pusher.Option{},
}
if registry.IsOCI(remote) {
c.Options = append(c.Options, pusher.WithRegistryClient(p.cfg.RegistryClient))
}
return out.String(), c.UploadTo(chartRef, remote)
}

@ -18,15 +18,18 @@ package action
import ( import (
"io" "io"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/action"
) )
// RegistryLogin performs a registry login operation. // RegistryLogin performs a registry login operation.
type RegistryLogin struct { type RegistryLogin struct {
cfg *Configuration cfg *action.Configuration
} }
// NewRegistryLogin creates a new RegistryLogin object with the given configuration. // NewRegistryLogin creates a new RegistryLogin object with the given configuration.
func NewRegistryLogin(cfg *Configuration) *RegistryLogin { func NewRegistryLogin(cfg *action.Configuration) *RegistryLogin {
return &RegistryLogin{ return &RegistryLogin{
cfg: cfg, cfg: cfg,
} }
@ -34,5 +37,8 @@ func NewRegistryLogin(cfg *Configuration) *RegistryLogin {
// Run executes the registry login operation // Run executes the registry login operation
func (a *RegistryLogin) Run(out io.Writer, hostname string, username string, password string, insecure bool) error { func (a *RegistryLogin) Run(out io.Writer, hostname string, username string, password string, insecure bool) error {
return a.cfg.RegistryClient.Login(hostname, username, password, insecure) return a.cfg.RegistryClient.Login(
hostname,
registry.LoginOptBasicAuth(username, password),
registry.LoginOptInsecure(insecure))
} }

@ -18,15 +18,17 @@ package action
import ( import (
"io" "io"
"helm.sh/helm/v3/pkg/action"
) )
// RegistryLogout performs a registry login operation. // RegistryLogout performs a registry login operation.
type RegistryLogout struct { type RegistryLogout struct {
cfg *Configuration cfg *action.Configuration
} }
// NewRegistryLogout creates a new RegistryLogout object with the given configuration. // NewRegistryLogout creates a new RegistryLogout object with the given configuration.
func NewRegistryLogout(cfg *Configuration) *RegistryLogout { func NewRegistryLogout(cfg *action.Configuration) *RegistryLogout {
return &RegistryLogout{ return &RegistryLogout{
cfg: cfg, cfg: cfg,
} }

@ -1,11 +1,10 @@
/* /*
Copyright The Helm Authors. Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
@ -14,15 +13,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package registry // import "helm.sh/helm/v3/internal/experimental/registry" /*
Package pusher provides a generalized tool for uploading data by scheme.
import ( This provides a method by which the plugin system can load arbitrary protocol
"github.com/containerd/containerd/remotes" handlers based upon a URL scheme.
) */
package pusher
type (
// Resolver provides remotes based on a locator
Resolver struct {
remotes.Resolver
}
)

@ -0,0 +1,104 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pusher
import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart/loader"
)
// OCIPusher is the default OCI backend handler
type OCIPusher struct {
opts options
}
// Push performs a Push from repo.Pusher.
func (pusher *OCIPusher) Push(chartRef, href string, options ...Option) error {
for _, opt := range options {
opt(&pusher.opts)
}
return pusher.push(chartRef, href)
}
func (pusher *OCIPusher) push(chartRef, href string) error {
stat, err := os.Stat(chartRef)
if err != nil {
if os.IsNotExist(err) {
return errors.Errorf("%s: no such file", chartRef)
}
return err
}
if stat.IsDir() {
return errors.New("cannot push directory, must provide chart archive (.tgz)")
}
meta, err := loader.Load(chartRef)
if err != nil {
return err
}
client := pusher.opts.registryClient
chartBytes, err := ioutil.ReadFile(chartRef)
if err != nil {
return err
}
var pushOpts []registry.PushOption
provRef := fmt.Sprintf("%s.prov", chartRef)
if _, err := os.Stat(provRef); err == nil {
provBytes, err := ioutil.ReadFile(provRef)
if err != nil {
return err
}
pushOpts = append(pushOpts, registry.PushOptProvData(provBytes))
}
ref := fmt.Sprintf("%s:%s",
path.Join(strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme)), meta.Metadata.Name),
meta.Metadata.Version)
_, err = client.Push(chartBytes, ref, pushOpts...)
return err
}
// NewOCIPusher constructs a valid OCI client as a Pusher
func NewOCIPusher(ops ...Option) (Pusher, error) {
registryClient, err := registry.NewClient()
if err != nil {
return nil, err
}
client := OCIPusher{
opts: options{
registryClient: registryClient,
},
}
for _, opt := range ops {
opt(&client.opts)
}
return &client, nil
}

@ -1,11 +1,10 @@
/* /*
Copyright The Helm Authors. Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
@ -14,16 +13,24 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package registry package pusher
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestConstants(t *testing.T) { func TestNewOCIPusher(t *testing.T) {
knownMediaTypes := KnownMediaTypes() testfn := func(ops *options) {
assert.Contains(t, knownMediaTypes, HelmChartConfigMediaType) if ops.registryClient == nil {
assert.Contains(t, knownMediaTypes, HelmChartContentLayerMediaType) t.Fatalf("the OCIPusher's registryClient should not be null")
}
}
p, err := NewOCIPusher(testfn)
if p == nil {
t.Error("NewOCIPusher returned nil")
}
if err != nil {
t.Error(err)
}
} }

@ -0,0 +1,95 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pusher
import (
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/cli"
)
// options are generic parameters to be provided to the pusher during instantiation.
//
// Pushers may or may not ignore these parameters as they are passed in.
type options struct {
registryClient *registry.Client
}
// Option allows specifying various settings configurable by the user for overriding the defaults
// used when performing Push operations with the Pusher.
type Option func(*options)
// WithRegistryClient sets the registryClient option.
func WithRegistryClient(client *registry.Client) Option {
return func(opts *options) {
opts.registryClient = client
}
}
// Pusher is an interface to support upload to the specified URL.
type Pusher interface {
// Push file content by url string
Push(chartRef, url string, options ...Option) error
}
// Constructor is the function for every pusher which creates a specific instance
// according to the configuration
type Constructor func(options ...Option) (Pusher, error)
// Provider represents any pusher and the schemes that it supports.
type Provider struct {
Schemes []string
New Constructor
}
// Provides returns true if the given scheme is supported by this Provider.
func (p Provider) Provides(scheme string) bool {
for _, i := range p.Schemes {
if i == scheme {
return true
}
}
return false
}
// Providers is a collection of Provider objects.
type Providers []Provider
// ByScheme returns a Provider that handles the given scheme.
//
// If no provider handles this scheme, this will return an error.
func (p Providers) ByScheme(scheme string) (Pusher, error) {
for _, pp := range p {
if pp.Provides(scheme) {
return pp.New()
}
}
return nil, errors.Errorf("scheme %q not supported", scheme)
}
var ociProvider = Provider{
Schemes: []string{registry.OCIScheme},
New: NewOCIPusher,
}
// All finds all of the registered pushers as a list of Provider instances.
// Currently, just the built-in pushers are collected.
func All(settings *cli.EnvSettings) Providers {
result := Providers{ociProvider}
return result
}

@ -0,0 +1,68 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pusher
import (
"testing"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/cli"
)
func TestProvider(t *testing.T) {
p := Provider{
[]string{"one", "three"},
func(_ ...Option) (Pusher, error) { return nil, nil },
}
if !p.Provides("three") {
t.Error("Expected provider to provide three")
}
}
func TestProviders(t *testing.T) {
ps := Providers{
{[]string{"one", "three"}, func(_ ...Option) (Pusher, error) { return nil, nil }},
{[]string{"two", "four"}, func(_ ...Option) (Pusher, error) { return nil, nil }},
}
if _, err := ps.ByScheme("one"); err != nil {
t.Error(err)
}
if _, err := ps.ByScheme("four"); err != nil {
t.Error(err)
}
if _, err := ps.ByScheme("five"); err == nil {
t.Error("Did not expect handler for five")
}
}
func TestAll(t *testing.T) {
env := cli.New()
all := All(env)
if len(all) != 1 {
t.Errorf("expected 1 provider (OCI), got %d", len(all))
}
}
func TestByScheme(t *testing.T) {
env := cli.New()
g := All(env)
if _, err := g.ByScheme(registry.OCIScheme); err != nil {
t.Error(err)
}
}

@ -1,368 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
orascontent "github.com/deislabs/oras/pkg/content"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
)
const (
// CacheRootDir is the root directory for a cache
CacheRootDir = "cache"
)
type (
// Cache handles local/in-memory storage of Helm charts, compliant with OCI Layout
Cache struct {
debug bool
out io.Writer
rootDir string
ociStore *orascontent.OCIStore
memoryStore *orascontent.Memorystore
}
// CacheRefSummary contains as much info as available describing a chart reference in cache
// Note: fields here are sorted by the order in which they are set in FetchReference method
CacheRefSummary struct {
Name string
Repo string
Tag string
Exists bool
Manifest *ocispec.Descriptor
Config *ocispec.Descriptor
ContentLayer *ocispec.Descriptor
Size int64
Digest digest.Digest
CreatedAt time.Time
Chart *chart.Chart
}
)
// NewCache returns a new OCI Layout-compliant cache with config
func NewCache(opts ...CacheOption) (*Cache, error) {
cache := &Cache{
out: ioutil.Discard,
}
for _, opt := range opts {
opt(cache)
}
// validate
if cache.rootDir == "" {
return nil, errors.New("must set cache root dir on initialization")
}
return cache, nil
}
// FetchReference retrieves a chart ref from cache
func (cache *Cache) FetchReference(ref *Reference) (*CacheRefSummary, error) {
if err := cache.init(); err != nil {
return nil, err
}
r := CacheRefSummary{
Name: ref.FullName(),
Repo: ref.Repo,
Tag: ref.Tag,
}
for _, desc := range cache.ociStore.ListReferences() {
if desc.Annotations[ocispec.AnnotationRefName] == r.Name {
r.Exists = true
manifestBytes, err := cache.fetchBlob(&desc)
if err != nil {
return &r, err
}
var manifest ocispec.Manifest
err = json.Unmarshal(manifestBytes, &manifest)
if err != nil {
return &r, err
}
r.Manifest = &desc
r.Config = &manifest.Config
numLayers := len(manifest.Layers)
if numLayers != 1 {
return &r, errors.New(
fmt.Sprintf("manifest does not contain exactly 1 layer (total: %d)", numLayers))
}
var contentLayer *ocispec.Descriptor
for _, layer := range manifest.Layers {
switch layer.MediaType {
case HelmChartContentLayerMediaType:
contentLayer = &layer
}
}
if contentLayer == nil {
return &r, errors.New(
fmt.Sprintf("manifest does not contain a layer with mediatype %s", HelmChartContentLayerMediaType))
}
if contentLayer.Size == 0 {
return &r, errors.New(
fmt.Sprintf("manifest layer with mediatype %s is of size 0", HelmChartContentLayerMediaType))
}
r.ContentLayer = contentLayer
info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest)
if err != nil {
return &r, err
}
r.Size = info.Size
r.Digest = info.Digest
r.CreatedAt = info.CreatedAt
contentBytes, err := cache.fetchBlob(contentLayer)
if err != nil {
return &r, err
}
ch, err := loader.LoadArchive(bytes.NewBuffer(contentBytes))
if err != nil {
return &r, err
}
r.Chart = ch
}
}
return &r, nil
}
// StoreReference stores a chart ref in cache
func (cache *Cache) StoreReference(ref *Reference, ch *chart.Chart) (*CacheRefSummary, error) {
if err := cache.init(); err != nil {
return nil, err
}
r := CacheRefSummary{
Name: ref.FullName(),
Repo: ref.Repo,
Tag: ref.Tag,
Chart: ch,
}
existing, _ := cache.FetchReference(ref)
r.Exists = existing.Exists
config, _, err := cache.saveChartConfig(ch)
if err != nil {
return &r, err
}
r.Config = config
contentLayer, _, err := cache.saveChartContentLayer(ch)
if err != nil {
return &r, err
}
r.ContentLayer = contentLayer
info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest)
if err != nil {
return &r, err
}
r.Size = info.Size
r.Digest = info.Digest
r.CreatedAt = info.CreatedAt
manifest, _, err := cache.saveChartManifest(config, contentLayer)
if err != nil {
return &r, err
}
r.Manifest = manifest
return &r, nil
}
// DeleteReference deletes a chart ref from cache
// TODO: garbage collection, only manifest removed
func (cache *Cache) DeleteReference(ref *Reference) (*CacheRefSummary, error) {
if err := cache.init(); err != nil {
return nil, err
}
r, err := cache.FetchReference(ref)
if err != nil || !r.Exists {
return r, err
}
cache.ociStore.DeleteReference(r.Name)
err = cache.ociStore.SaveIndex()
return r, err
}
// ListReferences lists all chart refs in a cache
func (cache *Cache) ListReferences() ([]*CacheRefSummary, error) {
if err := cache.init(); err != nil {
return nil, err
}
var rr []*CacheRefSummary
for _, desc := range cache.ociStore.ListReferences() {
name := desc.Annotations[ocispec.AnnotationRefName]
if name == "" {
if cache.debug {
fmt.Fprintf(cache.out, "warning: found manifest without name: %s", desc.Digest.Hex())
}
continue
}
ref, err := ParseReference(name)
if err != nil {
return rr, err
}
r, err := cache.FetchReference(ref)
if err != nil {
return rr, err
}
rr = append(rr, r)
}
return rr, nil
}
// AddManifest provides a manifest to the cache index.json
func (cache *Cache) AddManifest(ref *Reference, manifest *ocispec.Descriptor) error {
if err := cache.init(); err != nil {
return err
}
cache.ociStore.AddReference(ref.FullName(), *manifest)
err := cache.ociStore.SaveIndex()
return err
}
// Provider provides a valid containerd Provider
func (cache *Cache) Provider() content.Provider {
return content.Provider(cache.ociStore)
}
// Ingester provides a valid containerd Ingester
func (cache *Cache) Ingester() content.Ingester {
return content.Ingester(cache.ociStore)
}
// ProvideIngester provides a valid oras ProvideIngester
func (cache *Cache) ProvideIngester() orascontent.ProvideIngester {
return orascontent.ProvideIngester(cache.ociStore)
}
// init creates files needed necessary for OCI layout store
func (cache *Cache) init() error {
if cache.ociStore == nil {
ociStore, err := orascontent.NewOCIStore(cache.rootDir)
if err != nil {
return err
}
cache.ociStore = ociStore
cache.memoryStore = orascontent.NewMemoryStore()
}
return nil
}
// saveChartConfig stores the Chart.yaml as json blob and returns a descriptor
func (cache *Cache) saveChartConfig(ch *chart.Chart) (*ocispec.Descriptor, bool, error) {
configBytes, err := json.Marshal(ch.Metadata)
if err != nil {
return nil, false, err
}
configExists, err := cache.storeBlob(configBytes)
if err != nil {
return nil, configExists, err
}
descriptor := cache.memoryStore.Add("", HelmChartConfigMediaType, configBytes)
return &descriptor, configExists, nil
}
// saveChartContentLayer stores the chart as tarball blob and returns a descriptor
func (cache *Cache) saveChartContentLayer(ch *chart.Chart) (*ocispec.Descriptor, bool, error) {
destDir := filepath.Join(cache.rootDir, ".build")
os.MkdirAll(destDir, 0755)
tmpFile, err := chartutil.Save(ch, destDir)
defer os.Remove(tmpFile)
if err != nil {
return nil, false, errors.Wrap(err, "failed to save")
}
contentBytes, err := ioutil.ReadFile(tmpFile)
if err != nil {
return nil, false, err
}
contentExists, err := cache.storeBlob(contentBytes)
if err != nil {
return nil, contentExists, err
}
descriptor := cache.memoryStore.Add("", HelmChartContentLayerMediaType, contentBytes)
return &descriptor, contentExists, nil
}
// saveChartManifest stores the chart manifest as json blob and returns a descriptor
func (cache *Cache) saveChartManifest(config *ocispec.Descriptor, contentLayer *ocispec.Descriptor) (*ocispec.Descriptor, bool, error) {
manifest := ocispec.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
Config: *config,
Layers: []ocispec.Descriptor{*contentLayer},
}
manifestBytes, err := json.Marshal(manifest)
if err != nil {
return nil, false, err
}
manifestExists, err := cache.storeBlob(manifestBytes)
if err != nil {
return nil, manifestExists, err
}
descriptor := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifestBytes),
Size: int64(len(manifestBytes)),
}
return &descriptor, manifestExists, nil
}
// storeBlob stores a blob on filesystem
func (cache *Cache) storeBlob(blobBytes []byte) (bool, error) {
var exists bool
writer, err := cache.ociStore.Store.Writer(ctx(cache.out, cache.debug),
content.WithRef(digest.FromBytes(blobBytes).Hex()))
if err != nil {
return exists, err
}
_, err = writer.Write(blobBytes)
if err != nil {
return exists, err
}
err = writer.Commit(ctx(cache.out, cache.debug), 0, writer.Digest())
if err != nil {
if !errdefs.IsAlreadyExists(err) {
return exists, err
}
exists = true
}
err = writer.Close()
return exists, err
}
// fetchBlob retrieves a blob from filesystem
func (cache *Cache) fetchBlob(desc *ocispec.Descriptor) ([]byte, error) {
reader, err := cache.ociStore.ReaderAt(ctx(cache.out, cache.debug), *desc)
if err != nil {
return nil, err
}
defer reader.Close()
bytes := make([]byte, desc.Size)
_, err = reader.ReadAt(bytes, 0)
if err != nil {
return nil, err
}
return bytes, nil
}

@ -1,48 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import (
"io"
)
type (
// CacheOption allows specifying various settings configurable by the user for overriding the defaults
// used when creating a new default cache
CacheOption func(*Cache)
)
// CacheOptDebug returns a function that sets the debug setting on cache options set
func CacheOptDebug(debug bool) CacheOption {
return func(cache *Cache) {
cache.debug = debug
}
}
// CacheOptWriter returns a function that sets the writer setting on cache options set
func CacheOptWriter(out io.Writer) CacheOption {
return func(cache *Cache) {
cache.out = out
}
}
// CacheOptRoot returns a function that sets the root directory setting on cache options set
func CacheOptRoot(rootDir string) CacheOption {
return func(cache *Cache) {
cache.rootDir = rootDir
}
}

@ -17,320 +17,497 @@ limitations under the License.
package registry // import "helm.sh/helm/v3/internal/experimental/registry" package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import ( import (
"bytes" "encoding/json"
"context"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"sort" "strings"
auth "github.com/deislabs/oras/pkg/auth/docker" "github.com/containerd/containerd/remotes"
"github.com/deislabs/oras/pkg/content"
"github.com/deislabs/oras/pkg/oras"
"github.com/gosuri/uitable"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"oras.land/oras-go/pkg/auth"
dockerauth "oras.land/oras-go/pkg/auth/docker"
"oras.land/oras-go/pkg/content"
"oras.land/oras-go/pkg/oras"
"helm.sh/helm/v3/internal/version"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v3/pkg/helmpath"
) )
const (
// CredentialsFileBasename is the filename for auth credentials file
CredentialsFileBasename = "config.json"
)
type ( type (
// Client works with OCI-compliant registries and local Helm chart cache // Client works with OCI-compliant registries
Client struct { Client struct {
debug bool debug bool
// path to repository config file e.g. ~/.docker/config.json // path to repository config file e.g. ~/.docker/config.json
credentialsFile string credentialsFile string
out io.Writer out io.Writer
authorizer *Authorizer authorizer auth.Client
resolver *Resolver resolver remotes.Resolver
cache *Cache
} }
// ClientOption allows specifying various settings configurable by the user for overriding the defaults
// used when creating a new default client
ClientOption func(*Client)
) )
// NewClient returns a new registry client with config // NewClient returns a new registry client with config
func NewClient(opts ...ClientOption) (*Client, error) { func NewClient(options ...ClientOption) (*Client, error) {
client := &Client{ client := &Client{
out: ioutil.Discard, out: ioutil.Discard,
} }
for _, opt := range opts { for _, option := range options {
opt(client) option(client)
} }
// set defaults if fields are missing
if client.credentialsFile == "" { if client.credentialsFile == "" {
client.credentialsFile = helmpath.CachePath("registry", CredentialsFileBasename) client.credentialsFile = helmpath.ConfigPath(CredentialsFileBasename)
} }
if client.authorizer == nil { if client.authorizer == nil {
authClient, err := auth.NewClient(client.credentialsFile) authClient, err := dockerauth.NewClient(client.credentialsFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
client.authorizer = &Authorizer{ client.authorizer = authClient
Client: authClient,
}
} }
if client.resolver == nil { if client.resolver == nil {
resolver, err := client.authorizer.Resolver(context.Background(), http.DefaultClient, false) headers := http.Header{}
if err != nil { headers.Set("User-Agent", version.GetUserAgent())
return nil, err opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)}
} resolver, err := client.authorizer.ResolverWithOpts(opts...)
client.resolver = &Resolver{
Resolver: resolver,
}
}
if client.cache == nil {
cache, err := NewCache(
CacheOptDebug(client.debug),
CacheOptWriter(client.out),
CacheOptRoot(helmpath.CachePath("registry", CacheRootDir)),
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
client.cache = cache client.resolver = resolver
} }
return client, nil return client, nil
} }
// ClientOptDebug returns a function that sets the debug setting on client options set
func ClientOptDebug(debug bool) ClientOption {
return func(client *Client) {
client.debug = debug
}
}
// ClientOptWriter returns a function that sets the writer setting on client options set
func ClientOptWriter(out io.Writer) ClientOption {
return func(client *Client) {
client.out = out
}
}
// ClientOptCredentialsFile returns a function that sets the credentialsFile setting on a client options set
func ClientOptCredentialsFile(credentialsFile string) ClientOption {
return func(client *Client) {
client.credentialsFile = credentialsFile
}
}
type (
// LoginOption allows specifying various settings on login
LoginOption func(*loginOperation)
loginOperation struct {
username string
password string
insecure bool
}
)
// Login logs into a registry // Login logs into a registry
func (c *Client) Login(hostname string, username string, password string, insecure bool) error { func (c *Client) Login(host string, options ...LoginOption) error {
err := c.authorizer.Login(ctx(c.out, c.debug), hostname, username, password, insecure) operation := &loginOperation{}
if err != nil { for _, option := range options {
option(operation)
}
authorizerLoginOpts := []auth.LoginOption{
auth.WithLoginContext(ctx(c.out, c.debug)),
auth.WithLoginHostname(host),
auth.WithLoginUsername(operation.username),
auth.WithLoginSecret(operation.password),
auth.WithLoginUserAgent(version.GetUserAgent()),
}
if operation.insecure {
authorizerLoginOpts = append(authorizerLoginOpts, auth.WithLoginInsecure())
}
if err := c.authorizer.LoginWithOpts(authorizerLoginOpts...); err != nil {
return err return err
} }
fmt.Fprintf(c.out, "Login succeeded\n") fmt.Fprintln(c.out, "Login Succeeded")
return nil return nil
} }
// Logout logs out of a registry // LoginOptBasicAuth returns a function that sets the username/password settings on login
func (c *Client) Logout(hostname string) error { func LoginOptBasicAuth(username string, password string) LoginOption {
err := c.authorizer.Logout(ctx(c.out, c.debug), hostname) return func(operation *loginOperation) {
if err != nil { operation.username = username
return err operation.password = password
} }
fmt.Fprintln(c.out, "Logout succeeded")
return nil
} }
// PushChart uploads a chart to a registry // LoginOptInsecure returns a function that sets the insecure setting on login
func (c *Client) PushChart(ref *Reference) error { func LoginOptInsecure(insecure bool) LoginOption {
r, err := c.cache.FetchReference(ref) return func(operation *loginOperation) {
if err != nil { operation.insecure = insecure
return err
} }
if !r.Exists { }
return errors.New(fmt.Sprintf("Chart not found: %s", r.Name))
type (
// LogoutOption allows specifying various settings on logout
LogoutOption func(*logoutOperation)
logoutOperation struct{}
)
// Logout logs out of a registry
func (c *Client) Logout(host string, opts ...LogoutOption) error {
operation := &logoutOperation{}
for _, opt := range opts {
opt(operation)
} }
fmt.Fprintf(c.out, "The push refers to repository [%s]\n", r.Repo) if err := c.authorizer.Logout(ctx(c.out, c.debug), host); err != nil {
c.printCacheRefSummary(r)
layers := []ocispec.Descriptor{*r.ContentLayer}
_, err = oras.Push(ctx(c.out, c.debug), c.resolver, r.Name, c.cache.Provider(), layers,
oras.WithConfig(*r.Config), oras.WithNameValidation(nil))
if err != nil {
return err return err
} }
s := "" fmt.Fprintf(c.out, "Removing login credentials for %s\n", host)
numLayers := len(layers)
if 1 < numLayers {
s = "s"
}
fmt.Fprintf(c.out,
"%s: pushed to remote (%d layer%s, %s total)\n", r.Tag, numLayers, s, byteCountBinary(r.Size))
return nil return nil
} }
// PullChart downloads a chart from a registry type (
func (c *Client) PullChart(ref *Reference) (*bytes.Buffer, error) { // PullOption allows specifying various settings on pull
buf := bytes.NewBuffer(nil) PullOption func(*pullOperation)
if ref.Tag == "" { // PullResult is the result returned upon successful pull.
return buf, errors.New("tag explicitly required") PullResult struct {
Manifest *descriptorPullSummary `json:"manifest"`
Config *descriptorPullSummary `json:"config"`
Chart *descriptorPullSummaryWithMeta `json:"chart"`
Prov *descriptorPullSummary `json:"prov"`
Ref string `json:"ref"`
} }
fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo) descriptorPullSummary struct {
Data []byte `json:"-"`
store := content.NewMemoryStore() Digest string `json:"digest"`
fullname := ref.FullName() Size int64 `json:"size"`
_ = fullname
_, layerDescriptors, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref.FullName(), store,
oras.WithPullEmptyNameAllowed(),
oras.WithAllowedMediaTypes(KnownMediaTypes()))
if err != nil {
return buf, err
} }
numLayers := len(layerDescriptors) descriptorPullSummaryWithMeta struct {
if numLayers < 1 { descriptorPullSummary
return buf, errors.New( Meta *chart.Metadata `json:"meta"`
fmt.Sprintf("manifest does not contain at least 1 layer (total: %d)", numLayers))
} }
var contentLayer *ocispec.Descriptor pullOperation struct {
for _, layer := range layerDescriptors { withChart bool
layer := layer withProv bool
switch layer.MediaType { ignoreMissingProv bool
case HelmChartContentLayerMediaType: }
contentLayer = &layer )
// Pull downloads a chart from a registry
func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
operation := &pullOperation{
withChart: true, // By default, always download the chart layer
}
for _, option := range options {
option(operation)
}
if !operation.withChart && !operation.withProv {
return nil, errors.New(
"must specify at least one layer to pull (chart/prov)")
}
memoryStore := content.NewMemory()
allowedMediaTypes := []string{
ConfigMediaType,
}
minNumDescriptors := 1 // 1 for the config
if operation.withChart {
minNumDescriptors++
allowedMediaTypes = append(allowedMediaTypes, ChartLayerMediaType, LegacyChartLayerMediaType)
}
if operation.withProv {
if !operation.ignoreMissingProv {
minNumDescriptors++
} }
allowedMediaTypes = append(allowedMediaTypes, ProvLayerMediaType)
}
var descriptors, layers []ocispec.Descriptor
registryStore := content.Registry{Resolver: c.resolver}
manifest, err := oras.Copy(ctx(c.out, c.debug), registryStore, ref, memoryStore, "",
oras.WithPullEmptyNameAllowed(),
oras.WithAllowedMediaTypes(allowedMediaTypes),
oras.WithLayerDescriptors(func(l []ocispec.Descriptor) {
layers = l
}))
if err != nil {
fmt.Println(err)
return nil, err
} }
if contentLayer == nil { descriptors = append(descriptors, manifest)
return buf, errors.New( descriptors = append(descriptors, layers...)
numDescriptors := len(descriptors)
if numDescriptors < minNumDescriptors {
return nil, errors.New(
fmt.Sprintf("manifest does not contain minimum number of descriptors (%d), descriptors found: %d",
minNumDescriptors, numDescriptors))
}
var configDescriptor *ocispec.Descriptor
var chartDescriptor *ocispec.Descriptor
var provDescriptor *ocispec.Descriptor
for _, descriptor := range descriptors {
d := descriptor
switch d.MediaType {
case ConfigMediaType:
configDescriptor = &d
case ChartLayerMediaType:
chartDescriptor = &d
case ProvLayerMediaType:
provDescriptor = &d
case LegacyChartLayerMediaType:
chartDescriptor = &d
fmt.Fprintf(c.out, "Warning: chart media type %s is deprecated\n", LegacyChartLayerMediaType)
}
}
if configDescriptor == nil {
return nil, errors.New(
fmt.Sprintf("could not load config with mediatype %s", ConfigMediaType))
}
if operation.withChart && chartDescriptor == nil {
return nil, errors.New(
fmt.Sprintf("manifest does not contain a layer with mediatype %s", fmt.Sprintf("manifest does not contain a layer with mediatype %s",
HelmChartContentLayerMediaType)) ChartLayerMediaType))
}
var provMissing bool
if operation.withProv && provDescriptor == nil {
if operation.ignoreMissingProv {
provMissing = true
} else {
return nil, errors.New(
fmt.Sprintf("manifest does not contain a layer with mediatype %s",
ProvLayerMediaType))
}
}
result := &PullResult{
Manifest: &descriptorPullSummary{
Digest: manifest.Digest.String(),
Size: manifest.Size,
},
Config: &descriptorPullSummary{
Digest: configDescriptor.Digest.String(),
Size: configDescriptor.Size,
},
Chart: &descriptorPullSummaryWithMeta{},
Prov: &descriptorPullSummary{},
Ref: ref,
}
var getManifestErr error
if _, manifestData, ok := memoryStore.Get(manifest); !ok {
getManifestErr = errors.Errorf("Unable to retrieve blob with digest %s", manifest.Digest)
} else {
result.Manifest.Data = manifestData
} }
if getManifestErr != nil {
return nil, getManifestErr
}
var getConfigDescriptorErr error
if _, configData, ok := memoryStore.Get(*configDescriptor); !ok {
getConfigDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", configDescriptor.Digest)
} else {
result.Config.Data = configData
var meta *chart.Metadata
if err := json.Unmarshal(configData, &meta); err != nil {
return nil, err
}
result.Chart.Meta = meta
}
if getConfigDescriptorErr != nil {
return nil, getConfigDescriptorErr
}
if operation.withChart {
var getChartDescriptorErr error
if _, chartData, ok := memoryStore.Get(*chartDescriptor); !ok {
getChartDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", chartDescriptor.Digest)
} else {
result.Chart.Data = chartData
result.Chart.Digest = chartDescriptor.Digest.String()
result.Chart.Size = chartDescriptor.Size
}
if getChartDescriptorErr != nil {
return nil, getChartDescriptorErr
}
}
if operation.withProv && !provMissing {
var getProvDescriptorErr error
if _, provData, ok := memoryStore.Get(*provDescriptor); !ok {
getProvDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", provDescriptor.Digest)
} else {
result.Prov.Data = provData
result.Prov.Digest = provDescriptor.Digest.String()
result.Prov.Size = provDescriptor.Size
}
if getProvDescriptorErr != nil {
return nil, getProvDescriptorErr
}
}
fmt.Fprintf(c.out, "Pulled: %s\n", result.Ref)
fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
return result, nil
}
_, b, ok := store.Get(*contentLayer) // PullOptWithChart returns a function that sets the withChart setting on pull
if !ok { func PullOptWithChart(withChart bool) PullOption {
return buf, errors.Errorf("Unable to retrieve blob with digest %s", contentLayer.Digest) return func(operation *pullOperation) {
operation.withChart = withChart
} }
}
buf = bytes.NewBuffer(b) // PullOptWithProv returns a function that sets the withProv setting on pull
return buf, nil func PullOptWithProv(withProv bool) PullOption {
return func(operation *pullOperation) {
operation.withProv = withProv
}
} }
// PullChartToCache pulls a chart from an OCI Registry to the Registry Cache. // PullOptIgnoreMissingProv returns a function that sets the ignoreMissingProv setting on pull
// This function is needed for `helm chart pull`, which is experimental and will be deprecated soon. func PullOptIgnoreMissingProv(ignoreMissingProv bool) PullOption {
// Likewise, the Registry cache will soon be deprecated as will this function. return func(operation *pullOperation) {
func (c *Client) PullChartToCache(ref *Reference) error { operation.ignoreMissingProv = ignoreMissingProv
if ref.Tag == "" {
return errors.New("tag explicitly required")
} }
existing, err := c.cache.FetchReference(ref) }
if err != nil {
return err type (
// PushOption allows specifying various settings on push
PushOption func(*pushOperation)
// PushResult is the result returned upon successful push.
PushResult struct {
Manifest *descriptorPushSummary `json:"manifest"`
Config *descriptorPushSummary `json:"config"`
Chart *descriptorPushSummaryWithMeta `json:"chart"`
Prov *descriptorPushSummary `json:"prov"`
Ref string `json:"ref"`
} }
fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo)
manifest, _, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref.FullName(), c.cache.Ingester(), descriptorPushSummary struct {
oras.WithPullEmptyNameAllowed(), Digest string `json:"digest"`
oras.WithAllowedMediaTypes(KnownMediaTypes()), Size int64 `json:"size"`
oras.WithContentProvideIngester(c.cache.ProvideIngester()))
if err != nil {
return err
} }
err = c.cache.AddManifest(ref, &manifest)
if err != nil { descriptorPushSummaryWithMeta struct {
return err descriptorPushSummary
Meta *chart.Metadata `json:"meta"`
} }
r, err := c.cache.FetchReference(ref)
if err != nil { pushOperation struct {
return err provData []byte
strictMode bool
} }
if !r.Exists { )
return errors.New(fmt.Sprintf("Chart not found: %s", r.Name))
// Push uploads a chart to a registry.
func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResult, error) {
operation := &pushOperation{
strictMode: true, // By default, enable strict mode
} }
c.printCacheRefSummary(r) for _, option := range options {
if !existing.Exists { option(operation)
fmt.Fprintf(c.out, "Status: Downloaded newer chart for %s\n", ref.FullName())
} else {
fmt.Fprintf(c.out, "Status: Chart is up to date for %s\n", ref.FullName())
} }
return err meta, err := extractChartMeta(data)
}
// SaveChart stores a copy of chart in local cache
func (c *Client) SaveChart(ch *chart.Chart, ref *Reference) error {
r, err := c.cache.StoreReference(ref, ch)
if err != nil { if err != nil {
return err return nil, err
} }
c.printCacheRefSummary(r) if operation.strictMode {
err = c.cache.AddManifest(ref, r.Manifest) if !strings.HasSuffix(ref, fmt.Sprintf("/%s:%s", meta.Name, meta.Version)) {
return nil, errors.New(
"strict mode enabled, ref basename and tag must match the chart name and version")
}
}
memoryStore := content.NewMemory()
chartDescriptor, err := memoryStore.Add("", ChartLayerMediaType, data)
if err != nil { if err != nil {
return err return nil, err
} }
fmt.Fprintf(c.out, "%s: saved\n", r.Tag)
return nil
}
// LoadChart retrieves a chart object by reference configData, err := json.Marshal(meta)
func (c *Client) LoadChart(ref *Reference) (*chart.Chart, error) {
r, err := c.cache.FetchReference(ref)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !r.Exists {
return nil, errors.New(fmt.Sprintf("Chart not found: %s", ref.FullName()))
}
c.printCacheRefSummary(r)
return r.Chart, nil
}
// RemoveChart deletes a locally saved chart configDescriptor, err := memoryStore.Add("", ConfigMediaType, configData)
func (c *Client) RemoveChart(ref *Reference) error {
r, err := c.cache.DeleteReference(ref)
if err != nil { if err != nil {
return err return nil, err
} }
if !r.Exists {
return errors.New(fmt.Sprintf("Chart not found: %s", ref.FullName())) descriptors := []ocispec.Descriptor{chartDescriptor}
var provDescriptor ocispec.Descriptor
if operation.provData != nil {
provDescriptor, err = memoryStore.Add("", ProvLayerMediaType, operation.provData)
if err != nil {
return nil, err
}
descriptors = append(descriptors, provDescriptor)
} }
fmt.Fprintf(c.out, "%s: removed\n", r.Tag)
return nil
}
// PrintChartTable prints a list of locally stored charts manifestData, manifest, err := content.GenerateManifest(&configDescriptor, nil, descriptors...)
func (c *Client) PrintChartTable() error {
table := uitable.New()
table.MaxColWidth = 60
table.AddRow("REF", "NAME", "VERSION", "DIGEST", "SIZE", "CREATED")
rows, err := c.getChartTableRows()
if err != nil { if err != nil {
return err return nil, err
}
for _, row := range rows {
table.AddRow(row...)
} }
fmt.Fprintln(c.out, table.String())
return nil
}
// printCacheRefSummary prints out chart ref summary if err := memoryStore.StoreManifest(ref, manifest, manifestData); err != nil {
func (c *Client) printCacheRefSummary(r *CacheRefSummary) { return nil, err
fmt.Fprintf(c.out, "ref: %s\n", r.Name) }
fmt.Fprintf(c.out, "digest: %s\n", r.Manifest.Digest.Hex())
fmt.Fprintf(c.out, "size: %s\n", byteCountBinary(r.Size))
fmt.Fprintf(c.out, "name: %s\n", r.Chart.Metadata.Name)
fmt.Fprintf(c.out, "version: %s\n", r.Chart.Metadata.Version)
}
// getChartTableRows returns rows in uitable-friendly format registryStore := content.Registry{Resolver: c.resolver}
func (c *Client) getChartTableRows() ([][]interface{}, error) { _, err = oras.Copy(ctx(c.out, c.debug), memoryStore, ref, registryStore, "",
rr, err := c.cache.ListReferences() oras.WithNameValidation(nil))
if err != nil { if err != nil {
return nil, err return nil, err
} }
refsMap := map[string]map[string]string{} chartSummary := &descriptorPushSummaryWithMeta{
for _, r := range rr { Meta: meta,
refsMap[r.Name] = map[string]string{ }
"name": r.Chart.Metadata.Name, chartSummary.Digest = chartDescriptor.Digest.String()
"version": r.Chart.Metadata.Version, chartSummary.Size = chartDescriptor.Size
"digest": shortDigest(r.Manifest.Digest.Hex()), result := &PushResult{
"size": byteCountBinary(r.Size), Manifest: &descriptorPushSummary{
"created": timeAgo(r.CreatedAt), Digest: manifest.Digest.String(),
Size: manifest.Size,
},
Config: &descriptorPushSummary{
Digest: configDescriptor.Digest.String(),
Size: configDescriptor.Size,
},
Chart: chartSummary,
Prov: &descriptorPushSummary{}, // prevent nil references
Ref: ref,
}
if operation.provData != nil {
result.Prov = &descriptorPushSummary{
Digest: provDescriptor.Digest.String(),
Size: provDescriptor.Size,
} }
} }
// Sort and convert to format expected by uitable fmt.Fprintf(c.out, "Pushed: %s\n", result.Ref)
rows := make([][]interface{}, len(refsMap)) fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
keys := make([]string, 0, len(refsMap)) return result, err
for key := range refsMap { }
keys = append(keys, key)
} // PushOptProvData returns a function that sets the prov bytes setting on push
sort.Strings(keys) func PushOptProvData(provData []byte) PushOption {
for i, key := range keys { return func(operation *pushOperation) {
rows[i] = make([]interface{}, 6) operation.provData = provData
rows[i][0] = key }
ref := refsMap[key] }
for j, k := range []string{"name", "version", "digest", "size", "created"} {
rows[i][j+1] = ref[k] // PushOptStrictMode returns a function that sets the strictMode setting on push
} func PushOptStrictMode(strictMode bool) PushOption {
return func(operation *pushOperation) {
operation.strictMode = strictMode
} }
return rows, nil
} }

@ -1,69 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import (
"io"
)
type (
// ClientOption allows specifying various settings configurable by the user for overriding the defaults
// used when creating a new default client
ClientOption func(*Client)
)
// ClientOptDebug returns a function that sets the debug setting on client options set
func ClientOptDebug(debug bool) ClientOption {
return func(client *Client) {
client.debug = debug
}
}
// ClientOptWriter returns a function that sets the writer setting on client options set
func ClientOptWriter(out io.Writer) ClientOption {
return func(client *Client) {
client.out = out
}
}
// ClientOptResolver returns a function that sets the resolver setting on client options set
func ClientOptResolver(resolver *Resolver) ClientOption {
return func(client *Client) {
client.resolver = resolver
}
}
// ClientOptAuthorizer returns a function that sets the authorizer setting on client options set
func ClientOptAuthorizer(authorizer *Authorizer) ClientOption {
return func(client *Client) {
client.authorizer = authorizer
}
}
// ClientOptCache returns a function that sets the cache setting on a client options set
func ClientOptCache(cache *Cache) ClientOption {
return func(client *Client) {
client.cache = cache
}
}
// ClientOptCredentialsFile returns a function that sets the cache setting on a client options set
func ClientOptCredentialsFile(credentialsFile string) ClientOption {
return func(client *Client) {
client.credentialsFile = credentialsFile
}
}

@ -32,20 +32,17 @@ import (
"time" "time"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
auth "github.com/deislabs/oras/pkg/auth/docker" "github.com/distribution/distribution/v3/configuration"
"github.com/docker/distribution/configuration" "github.com/distribution/distribution/v3/registry"
"github.com/docker/distribution/registry" _ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
_ "github.com/docker/distribution/registry/auth/htpasswd" _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
_ "github.com/docker/distribution/registry/storage/driver/inmemory"
"github.com/phayes/freeport" "github.com/phayes/freeport"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"helm.sh/helm/v3/pkg/chart"
) )
var ( var (
testCacheRootDir = "helm-registry-test" testWorkspaceDir = "helm-registry-test"
testHtpasswdFileBasename = "authtest.htpasswd" testHtpasswdFileBasename = "authtest.htpasswd"
testUsername = "myuser" testUsername = "myuser"
testPassword = "mypass" testPassword = "mypass"
@ -56,51 +53,32 @@ type RegistryClientTestSuite struct {
Out io.Writer Out io.Writer
DockerRegistryHost string DockerRegistryHost string
CompromisedRegistryHost string CompromisedRegistryHost string
CacheRootDir string WorkspaceDir string
RegistryClient *Client RegistryClient *Client
} }
func (suite *RegistryClientTestSuite) SetupSuite() { func (suite *RegistryClientTestSuite) SetupSuite() {
suite.CacheRootDir = testCacheRootDir suite.WorkspaceDir = testWorkspaceDir
os.RemoveAll(suite.CacheRootDir) os.RemoveAll(suite.WorkspaceDir)
os.Mkdir(suite.CacheRootDir, 0700) os.Mkdir(suite.WorkspaceDir, 0700)
var out bytes.Buffer var out bytes.Buffer
suite.Out = &out suite.Out = &out
credentialsFile := filepath.Join(suite.CacheRootDir, CredentialsFileBasename) credentialsFile := filepath.Join(suite.WorkspaceDir, CredentialsFileBasename)
client, err := auth.NewClient(credentialsFile)
suite.Nil(err, "no error creating auth client")
resolver, err := client.Resolver(context.Background(), http.DefaultClient, false)
suite.Nil(err, "no error creating resolver")
// create cache
cache, err := NewCache(
CacheOptDebug(true),
CacheOptWriter(suite.Out),
CacheOptRoot(filepath.Join(suite.CacheRootDir, CacheRootDir)),
)
suite.Nil(err, "no error creating cache")
// init test client // init test client
var err error
suite.RegistryClient, err = NewClient( suite.RegistryClient, err = NewClient(
ClientOptDebug(true), ClientOptDebug(true),
ClientOptWriter(suite.Out), ClientOptWriter(suite.Out),
ClientOptAuthorizer(&Authorizer{ ClientOptCredentialsFile(credentialsFile),
Client: client,
}),
ClientOptResolver(&Resolver{
Resolver: resolver,
}),
ClientOptCache(cache),
) )
suite.Nil(err, "no error creating registry client") suite.Nil(err, "no error creating registry client")
// create htpasswd file (w BCrypt, which is required) // create htpasswd file (w BCrypt, which is required)
pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost)
suite.Nil(err, "no error generating bcrypt password for test htpasswd file") suite.Nil(err, "no error generating bcrypt password for test htpasswd file")
htpasswdPath := filepath.Join(suite.CacheRootDir, testHtpasswdFileBasename) htpasswdPath := filepath.Join(suite.WorkspaceDir, testHtpasswdFileBasename)
err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644) err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644)
suite.Nil(err, "no error creating test htpasswd file") suite.Nil(err, "no error creating test htpasswd file")
@ -109,7 +87,7 @@ func (suite *RegistryClientTestSuite) SetupSuite() {
port, err := freeport.GetFreePort() port, err := freeport.GetFreePort()
suite.Nil(err, "no error finding free port for test registry") suite.Nil(err, "no error finding free port for test registry")
suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port) suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port)
config.HTTP.Addr = fmt.Sprintf(":%d", port) config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port)
config.HTTP.DrainTimeout = time.Duration(10) * time.Second config.HTTP.DrainTimeout = time.Duration(10) * time.Second
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
config.Auth = configuration.Auth{ config.Auth = configuration.Auth{
@ -128,110 +106,195 @@ func (suite *RegistryClientTestSuite) SetupSuite() {
} }
func (suite *RegistryClientTestSuite) TearDownSuite() { func (suite *RegistryClientTestSuite) TearDownSuite() {
os.RemoveAll(suite.CacheRootDir) os.RemoveAll(suite.WorkspaceDir)
} }
func (suite *RegistryClientTestSuite) Test_0_Login() { func (suite *RegistryClientTestSuite) Test_0_Login() {
err := suite.RegistryClient.Login(suite.DockerRegistryHost, "badverybad", "ohsobad", false) err := suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth("badverybad", "ohsobad"),
LoginOptInsecure(false))
suite.NotNil(err, "error logging into registry with bad credentials") suite.NotNil(err, "error logging into registry with bad credentials")
err = suite.RegistryClient.Login(suite.DockerRegistryHost, "badverybad", "ohsobad", true) err = suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth("badverybad", "ohsobad"),
LoginOptInsecure(true))
suite.NotNil(err, "error logging into registry with bad credentials, insecure mode") suite.NotNil(err, "error logging into registry with bad credentials, insecure mode")
err = suite.RegistryClient.Login(suite.DockerRegistryHost, testUsername, testPassword, false) err = suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth(testUsername, testPassword),
LoginOptInsecure(false))
suite.Nil(err, "no error logging into registry with good credentials") suite.Nil(err, "no error logging into registry with good credentials")
err = suite.RegistryClient.Login(suite.DockerRegistryHost, testUsername, testPassword, true) err = suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth(testUsername, testPassword),
LoginOptInsecure(true))
suite.Nil(err, "no error logging into registry with good credentials, insecure mode") suite.Nil(err, "no error logging into registry with good credentials, insecure mode")
} }
func (suite *RegistryClientTestSuite) Test_1_SaveChart() { func (suite *RegistryClientTestSuite) Test_1_Push() {
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) // Bad bytes
suite.Nil(err) ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)
_, err := suite.RegistryClient.Push([]byte("hello"), ref)
// empty chart suite.NotNil(err, "error pushing non-chart bytes")
err = suite.RegistryClient.SaveChart(&chart.Chart{}, ref)
suite.NotNil(err) // Load a test chart
chartData, err := ioutil.ReadFile("../../../pkg/repo/repotest/testdata/examplechart-0.1.0.tgz")
// valid chart suite.Nil(err, "no error loading test chart")
ch := &chart.Chart{} meta, err := extractChartMeta(chartData)
ch.Metadata = &chart.Metadata{ suite.Nil(err, "no error extracting chart meta")
APIVersion: "v1",
Name: "testchart", // non-strict ref (chart name)
Version: "1.2.3", ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version)
} _, err = suite.RegistryClient.Push(chartData, ref)
err = suite.RegistryClient.SaveChart(ch, ref) suite.NotNil(err, "error pushing non-strict ref (bad basename)")
suite.Nil(err)
// non-strict ref (chart name), with strict mode disabled
_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false))
suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled")
// non-strict ref (chart version)
ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name)
_, err = suite.RegistryClient.Push(chartData, ref)
suite.NotNil(err, "error pushing non-strict ref (bad tag)")
// non-strict ref (chart version), with strict mode disabled
_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false))
suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled")
// basic push, good ref
chartData, err = ioutil.ReadFile("../../../pkg/downloader/testdata/local-subchart-0.1.0.tgz")
suite.Nil(err, "no error loading test chart")
meta, err = extractChartMeta(chartData)
suite.Nil(err, "no error extracting chart meta")
ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
_, err = suite.RegistryClient.Push(chartData, ref)
suite.Nil(err, "no error pushing good ref")
_, err = suite.RegistryClient.Pull(ref)
suite.Nil(err, "no error pulling a simple chart")
// Load another test chart
chartData, err = ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz")
suite.Nil(err, "no error loading test chart")
meta, err = extractChartMeta(chartData)
suite.Nil(err, "no error extracting chart meta")
// Load prov file
provData, err := ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz.prov")
suite.Nil(err, "no error loading test prov")
// push with prov
ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData))
suite.Nil(err, "no error pushing good ref with prov")
_, err = suite.RegistryClient.Pull(ref)
suite.Nil(err, "no error pulling a simple chart")
// Validate the output
// Note: these digests/sizes etc may change if the test chart/prov files are modified,
// or if the format of the OCI manifest changes
suite.Equal(ref, result.Ref)
suite.Equal(meta.Name, result.Chart.Meta.Name)
suite.Equal(meta.Version, result.Chart.Meta.Version)
suite.Equal(int64(512), result.Manifest.Size)
suite.Equal(int64(99), result.Config.Size)
suite.Equal(int64(973), result.Chart.Size)
suite.Equal(int64(695), result.Prov.Size)
suite.Equal(
"sha256:af4c20a1df1431495e673c14ecfa3a2ba24839a7784349d6787cd67957392e83",
result.Manifest.Digest)
suite.Equal(
"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
result.Config.Digest)
suite.Equal(
"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
result.Chart.Digest)
suite.Equal(
"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
result.Prov.Digest)
} }
func (suite *RegistryClientTestSuite) Test_2_LoadChart() { func (suite *RegistryClientTestSuite) Test_2_Pull() {
// bad/missing ref
// non-existent ref ref := fmt.Sprintf("%s/testrepo/no-existy:1.2.3", suite.DockerRegistryHost)
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) _, err := suite.RegistryClient.Pull(ref)
suite.Nil(err) suite.NotNil(err, "error on bad/missing ref")
_, err = suite.RegistryClient.LoadChart(ref)
suite.NotNil(err) // Load test chart (to build ref pushed in previous test)
chartData, err := ioutil.ReadFile("../../../pkg/downloader/testdata/local-subchart-0.1.0.tgz")
// existing ref suite.Nil(err, "no error loading test chart")
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) meta, err := extractChartMeta(chartData)
suite.Nil(err) suite.Nil(err, "no error extracting chart meta")
ch, err := suite.RegistryClient.LoadChart(ref) ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
suite.Nil(err)
suite.Equal("testchart", ch.Metadata.Name) // Simple pull, chart only
suite.Equal("1.2.3", ch.Metadata.Version) _, err = suite.RegistryClient.Pull(ref)
} suite.Nil(err, "no error pulling a simple chart")
func (suite *RegistryClientTestSuite) Test_3_PushChart() { // Simple pull with prov (no prov uploaded)
_, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true))
// non-existent ref suite.NotNil(err, "error pulling a chart with prov when no prov exists")
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
suite.Nil(err) // Simple pull with prov, ignoring missing prov
err = suite.RegistryClient.PushChart(ref) _, err = suite.RegistryClient.Pull(ref,
suite.NotNil(err) PullOptWithProv(true),
PullOptIgnoreMissingProv(true))
// existing ref suite.Nil(err,
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) "no error pulling a chart with prov when no prov exists, ignoring missing")
suite.Nil(err)
err = suite.RegistryClient.PushChart(ref) // Load test chart (to build ref pushed in previous test)
suite.Nil(err) chartData, err = ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz")
} suite.Nil(err, "no error loading test chart")
meta, err = extractChartMeta(chartData)
func (suite *RegistryClientTestSuite) Test_4_PullChart() { suite.Nil(err, "no error extracting chart meta")
ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
// non-existent ref
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) // Load prov file
suite.Nil(err) provData, err := ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz.prov")
_, err = suite.RegistryClient.PullChart(ref) suite.Nil(err, "no error loading test prov")
suite.NotNil(err)
// no chart and no prov causes error
// existing ref _, err = suite.RegistryClient.Pull(ref,
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) PullOptWithChart(false),
suite.Nil(err) PullOptWithProv(false))
_, err = suite.RegistryClient.PullChart(ref) suite.NotNil(err, "error on both no chart and no prov")
suite.Nil(err)
} // full pull with chart and prov
result, err := suite.RegistryClient.Pull(ref, PullOptWithProv(true))
func (suite *RegistryClientTestSuite) Test_5_PrintChartTable() { suite.Nil(err, "no error pulling a chart with prov")
err := suite.RegistryClient.PrintChartTable()
suite.Nil(err) // Validate the output
} // Note: these digests/sizes etc may change if the test chart/prov files are modified,
// or if the format of the OCI manifest changes
func (suite *RegistryClientTestSuite) Test_6_RemoveChart() { suite.Equal(ref, result.Ref)
suite.Equal(meta.Name, result.Chart.Meta.Name)
// non-existent ref suite.Equal(meta.Version, result.Chart.Meta.Version)
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) suite.Equal(int64(512), result.Manifest.Size)
suite.Nil(err) suite.Equal(int64(99), result.Config.Size)
err = suite.RegistryClient.RemoveChart(ref) suite.Equal(int64(973), result.Chart.Size)
suite.NotNil(err) suite.Equal(int64(695), result.Prov.Size)
suite.Equal(
// existing ref "sha256:af4c20a1df1431495e673c14ecfa3a2ba24839a7784349d6787cd67957392e83",
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) result.Manifest.Digest)
suite.Nil(err) suite.Equal(
err = suite.RegistryClient.RemoveChart(ref) "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
suite.Nil(err) result.Config.Digest)
suite.Equal(
"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
result.Chart.Digest)
suite.Equal(
"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
result.Prov.Digest)
suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}]}",
string(result.Manifest.Data))
suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}",
string(result.Config.Data))
suite.Equal(chartData, result.Chart.Data)
suite.Equal(provData, result.Prov.Data)
} }
func (suite *RegistryClientTestSuite) Test_7_Logout() { func (suite *RegistryClientTestSuite) Test_3_Logout() {
err := suite.RegistryClient.Logout("this-host-aint-real:5000") err := suite.RegistryClient.Logout("this-host-aint-real:5000")
suite.NotNil(err, "error logging out of registry that has no entry") suite.NotNil(err, "error logging out of registry that has no entry")
@ -239,12 +302,11 @@ func (suite *RegistryClientTestSuite) Test_7_Logout() {
suite.Nil(err, "no error logging out of registry") suite.Nil(err, "no error logging out of registry")
} }
func (suite *RegistryClientTestSuite) Test_8_ManInTheMiddle() { func (suite *RegistryClientTestSuite) Test_4_ManInTheMiddle() {
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/supposedlysafechart:9.9.9", suite.CompromisedRegistryHost)) ref := fmt.Sprintf("%s/testrepo/supposedlysafechart:9.9.9", suite.CompromisedRegistryHost)
suite.Nil(err)
// returns content that does not match the expected digest // returns content that does not match the expected digest
_, err = suite.RegistryClient.PullChart(ref) _, err := suite.RegistryClient.Pull(ref)
suite.NotNil(err) suite.NotNil(err)
suite.True(errdefs.IsFailedPrecondition(err)) suite.True(errdefs.IsFailedPrecondition(err))
} }
@ -261,19 +323,19 @@ func initCompromisedRegistryTestServer() string {
// layers[0] is the blob []byte("a") // layers[0] is the blob []byte("a")
w.Write([]byte( w.Write([]byte(
`{ "schemaVersion": 2, "config": { fmt.Sprintf(`{ "schemaVersion": 2, "config": {
"mediaType": "application/vnd.cncf.helm.config.v1+json", "mediaType": "%s",
"digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133", "digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133",
"size": 181 "size": 181
}, },
"layers": [ "layers": [
{ {
"mediaType": "application/tar+gzip", "mediaType": "%s",
"digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", "digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
"size": 1 "size": 1
} }
] ]
}`)) }`, ConfigMediaType, ChartLayerMediaType)))
} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" { } else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200) w.WriteHeader(200)
@ -281,7 +343,7 @@ func initCompromisedRegistryTestServer() string {
"an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" + "an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" +
"\"application\"}")) "\"application\"}"))
} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" { } else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" {
w.Header().Set("Content-Type", "application/tar+gzip") w.Header().Set("Content-Type", ChartLayerMediaType)
w.WriteHeader(200) w.WriteHeader(200)
w.Write([]byte("b")) w.Write([]byte("b"))
} else { } else {

@ -17,17 +17,21 @@ limitations under the License.
package registry // import "helm.sh/helm/v3/internal/experimental/registry" package registry // import "helm.sh/helm/v3/internal/experimental/registry"
const ( const (
// HelmChartConfigMediaType is the reserved media type for the Helm chart manifest config // OCIScheme is the URL scheme for OCI-based requests
HelmChartConfigMediaType = "application/vnd.cncf.helm.config.v1+json" OCIScheme = "oci"
// HelmChartContentLayerMediaType is the reserved media type for Helm chart package content // CredentialsFileBasename is the filename for auth credentials file
HelmChartContentLayerMediaType = "application/tar+gzip" CredentialsFileBasename = "registry.json"
)
// ConfigMediaType is the reserved media type for the Helm chart manifest config
ConfigMediaType = "application/vnd.cncf.helm.config.v1+json"
// ChartLayerMediaType is the reserved media type for Helm chart package content
ChartLayerMediaType = "application/vnd.cncf.helm.chart.content.v1.tar+gzip"
// KnownMediaTypes returns a list of layer mediaTypes that the Helm client knows about // ProvLayerMediaType is the reserved media type for Helm chart provenance files
func KnownMediaTypes() []string { ProvLayerMediaType = "application/vnd.cncf.helm.chart.provenance.v1.prov"
return []string{
HelmChartConfigMediaType, // LegacyChartLayerMediaType is the legacy reserved media type for Helm chart package content.
HelmChartContentLayerMediaType, LegacyChartLayerMediaType = "application/tar+gzip"
} )
}

@ -1,146 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import (
"errors"
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
)
var (
validPortRegEx = regexp.MustCompile(`^([1-9]\d{0,3}|0|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$`) // adapted from https://stackoverflow.com/a/12968117
// TODO: Currently we don't support digests, so we are only splitting on the
// colon. However, when we add support for digests, we'll need to use the
// regexp anyway to split on both colons and @, so leaving it like this for
// now
referenceDelimiter = regexp.MustCompile(`[:]`)
errEmptyRepo = errors.New("parsed repo was empty")
errTooManyColons = errors.New("ref may only contain a single colon character (:) unless specifying a port number")
)
type (
// Reference defines the main components of a reference specification
Reference struct {
Tag string
Repo string
}
)
// ParseReference converts a string to a Reference
func ParseReference(s string) (*Reference, error) {
if s == "" {
return nil, errEmptyRepo
}
// Split the components of the string on the colon or @, if it is more than 3,
// immediately return an error. Other validation will be performed later in
// the function
splitComponents := fixSplitComponents(referenceDelimiter.Split(s, -1))
if len(splitComponents) > 3 {
return nil, errTooManyColons
}
var ref *Reference
switch len(splitComponents) {
case 1:
ref = &Reference{Repo: splitComponents[0]}
case 2:
ref = &Reference{Repo: splitComponents[0], Tag: splitComponents[1]}
case 3:
ref = &Reference{Repo: strings.Join(splitComponents[:2], ":"), Tag: splitComponents[2]}
}
// ensure the reference is valid
err := ref.validate()
if err != nil {
return nil, err
}
return ref, nil
}
// FullName the full name of a reference (repo:tag)
func (ref *Reference) FullName() string {
if ref.Tag == "" {
return ref.Repo
}
return fmt.Sprintf("%s:%s", ref.Repo, ref.Tag)
}
// validate makes sure the ref meets our criteria
func (ref *Reference) validate() error {
err := ref.validateRepo()
if err != nil {
return err
}
return ref.validateNumColons()
}
// validateRepo checks that the Repo field is non-empty
func (ref *Reference) validateRepo() error {
if ref.Repo == "" {
return errEmptyRepo
}
// Makes sure the repo results in a parsable URL (similar to what is done
// with containerd reference parsing)
_, err := url.Parse("//" + ref.Repo)
return err
}
// validateNumColons ensures the ref only contains a single colon character (:)
// (or potentially two, there might be a port number specified i.e. :5000)
func (ref *Reference) validateNumColons() error {
if strings.Contains(ref.Tag, ":") {
return errTooManyColons
}
parts := strings.Split(ref.Repo, ":")
lastIndex := len(parts) - 1
if 1 < lastIndex {
return errTooManyColons
}
if 0 < lastIndex {
port := strings.Split(parts[lastIndex], "/")[0]
if !isValidPort(port) {
return errTooManyColons
}
}
return nil
}
// isValidPort returns whether or not a string looks like a valid port
func isValidPort(s string) bool {
return validPortRegEx.MatchString(s)
}
// fixSplitComponents this will modify reference parts based on presence of port
// Example: {localhost, 5000/x/y/z, 0.1.0} => {localhost:5000/x/y/z, 0.1.0}
func fixSplitComponents(c []string) []string {
if len(c) <= 1 {
return c
}
possiblePortParts := strings.Split(c[1], "/")
if _, err := strconv.Atoi(possiblePortParts[0]); err == nil {
components := []string{strings.Join(c[:2], ":")}
components = append(components, c[2:]...)
return components
}
return c
}

@ -1,126 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseReference(t *testing.T) {
is := assert.New(t)
// bad refs
s := ""
_, err := ParseReference(s)
is.Error(err, "empty ref")
s = "my:bad:ref"
_, err = ParseReference(s)
is.Error(err, "ref contains too many colons (2)")
s = "my:really:bad:ref"
_, err = ParseReference(s)
is.Error(err, "ref contains too many colons (3)")
// good refs
s = "mychart"
ref, err := ParseReference(s)
is.NoError(err)
is.Equal("mychart", ref.Repo)
is.Equal("", ref.Tag)
is.Equal("mychart", ref.FullName())
s = "mychart:1.5.0"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("mychart", ref.Repo)
is.Equal("1.5.0", ref.Tag)
is.Equal("mychart:1.5.0", ref.FullName())
s = "myrepo/mychart"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("myrepo/mychart", ref.Repo)
is.Equal("", ref.Tag)
is.Equal("myrepo/mychart", ref.FullName())
s = "myrepo/mychart:1.5.0"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("myrepo/mychart", ref.Repo)
is.Equal("1.5.0", ref.Tag)
is.Equal("myrepo/mychart:1.5.0", ref.FullName())
s = "mychart:5001:1.5.0"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("mychart:5001", ref.Repo)
is.Equal("1.5.0", ref.Tag)
is.Equal("mychart:5001:1.5.0", ref.FullName())
s = "myrepo:5001/mychart:1.5.0"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("myrepo:5001/mychart", ref.Repo)
is.Equal("1.5.0", ref.Tag)
is.Equal("myrepo:5001/mychart:1.5.0", ref.FullName())
s = "127.0.0.1:5001/mychart:1.5.0"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("127.0.0.1:5001/mychart", ref.Repo)
is.Equal("1.5.0", ref.Tag)
is.Equal("127.0.0.1:5001/mychart:1.5.0", ref.FullName())
s = "localhost:5000/mychart:latest"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("localhost:5000/mychart", ref.Repo)
is.Equal("latest", ref.Tag)
is.Equal("localhost:5000/mychart:latest", ref.FullName())
s = "my.host.com/my/nested/repo:1.2.3"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("my.host.com/my/nested/repo", ref.Repo)
is.Equal("1.2.3", ref.Tag)
is.Equal("my.host.com/my/nested/repo:1.2.3", ref.FullName())
s = "localhost:5000/x/y/z"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("localhost:5000/x/y/z", ref.Repo)
is.Equal("", ref.Tag)
is.Equal("localhost:5000/x/y/z", ref.FullName())
s = "localhost:5000/x/y/z:123"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("localhost:5000/x/y/z", ref.Repo)
is.Equal("123", ref.Tag)
is.Equal("localhost:5000/x/y/z:123", ref.FullName())
s = "localhost:5000/x/y/z:123:x"
_, err = ParseReference(s)
is.Error(err, "ref contains too many colons (3)")
s = "localhost:5000/x/y/z:123:x:y"
_, err = ParseReference(s)
is.Error(err, "ref contains too many colons (4)")
}

@ -17,41 +17,31 @@ limitations under the License.
package registry // import "helm.sh/helm/v3/internal/experimental/registry" package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
"time" "strings"
orascontext "github.com/deislabs/oras/pkg/context"
units "github.com/docker/go-units"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
orascontext "oras.land/oras-go/pkg/context"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
) )
// byteCountBinary produces a human-readable file size // IsOCI determines whether or not a URL is to be treated as an OCI URL
func byteCountBinary(b int64) string { func IsOCI(url string) bool {
const unit = 1024 return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme))
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
} }
// shortDigest returns first 7 characters of a sha256 digest // extractChartMeta is used to extract a chart metadata from a byte array
func shortDigest(digest string) string { func extractChartMeta(chartData []byte) (*chart.Metadata, error) {
if len(digest) == 64 { ch, err := loader.LoadArchive(bytes.NewReader(chartData))
return digest[:7] if err != nil {
return nil, err
} }
return digest return ch.Metadata, nil
}
// timeAgo returns a human-readable timestamp representing time that has passed
func timeAgo(t time.Time) string {
return units.HumanDuration(time.Now().UTC().Sub(t))
} }
// ctx retrieves a fresh context. // ctx retrieves a fresh context.

@ -0,0 +1,58 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package uploader
import (
"fmt"
"io"
"net/url"
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/pusher"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartUploader handles uploading a chart.
type ChartUploader struct {
// Out is the location to write warning and info messages.
Out io.Writer
// Pusher collection for the operation
Pushers pusher.Providers
// Options provide parameters to be passed along to the Pusher being initialized.
Options []pusher.Option
// RegistryClient is a client for interacting with registries.
RegistryClient *registry.Client
}
// UploadTo uploads a chart. Depending on the settings, it may also upload a provenance file.
func (c *ChartUploader) UploadTo(ref, remote string) error {
u, err := url.Parse(remote)
if err != nil {
return errors.Errorf("invalid chart URL format: %s", remote)
}
if u.Scheme == "" {
return errors.New(fmt.Sprintf("scheme prefix missing from remote (e.g. \"%s://\")", registry.OCIScheme))
}
p, err := c.Pushers.ByScheme(u.Scheme)
if err != nil {
return err
}
return p.Push(ref, u.String(), c.Options...)
}

@ -1,11 +1,10 @@
/* /*
Copyright The Helm Authors. Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
@ -14,15 +13,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package registry // import "helm.sh/helm/v3/internal/experimental/registry" /*Package uploader provides a library for uploading charts.
import (
"github.com/deislabs/oras/pkg/auth"
)
type ( This package contains tools for uploading charts to registries.
// Authorizer handles registry auth operations */
Authorizer struct { package uploader
auth.Client
}
)

@ -26,6 +26,7 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/pkg/errors" "github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/gates" "helm.sh/helm/v3/pkg/gates"
@ -57,6 +58,11 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
locked := make([]*chart.Dependency, len(reqs)) locked := make([]*chart.Dependency, len(reqs))
missing := []string{} missing := []string{}
for i, d := range reqs { for i, d := range reqs {
constraint, err := semver.NewConstraint(d.Version)
if err != nil {
return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name)
}
if d.Repository == "" { if d.Repository == "" {
// Local chart subfolder // Local chart subfolder
if _, err := GetLocalPath(filepath.Join("charts", d.Name), r.chartpath); err != nil { if _, err := GetLocalPath(filepath.Join("charts", d.Name), r.chartpath); err != nil {
@ -77,13 +83,22 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
return nil, err return nil, err
} }
// The version of the chart locked will be the version of the chart
// currently listed in the file system within the chart.
ch, err := loader.LoadDir(chartpath) ch, err := loader.LoadDir(chartpath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
v, err := semver.NewVersion(ch.Metadata.Version)
if err != nil {
// Not a legit entry.
continue
}
if !constraint.Check(v) {
missing = append(missing, d.Name)
continue
}
locked[i] = &chart.Dependency{ locked[i] = &chart.Dependency{
Name: d.Name, Name: d.Name,
Repository: d.Repository, Repository: d.Repository,
@ -92,11 +107,6 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
continue continue
} }
constraint, err := semver.NewConstraint(d.Version)
if err != nil {
return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name)
}
repoName := repoNames[d.Name] repoName := repoNames[d.Name]
// if the repository was not defined, but the dependency defines a repository url, bypass the cache // if the repository was not defined, but the dependency defines a repository url, bypass the cache
if repoName == "" && d.Repository != "" { if repoName == "" && d.Repository != "" {
@ -112,7 +122,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
var version string var version string
var ok bool var ok bool
found := true found := true
if !strings.HasPrefix(d.Repository, "oci://") { if !registry.IsOCI(d.Repository) {
repoIndex, err := repo.LoadIndexFile(filepath.Join(r.cachepath, helmpath.CacheIndexFile(repoName))) repoIndex, err := repo.LoadIndexFile(filepath.Join(r.cachepath, helmpath.CacheIndexFile(repoName)))
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "no cached repository for %s found. (try 'helm repo update')", repoName) return nil, errors.Wrapf(err, "no cached repository for %s found. (try 'helm repo update')", repoName)

@ -16,6 +16,7 @@ limitations under the License.
package resolver package resolver
import ( import (
"runtime"
"testing" "testing"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
@ -28,6 +29,18 @@ func TestResolve(t *testing.T) {
expect *chart.Lock expect *chart.Lock
err bool err bool
}{ }{
{
name: "repo from invalid version",
req: []*chart.Dependency{
{Name: "base", Repository: "file://base", Version: "1.1.0"},
},
expect: &chart.Lock{
Dependencies: []*chart.Dependency{
{Name: "base", Repository: "file://base", Version: "0.1.0"},
},
},
err: true,
},
{ {
name: "version failure", name: "version failure",
req: []*chart.Dependency{ req: []*chart.Dependency{
@ -234,24 +247,28 @@ func TestGetLocalPath(t *testing.T) {
repo string repo string
chartpath string chartpath string
expect string expect string
winExpect string
err bool err bool
}{ }{
{ {
name: "absolute path", name: "absolute path",
repo: "file:////tmp", repo: "file:////",
expect: "/tmp", expect: "/",
winExpect: "\\",
}, },
{ {
name: "relative path", name: "relative path",
repo: "file://../../testdata/chartpath/base", repo: "file://../../testdata/chartpath/base",
chartpath: "foo/bar", chartpath: "foo/bar",
expect: "testdata/chartpath/base", expect: "testdata/chartpath/base",
winExpect: "testdata\\chartpath\\base",
}, },
{ {
name: "current directory path", name: "current directory path",
repo: "../charts/localdependency", repo: "../charts/localdependency",
chartpath: "testdata/chartpath/charts", chartpath: "testdata/chartpath/charts",
expect: "testdata/chartpath/charts/localdependency", expect: "testdata/chartpath/charts/localdependency",
winExpect: "testdata\\chartpath\\charts\\localdependency",
}, },
{ {
name: "invalid local path", name: "invalid local path",
@ -279,8 +296,12 @@ func TestGetLocalPath(t *testing.T) {
if tt.err { if tt.err {
t.Fatalf("Expected error in test %q", tt.name) t.Fatalf("Expected error in test %q", tt.name)
} }
if p != tt.expect { expect := tt.expect
t.Errorf("%q: expected %q, got %q", tt.name, tt.expect, p) if runtime.GOOS == "windows" {
expect = tt.winExpect
}
if p != expect {
t.Errorf("%q: expected %q, got %q", tt.name, expect, p)
} }
}) })
} }

@ -33,7 +33,6 @@ package fs
import ( import (
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -119,7 +118,7 @@ func CopyDir(src, dst string) error {
return errors.Wrapf(err, "cannot mkdir %s", dst) return errors.Wrapf(err, "cannot mkdir %s", dst)
} }
entries, err := ioutil.ReadDir(src) entries, err := os.ReadDir(src)
if err != nil { if err != nil {
return errors.Wrapf(err, "cannot read directory %s", dst) return errors.Wrapf(err, "cannot read directory %s", dst)
} }

@ -29,7 +29,7 @@ var (
// //
// Increment major number for new feature additions and behavioral changes. // Increment major number for new feature additions and behavioral changes.
// Increment minor number for bug fixes and performance enhancements. // Increment minor number for bug fixes and performance enhancements.
version = "v3.5" version = "v3.7"
// metadata is extra build time data // metadata is extra build time data
metadata = "" metadata = ""

@ -102,11 +102,11 @@ type Configuration struct {
// TODO: This function is badly in need of a refactor. // TODO: This function is badly in need of a refactor.
// TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed
// This code has to do with writing files to disk. // This code has to do with writing files to disk.
func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun bool) ([]*release.Hook, *bytes.Buffer, string, error) { func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun bool) ([]*release.Hook, *bytes.Buffer, string, error) {
hs := []*release.Hook{} hs := []*release.Hook{}
b := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)
caps, err := c.getCapabilities() caps, err := cfg.getCapabilities()
if err != nil { if err != nil {
return hs, b, "", err return hs, b, "", err
} }
@ -125,12 +125,12 @@ func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values
// is mocked. It is not up to the template author to decide when the user wants to // is mocked. It is not up to the template author to decide when the user wants to
// connect to the cluster. So when the user says to dry run, respect the user's // connect to the cluster. So when the user says to dry run, respect the user's
// wishes and do not connect to the cluster. // wishes and do not connect to the cluster.
if !dryRun && c.RESTClientGetter != nil { if !dryRun && cfg.RESTClientGetter != nil {
rest, err := c.RESTClientGetter.ToRESTConfig() restConfig, err := cfg.RESTClientGetter.ToRESTConfig()
if err != nil { if err != nil {
return hs, b, "", err return hs, b, "", err
} }
files, err2 = engine.RenderWithClient(ch, values, rest) files, err2 = engine.RenderWithClient(ch, values, restConfig)
} else { } else {
files, err2 = engine.Render(ch, values) files, err2 = engine.Render(ch, values)
} }
@ -236,11 +236,11 @@ type RESTClientGetter interface {
type DebugLog func(format string, v ...interface{}) type DebugLog func(format string, v ...interface{})
// capabilities builds a Capabilities from discovery information. // capabilities builds a Capabilities from discovery information.
func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) { func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
if c.Capabilities != nil { if cfg.Capabilities != nil {
return c.Capabilities, nil return cfg.Capabilities, nil
} }
dc, err := c.RESTClientGetter.ToDiscoveryClient() dc, err := cfg.RESTClientGetter.ToDiscoveryClient()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not get Kubernetes discovery client") return nil, errors.Wrap(err, "could not get Kubernetes discovery client")
} }
@ -258,14 +258,14 @@ func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
apiVersions, err := GetVersionSet(dc) apiVersions, err := GetVersionSet(dc)
if err != nil { if err != nil {
if discovery.IsGroupDiscoveryFailedError(err) { if discovery.IsGroupDiscoveryFailedError(err) {
c.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err) cfg.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err)
c.Log("WARNING: To fix this, kubectl delete apiservice <service-name>") cfg.Log("WARNING: To fix this, kubectl delete apiservice <service-name>")
} else { } else {
return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes") return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes")
} }
} }
c.Capabilities = &chartutil.Capabilities{ cfg.Capabilities = &chartutil.Capabilities{
APIVersions: apiVersions, APIVersions: apiVersions,
KubeVersion: chartutil.KubeVersion{ KubeVersion: chartutil.KubeVersion{
Version: kubeVersion.GitVersion, Version: kubeVersion.GitVersion,
@ -273,12 +273,12 @@ func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
Minor: kubeVersion.Minor, Minor: kubeVersion.Minor,
}, },
} }
return c.Capabilities, nil return cfg.Capabilities, nil
} }
// KubernetesClientSet creates a new kubernetes ClientSet based on the configuration // KubernetesClientSet creates a new kubernetes ClientSet based on the configuration
func (c *Configuration) KubernetesClientSet() (kubernetes.Interface, error) { func (cfg *Configuration) KubernetesClientSet() (kubernetes.Interface, error) {
conf, err := c.RESTClientGetter.ToRESTConfig() conf, err := cfg.RESTClientGetter.ToRESTConfig()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "unable to generate config for kubernetes client") return nil, errors.Wrap(err, "unable to generate config for kubernetes client")
} }
@ -290,20 +290,20 @@ func (c *Configuration) KubernetesClientSet() (kubernetes.Interface, error) {
// //
// If the configuration has a Timestamper on it, that will be used. // If the configuration has a Timestamper on it, that will be used.
// Otherwise, this will use time.Now(). // Otherwise, this will use time.Now().
func (c *Configuration) Now() time.Time { func (cfg *Configuration) Now() time.Time {
return Timestamper() return Timestamper()
} }
func (c *Configuration) releaseContent(name string, version int) (*release.Release, error) { func (cfg *Configuration) releaseContent(name string, version int) (*release.Release, error) {
if err := chartutil.ValidateReleaseName(name); err != nil { if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name) return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name)
} }
if version <= 0 { if version <= 0 {
return c.Releases.Last(name) return cfg.Releases.Last(name)
} }
return c.Releases.Get(name, version) return cfg.Releases.Get(name, version)
} }
// GetVersionSet retrieves a set of available k8s API versions // GetVersionSet retrieves a set of available k8s API versions
@ -355,14 +355,14 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.Version
} }
// recordRelease with an update operation in case reuse has been set. // recordRelease with an update operation in case reuse has been set.
func (c *Configuration) recordRelease(r *release.Release) { func (cfg *Configuration) recordRelease(r *release.Release) {
if err := c.Releases.Update(r); err != nil { if err := cfg.Releases.Update(r); err != nil {
c.Log("warning: Failed to update release %s: %s", r.Name, err) cfg.Log("warning: Failed to update release %s: %s", r.Name, err)
} }
} }
// Init initializes the action configuration // Init initializes the action configuration
func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error { func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error {
kc := kube.New(getter) kc := kube.New(getter)
kc.Log = log kc.Log = log
@ -383,8 +383,8 @@ func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespac
store = storage.Init(d) store = storage.Init(d)
case "memory": case "memory":
var d *driver.Memory var d *driver.Memory
if c.Releases != nil { if cfg.Releases != nil {
if mem, ok := c.Releases.Driver.(*driver.Memory); ok { if mem, ok := cfg.Releases.Driver.(*driver.Memory); ok {
// This function can be called more than once (e.g., helm list --all-namespaces). // This function can be called more than once (e.g., helm list --all-namespaces).
// If a memory driver was already initialized, re-use it but set the possibly new namespace. // If a memory driver was already initialized, re-use it but set the possibly new namespace.
// We re-use it in case some releases where already created in the existing memory driver. // We re-use it in case some releases where already created in the existing memory driver.
@ -411,10 +411,10 @@ func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespac
panic("Unknown driver in HELM_DRIVER: " + helmDriver) panic("Unknown driver in HELM_DRIVER: " + helmDriver)
} }
c.RESTClientGetter = getter cfg.RESTClientGetter = getter
c.KubeClient = kc cfg.KubeClient = kc
c.Releases = store cfg.Releases = store
c.Log = log cfg.Log = log
return nil return nil
} }

@ -16,15 +16,11 @@ limitations under the License.
package action package action
import ( import (
"context"
"flag" "flag"
"io/ioutil" "io/ioutil"
"net/http"
"os" "os"
"path/filepath"
"testing" "testing"
dockerauth "github.com/deislabs/oras/pkg/auth/docker"
fakeclientset "k8s.io/client-go/kubernetes/fake" fakeclientset "k8s.io/client-go/kubernetes/fake"
"helm.sh/helm/v3/internal/experimental/registry" "helm.sh/helm/v3/internal/experimental/registry"
@ -42,16 +38,6 @@ var verbose = flag.Bool("test.log", false, "enable test logging")
func actionConfigFixture(t *testing.T) *Configuration { func actionConfigFixture(t *testing.T) *Configuration {
t.Helper() t.Helper()
client, err := dockerauth.NewClient()
if err != nil {
t.Fatal(err)
}
resolver, err := client.Resolver(context.Background(), http.DefaultClient, false)
if err != nil {
t.Fatal(err)
}
tdir, err := ioutil.TempDir("", "helm-action-test") tdir, err := ioutil.TempDir("", "helm-action-test")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -59,23 +45,7 @@ func actionConfigFixture(t *testing.T) *Configuration {
t.Cleanup(func() { os.RemoveAll(tdir) }) t.Cleanup(func() { os.RemoveAll(tdir) })
cache, err := registry.NewCache( registryClient, err := registry.NewClient()
registry.CacheOptDebug(true),
registry.CacheOptRoot(filepath.Join(tdir, registry.CacheRootDir)),
)
if err != nil {
t.Fatal(err)
}
registryClient, err := registry.NewClient(
registry.ClientOptAuthorizer(&registry.Authorizer{
Client: client,
}),
registry.ClientOptResolver(&registry.Resolver{
Resolver: resolver,
}),
registry.ClientOptCache(cache),
)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

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

Loading…
Cancel
Save