Merge branch 'main' into error-on-failed-jobs-9285

pull/9950/head
Jeff Rosenberg 4 years ago committed by GitHub
commit b13f3a13d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

@ -72,6 +72,16 @@ To load completions for every new session, execute once:
You will need to start a new shell for this setup to take effect.
`
const powershellCompDesc = `
Generate the autocompletion script for powershell.
To load completions in your current shell session:
PS C:\> helm completion powershell | Out-String | Invoke-Expression
To load completions for every new session, add the output of the above command
to your powershell profile.
`
const (
noDescFlagName = "no-descriptions"
noDescFlagText = "disable completion descriptions"
@ -88,16 +98,16 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
}
bash := &cobra.Command{
Use: "bash",
Short: "generate autocompletion script for bash",
Long: bashCompDesc,
Args: require.NoArgs,
DisableFlagsInUseLine: true,
ValidArgsFunction: noCompletions,
Use: "bash",
Short: "generate autocompletion script for bash",
Long: bashCompDesc,
Args: require.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionBash(out, cmd)
},
}
bash.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
zsh := &cobra.Command{
Use: "zsh",
@ -123,13 +133,25 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
}
fish.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
cmd.AddCommand(bash, zsh, fish)
powershell := &cobra.Command{
Use: "powershell",
Short: "generate autocompletion script for powershell",
Long: powershellCompDesc,
Args: require.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionPowershell(out, cmd)
},
}
powershell.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
cmd.AddCommand(bash, zsh, fish, powershell)
return cmd
}
func runCompletionBash(out io.Writer, cmd *cobra.Command) error {
err := cmd.Root().GenBashCompletion(out)
err := cmd.Root().GenBashCompletionV2(out, !disableCompDescriptions)
// In case the user renamed the helm binary (e.g., to be able to run
// both helm2 and helm3), we hook the new binary name to the completion function
@ -180,6 +202,13 @@ func runCompletionFish(out io.Writer, cmd *cobra.Command) error {
return cmd.Root().GenFishCompletion(out, !disableCompDescriptions)
}
func runCompletionPowershell(out io.Writer, cmd *cobra.Command) error {
if disableCompDescriptions {
return cmd.Root().GenPowerShellCompletion(out)
}
return cmd.Root().GenPowerShellCompletionWithDesc(out)
}
// Function to disable file completion
func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp

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

@ -46,7 +46,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
ociChartName := "oci-depending-chart"
c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL)
if err := chartutil.SaveDir(c, ociSrv.Dir); err != nil {
if _, err := chartutil.Save(c, ociSrv.Dir); err != nil {
t.Fatal(err)
}
ociSrv.Run(t, repotest.WithDependingChart(c))
@ -133,6 +133,9 @@ func TestDependencyUpdateCmd(t *testing.T) {
}
// test for OCI charts
if err := chartutil.SaveDir(c, dir()); err != nil {
t.Fatal(err)
}
cmd := fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json",
dir(ociChartName),
dir("repositories.yaml"),

@ -17,8 +17,13 @@ limitations under the License.
package main
import (
"context"
"fmt"
"io"
"log"
"os"
"os/signal"
"syscall"
"time"
"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 {
rel, err := runInstall(args, client, valueOpts, out)
if err != nil {
return err
return errors.Wrap(err, "INSTALLATION FAILED")
}
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false})
@ -182,6 +187,10 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
}
client.ReleaseName = name
if err := checkOCI(chart); err != nil {
return nil, err
}
cp, err := client.ChartPathOptions.LocateChart(chart, settings)
if err != nil {
return nil, err
@ -239,7 +248,21 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
}
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)
// Handle SIGTERM
cSignal := make(chan os.Signal)
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

@ -169,7 +169,7 @@ func TestInstall(t *testing.T) {
name: "install library chart",
cmd: "install libchart testdata/testcharts/lib-chart",
wantError: true,
golden: "output/template-lib-chart.txt",
golden: "output/install-lib-chart.txt",
},
// Install, chart with bad type
{

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

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

@ -29,6 +29,7 @@ import (
"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"
)
@ -54,7 +55,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
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
}
// Adapted from https://github.com/deislabs/oras
// Adapted from https://github.com/oras-project/oras
func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) {
var err error
username := usernameOpt
@ -110,7 +111,7 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd
return username, password, nil
}
// Copied/adapted from https://github.com/deislabs/oras
// Copied/adapted from https://github.com/oras-project/oras
func readLine(prompt string, silent bool) (string, error) {
fmt.Print(prompt)
if silent {

@ -22,6 +22,7 @@ import (
"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"
)
@ -38,7 +39,7 @@ func newRegistryLogoutCmd(cfg *action.Configuration, out io.Writer) *cobra.Comma
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
hostname := args[0]
return action.NewRegistryLogout(cfg).Run(out, hostname)
return experimental.NewRegistryLogout(cfg).Run(out, hostname)
},
}
}

@ -48,6 +48,7 @@ type repoAddOptions struct {
url string
username string
password string
passwordFromStdinOpt bool
passCredentialsAll bool
forceUpdate bool
allowDeprecatedRepos bool
@ -85,6 +86,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
f := cmd.Flags()
f.StringVar(&o.username, "username", "", "chart repository username")
f.StringVar(&o.password, "password", "", "chart repository password")
f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read chart repository password from stdin")
f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists")
f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.")
f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
@ -143,14 +145,24 @@ func (o *repoAddOptions) run(out io.Writer) error {
}
if o.username != "" && o.password == "" {
fd := int(os.Stdin.Fd())
fmt.Fprint(out, "Password: ")
password, err := term.ReadPassword(fd)
fmt.Fprintln(out)
if err != nil {
return err
if o.passwordFromStdinOpt {
passwordFromStdin, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
password := strings.TrimSuffix(string(passwordFromStdin), "\n")
password = strings.TrimSuffix(password, "\r")
o.password = password
} else {
fd := int(os.Stdin.Fd())
fmt.Fprint(out, "Password: ")
password, err := term.ReadPassword(fd)
fmt.Fprintln(out)
if err != nil {
return err
}
o.password = string(password)
}
o.password = string(password)
}
c := repo.Entry{

@ -21,6 +21,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"testing"
@ -204,3 +205,33 @@ func TestRepoAddFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo add reponame", false)
checkFileCompletion(t, "repo add reponame https://example.com", false)
}
func TestRepoAddWithPasswordFromStdin(t *testing.T) {
srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/testserver/*.*")
defer srv.Stop()
defer resetEnv()()
in, err := os.Open("testdata/password")
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
tmpdir := ensure.TempDir(t)
repoFile := filepath.Join(tmpdir, "repositories.yaml")
store := storageFixture()
const testName = "test-name"
const username = "username"
cmd := fmt.Sprintf("repo add %s %s --repository-config %s --repository-cache %s --username %s --password-stdin", testName, srv.URL(), repoFile, tmpdir, username)
var result string
_, result, err = executeActionCommandStdinC(store, in, cmd)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
if !strings.Contains(result, fmt.Sprintf("\"%s\" has been added to your repositories", testName)) {
t.Errorf("Repo was not successfully added. Output: %s", result)
}
}

@ -17,6 +17,7 @@ limitations under the License.
package main
import (
"fmt"
"io"
"strings"
@ -131,7 +132,7 @@ func compListRepos(prefix string, ignoredRepoNames []string) []string {
filteredRepos := filterRepos(f.Repositories, ignoredRepoNames)
for _, repo := range filteredRepos {
if strings.HasPrefix(repo.Name, prefix) {
rNames = append(rNames, repo.Name)
rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL))
}
}
}

@ -18,6 +18,7 @@ package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"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) {
checkFileCompletion(t, "repo remove", false)
checkFileCompletion(t, "repo remove repo1", false)

@ -41,10 +41,11 @@ To update all the repositories, use 'helm repo update'.
var errNoRepositories = errors.New("no repositories found. You must add one before updating")
type repoUpdateOptions struct {
update func([]*repo.ChartRepository, io.Writer)
repoFile string
repoCache string
names []string
update func([]*repo.ChartRepository, io.Writer, bool) error
repoFile string
repoCache string
names []string
failOnRepoUpdateFail bool
}
func newRepoUpdateCmd(out io.Writer) *cobra.Command {
@ -66,6 +67,13 @@ func newRepoUpdateCmd(out io.Writer) *cobra.Command {
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
}
@ -103,26 +111,34 @@ func (o *repoUpdateOptions) run(out io.Writer) error {
}
}
o.update(repos, out)
return nil
return o.update(repos, out, o.failOnRepoUpdateFail)
}
func updateCharts(repos []*repo.ChartRepository, out io.Writer) {
func updateCharts(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup
var repoFailList []string
for _, re := range repos {
wg.Add(1)
go func(re *repo.ChartRepository) {
defer wg.Done()
if _, err := re.DownloadIndexFile(); err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err)
repoFailList = append(repoFailList, re.Config.URL)
} else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name)
}
}(re)
}
wg.Wait()
if len(repoFailList) > 0 && failOnRepoUpdateFail {
return errors.New(fmt.Sprintf("Failed to update the following repositories: %s",
repoFailList))
}
fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
return nil
}
func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) error {

@ -35,10 +35,11 @@ func TestUpdateCmd(t *testing.T) {
var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer) {
updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos {
fmt.Fprintln(out, re.Config.Name)
}
return nil
}
o := &repoUpdateOptions{
update: updater,
@ -59,10 +60,11 @@ 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) {
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,
@ -84,10 +86,11 @@ 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) {
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,
@ -144,7 +147,7 @@ func TestUpdateCharts(t *testing.T) {
}
b := bytes.NewBuffer(nil)
updateCharts([]*repo.ChartRepository{r}, b)
updateCharts([]*repo.ChartRepository{r}, b, false)
got := b.String()
if strings.Contains(got, "Unable to get an update") {
@ -159,3 +162,79 @@ func TestRepoUpdateFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo update", false)
checkFileCompletion(t, "repo update repo1", false)
}
func TestUpdateChartsFail(t *testing.T) {
defer resetEnv()()
defer ensure.HelmHome(t)()
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
var invalidURL = ts.URL() + "55"
r, err := repo.NewChartRepository(&repo.Entry{
Name: "charts",
URL: invalidURL,
}, getter.All(settings))
if err != nil {
t.Error(err)
}
b := bytes.NewBuffer(nil)
if err := updateCharts([]*repo.ChartRepository{r}, b, false); err != nil {
t.Error("Repo update should not return error if update of repository fails")
}
got := b.String()
if !strings.Contains(got, "Unable to get an update") {
t.Errorf("Repo should have failed update but instead got: %q", got)
}
if !strings.Contains(got, "Update Complete.") {
t.Error("Update was not successful")
}
}
func TestUpdateChartsFailWithError(t *testing.T) {
defer resetEnv()()
defer ensure.HelmHome(t)()
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
var invalidURL = ts.URL() + "55"
r, err := repo.NewChartRepository(&repo.Entry{
Name: "charts",
URL: invalidURL,
}, getter.All(settings))
if err != nil {
t.Error(err)
}
b := bytes.NewBuffer(nil)
err = updateCharts([]*repo.ChartRepository{r}, b, true)
if err == nil {
t.Error("Repo update should return error because update of repository fails and 'fail-on-repo-update-fail' flag set")
return
}
var expectedErr = "Failed to update the following repositories"
var receivedErr = err.Error()
if !strings.Contains(receivedErr, expectedErr) {
t.Errorf("Expected error (%s) but got (%s) instead", expectedErr, receivedErr)
}
if !strings.Contains(receivedErr, invalidURL) {
t.Errorf("Expected invalid URL (%s) in error message but got (%s) instead", invalidURL, receivedErr)
}
got := b.String()
if !strings.Contains(got, "Unable to get an update") {
t.Errorf("Repo should have failed update but instead got: %q", got)
}
if strings.Contains(got, "Update Complete.") {
t.Error("Update was not successful and should return error message because 'fail-on-repo-update-fail' flag set")
}
}

@ -51,7 +51,7 @@ Environment variables:
| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. |
| $HELM_DATA_HOME | set an alternative location for storing Helm data. |
| $HELM_DEBUG | indicate whether or not Helm is running in Debug mode |
| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, postgres |
| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. |
| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. |
| $HELM_MAX_HISTORY | set the maximum number of helm release history. |
| $HELM_NAMESPACE | set the namespace used for the helm operations. |
@ -200,7 +200,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
// Add *experimental* subcommands
cmd.AddCommand(
newRegistryCmd(actionConfig, out),
newChartCmd(actionConfig, out),
newPushCmd(actionConfig, out),
)
// 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
}

@ -302,7 +302,14 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
// First check completions for repos
repos := compListRepos("", nil)
for _, repo := range repos {
for _, repoInfo := range repos {
// Split name from description
repoInfo := strings.Split(repoInfo, "\t")
repo := repoInfo[0]
repoDesc := ""
if len(repoInfo) > 1 {
repoDesc = repoInfo[1]
}
repoWithSlash := fmt.Sprintf("%s/", repo)
if strings.HasPrefix(toComplete, repoWithSlash) {
// Must complete with charts within the specified repo
@ -310,15 +317,15 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
noSpace = false
break
} else if strings.HasPrefix(repo, toComplete) {
// Must complete the repo name
completions = append(completions, repoWithSlash)
// Must complete the repo name with the slash, followed by the description
completions = append(completions, fmt.Sprintf("%s\t%s", repoWithSlash, repoDesc))
noSpace = true
}
}
cobra.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions), settings.Debug)
// Now handle completions for url prefixes
for _, url := range []string{"https://", "http://", "file://"} {
for _, url := range []string{"https://\tChart URL prefix", "http://\tChart URL prefix", "file://\tChart local URL prefix"} {
if strings.HasPrefix(toComplete, url) {
// The user already put in the full url prefix; we don't have
// anything to add, but make sure the shell does not default
@ -355,7 +362,7 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
// If the user didn't provide any input to completion,
// we provide a hint that a path can also be used
if includeFiles && len(toComplete) == 0 {
completions = append(completions, "./", "/")
completions = append(completions, "./\tRelative path prefix to local chart", "/\tAbsolute path prefix to local chart")
}
cobra.CompDebugln(fmt.Sprintf("Completions after checking empty input: %v", completions), settings.Debug)

@ -51,6 +51,11 @@ This command inspects a chart (directory, file, or URL) and displays the content
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 {
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 {
addShowFlags(subCmd, client)
showCommand.AddCommand(subCmd)
@ -176,6 +198,10 @@ func runShow(args []string, client *action.Show) (string, error) {
client.Version = ">0.0.0-0"
}
if err := checkOCI(args[0]); err != nil {
return "", err
}
cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
if err != nil {
return "", err

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

@ -62,7 +62,7 @@ func TestTemplateCmd(t *testing.T) {
name: "check chart bad type",
cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-bad-type"),
wantError: true,
golden: "output/install-chart-bad-type.txt",
golden: "output/template-chart-bad-type.txt",
},
{
name: "check chart with dependency which is an app chart acting as a library chart",

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

@ -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:
- 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:
- (root): employmentInfo is required
- 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:
- 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:
- (root): lastname is required
subchart-with-schema:

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

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

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

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

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

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

@ -0,0 +1 @@
password

@ -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.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.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.StringVar(&client.Description, "description", "", "add a custom description")

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

@ -17,9 +17,13 @@ limitations under the License.
package main
import (
"context"
"fmt"
"io"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/pkg/errors"
@ -83,6 +87,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return nil, cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := checkOCI(args[1]); err != nil {
return err
}
client.Namespace = settings.Namespace()
// Fixes #7002 - Support reading values from STDIN for `upgrade` command
@ -174,7 +182,20 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
warning("This chart is deprecated")
}
rel, err := client.Run(args[0], ch, vals)
// Create context and prepare the handle of SIGTERM
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
// Handle SIGTERM
cSignal := make(chan os.Signal)
signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM)
go func() {
<-cSignal
fmt.Fprintf(out, "Release %s has been cancelled.\n", args[0])
cancel()
}()
rel, err := client.RunWithContext(ctx, args[0], ch, vals)
if err != nil {
return errors.Wrap(err, "UPGRADE FAILED")
}

@ -10,13 +10,11 @@ require (
github.com/Masterminds/squirrel v1.5.0
github.com/Masterminds/vcs v1.13.1
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/containerd/containerd v1.4.4
github.com/containerd/containerd v1.5.4
github.com/cyphar/filepath-securejoin v0.2.2
github.com/deislabs/oras v0.11.1
github.com/docker/distribution v2.7.1+incompatible
github.com/distribution/distribution/v3 v3.0.0-20210804104954-38ab4c606ee3
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
github.com/docker/go-units v0.4.0
github.com/evanphx/json-patch v4.9.0+incompatible
github.com/evanphx/json-patch v4.11.0+incompatible
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.8.0
github.com/gosuri/uitable v0.0.4
@ -24,27 +22,27 @@ require (
github.com/lib/pq v1.10.0
github.com/mattn/go-shellwords v1.0.11
github.com/mitchellh/copystructure v1.1.1
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.0.1
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/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/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
github.com/ziutek/mymysql v1.5.4 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
k8s.io/api v0.21.0
k8s.io/apiextensions-apiserver v0.21.0
k8s.io/apimachinery v0.21.0
k8s.io/apiserver v0.21.0
k8s.io/cli-runtime v0.21.0
k8s.io/client-go v0.21.0
k8s.io/klog/v2 v2.8.0
k8s.io/kubectl v0.21.0
k8s.io/api v0.22.1
k8s.io/apiextensions-apiserver v0.22.1
k8s.io/apimachinery v0.22.1
k8s.io/apiserver v0.22.1
k8s.io/cli-runtime v0.22.1
k8s.io/client-go v0.22.1
k8s.io/klog/v2 v2.9.0
k8s.io/kubectl v0.22.1
oras.land/oras-go v0.4.0
rsc.io/letsencrypt v0.0.3 // indirect
sigs.k8s.io/yaml v1.2.0
)
replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d

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

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

@ -1,11 +1,10 @@
/*
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
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,
@ -14,15 +13,9 @@ 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 (
"github.com/containerd/containerd/remotes"
)
type (
// Resolver provides remotes based on a locator
Resolver struct {
remotes.Resolver
}
)
/*
Package pusher provides a generalized tool for uploading data by scheme.
This provides a method by which the plugin system can load arbitrary protocol
handlers based upon a URL scheme.
*/
package pusher

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

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

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

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

@ -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"
import (
"bytes"
"context"
"fmt"
"io"
"time"
"strings"
orascontext "github.com/deislabs/oras/pkg/context"
units "github.com/docker/go-units"
"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
func byteCountBinary(b int64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
// IsOCI determines whether or not a URL is to be treated as an OCI URL
func IsOCI(url string) bool {
return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme))
}
// shortDigest returns first 7 characters of a sha256 digest
func shortDigest(digest string) string {
if len(digest) == 64 {
return digest[:7]
// extractChartMeta is used to extract a chart metadata from a byte array
func extractChartMeta(chartData []byte) (*chart.Metadata, error) {
ch, err := loader.LoadArchive(bytes.NewReader(chartData))
if err != nil {
return nil, err
}
return digest
}
// timeAgo returns a human-readable timestamp representing time that has passed
func timeAgo(t time.Time) string {
return units.HumanDuration(time.Now().UTC().Sub(t))
return ch.Metadata, nil
}
// 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.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
@ -14,15 +13,8 @@ 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 (
"github.com/deislabs/oras/pkg/auth"
)
/*Package uploader provides a library for uploading charts.
type (
// Authorizer handles registry auth operations
Authorizer struct {
auth.Client
}
)
This package contains tools for uploading charts to registries.
*/
package uploader

@ -26,6 +26,7 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/gates"
@ -121,7 +122,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
var version string
var ok bool
found := true
if !strings.HasPrefix(d.Repository, "oci://") {
if !registry.IsOCI(d.Repository) {
repoIndex, err := repo.LoadIndexFile(filepath.Join(r.cachepath, helmpath.CacheIndexFile(repoName)))
if err != nil {
return nil, errors.Wrapf(err, "no cached repository for %s found. (try 'helm repo update')", repoName)

@ -16,6 +16,7 @@ limitations under the License.
package resolver
import (
"runtime"
"testing"
"helm.sh/helm/v3/pkg/chart"
@ -246,24 +247,28 @@ func TestGetLocalPath(t *testing.T) {
repo string
chartpath string
expect string
winExpect string
err bool
}{
{
name: "absolute path",
repo: "file:////tmp",
expect: "/tmp",
name: "absolute path",
repo: "file:////",
expect: "/",
winExpect: "\\",
},
{
name: "relative path",
repo: "file://../../testdata/chartpath/base",
chartpath: "foo/bar",
expect: "testdata/chartpath/base",
winExpect: "testdata\\chartpath\\base",
},
{
name: "current directory path",
repo: "../charts/localdependency",
chartpath: "testdata/chartpath/charts",
expect: "testdata/chartpath/charts/localdependency",
winExpect: "testdata\\chartpath\\charts\\localdependency",
},
{
name: "invalid local path",
@ -291,8 +296,12 @@ func TestGetLocalPath(t *testing.T) {
if tt.err {
t.Fatalf("Expected error in test %q", tt.name)
}
if p != tt.expect {
t.Errorf("%q: expected %q, got %q", tt.name, tt.expect, p)
expect := tt.expect
if runtime.GOOS == "windows" {
expect = tt.winExpect
}
if p != expect {
t.Errorf("%q: expected %q, got %q", tt.name, expect, p)
}
})
}

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

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

@ -1,63 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"io"
"path/filepath"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chartutil"
)
// ChartExport performs a chart export operation.
type ChartExport struct {
cfg *Configuration
Destination string
}
// NewChartExport creates a new ChartExport object with the given configuration.
func NewChartExport(cfg *Configuration) *ChartExport {
return &ChartExport{
cfg: cfg,
}
}
// Run executes the chart export operation
func (a *ChartExport) Run(out io.Writer, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
ch, err := a.cfg.RegistryClient.LoadChart(r)
if err != nil {
return err
}
// Save the chart to local destination directory
err = chartutil.SaveDir(ch, a.Destination)
if err != nil {
return err
}
d := filepath.Join(a.Destination, ch.Metadata.Name)
fmt.Fprintf(out, "Exported chart to %s/\n", d)
return nil
}

@ -1,44 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartList performs a chart list operation.
type ChartList struct {
cfg *Configuration
ColumnWidth uint
}
// NewChartList creates a new ChartList object with the given configuration.
func NewChartList(cfg *Configuration) *ChartList {
return &ChartList{
cfg: cfg,
}
}
// Run executes the chart list operation
func (a *ChartList) Run(out io.Writer) error {
client := a.cfg.RegistryClient
opt := registry.ClientOptColumnWidth(a.ColumnWidth)
opt(client)
return client.PrintChartTable()
}

@ -1,44 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartPull performs a chart pull operation.
type ChartPull struct {
cfg *Configuration
}
// NewChartPull creates a new ChartPull object with the given configuration.
func NewChartPull(cfg *Configuration) *ChartPull {
return &ChartPull{
cfg: cfg,
}
}
// Run executes the chart pull operation
func (a *ChartPull) Run(out io.Writer, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
return a.cfg.RegistryClient.PullChartToCache(r)
}

@ -1,44 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartPush performs a chart push operation.
type ChartPush struct {
cfg *Configuration
}
// NewChartPush creates a new ChartPush object with the given configuration.
func NewChartPush(cfg *Configuration) *ChartPush {
return &ChartPush{
cfg: cfg,
}
}
// Run executes the chart push operation
func (a *ChartPush) Run(out io.Writer, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
return a.cfg.RegistryClient.PushChart(r)
}

@ -1,44 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartRemove performs a chart remove operation.
type ChartRemove struct {
cfg *Configuration
}
// NewChartRemove creates a new ChartRemove object with the given configuration.
func NewChartRemove(cfg *Configuration) *ChartRemove {
return &ChartRemove{
cfg: cfg,
}
}
// Run executes the chart remove operation
func (a *ChartRemove) Run(out io.Writer, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
return a.cfg.RegistryClient.RemoveChart(r)
}

@ -1,51 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart"
)
// ChartSave performs a chart save operation.
type ChartSave struct {
cfg *Configuration
}
// NewChartSave creates a new ChartSave object with the given configuration.
func NewChartSave(cfg *Configuration) *ChartSave {
return &ChartSave{
cfg: cfg,
}
}
// Run executes the chart save operation
func (a *ChartSave) Run(out io.Writer, ch *chart.Chart, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
// If no tag is present, use the chart version
if r.Tag == "" {
r.Tag = ch.Metadata.Version
}
return a.cfg.RegistryClient.SaveChart(ch, r)
}

@ -1,70 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io/ioutil"
"testing"
"helm.sh/helm/v3/internal/experimental/registry"
)
func chartSaveAction(t *testing.T) *ChartSave {
t.Helper()
config := actionConfigFixture(t)
action := NewChartSave(config)
return action
}
func TestChartSave(t *testing.T) {
action := chartSaveAction(t)
input := buildChart()
if err := action.Run(ioutil.Discard, input, "localhost:5000/test:0.2.0"); err != nil {
t.Error(err)
}
ref, err := registry.ParseReference("localhost:5000/test:0.2.0")
if err != nil {
t.Fatal(err)
}
if _, err := action.cfg.RegistryClient.LoadChart(ref); err != nil {
t.Error(err)
}
// now let's check if `helm chart save` can use the chart version when the tag is not present
if err := action.Run(ioutil.Discard, input, "localhost:5000/test"); err != nil {
t.Error(err)
}
ref, err = registry.ParseReference("localhost:5000/test")
if err != nil {
t.Fatal(err)
}
// TODO: guess latest based on semver?
_, err = action.cfg.RegistryClient.LoadChart(ref)
if err == nil {
t.Error("Expected error parsing ref without tag")
}
ref.Tag = "0.1.0"
if _, err := action.cfg.RegistryClient.LoadChart(ref); err != nil {
t.Error(err)
}
}

@ -18,6 +18,7 @@ package action
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/url"
@ -25,6 +26,7 @@ import (
"path"
"path/filepath"
"strings"
"sync"
"text/template"
"time"
@ -36,6 +38,7 @@ import (
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
@ -104,6 +107,8 @@ type Install struct {
// OutputDir/<ReleaseName>
UseReleaseName bool
PostRenderer postrender.PostRenderer
// Lock to control raceconditions when the process receives a SIGTERM
Lock sync.Mutex
}
// ChartPathOptions captures common options used for controlling chart paths
@ -174,7 +179,14 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
// Run executes the installation
//
// If DryRun is set to true, this will prepare the release, but not install it
func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
ctx := context.Background()
return i.RunWithContext(ctx, chrt, vals)
}
// Run executes the installation with Context
func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
// Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`)
if !i.ClientOnly {
if err := i.cfg.KubeClient.IsReachable(); err != nil {
@ -331,11 +343,21 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
// not working.
return rel, err
}
rChan := make(chan resultMessage)
go i.performInstall(rChan, rel, toBeAdopted, resources)
go i.handleContext(ctx, rChan, rel)
result := <-rChan
//start preformInstall go routine
return result.r, result.e
}
func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) {
// pre-install hooks
if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil {
return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err))
i.reportToRun(c, rel, fmt.Errorf("failed pre-install: %s", err))
return
}
}
@ -344,29 +366,34 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
// to true, since that is basically an upgrade operation.
if len(toBeAdopted) == 0 && len(resources) > 0 {
if _, err := i.cfg.KubeClient.Create(resources); err != nil {
return i.failRelease(rel, err)
i.reportToRun(c, rel, err)
return
}
} else if len(resources) > 0 {
if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, false); err != nil {
return i.failRelease(rel, err)
i.reportToRun(c, rel, err)
return
}
}
if i.Wait {
if i.WaitForJobs {
if err := i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout); err != nil {
return i.failRelease(rel, err)
i.reportToRun(c, rel, err)
return
}
} else {
if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil {
return i.failRelease(rel, err)
i.reportToRun(c, rel, err)
return
}
}
}
if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil {
return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err))
i.reportToRun(c, rel, fmt.Errorf("failed post-install: %s", err))
return
}
}
@ -387,9 +414,23 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
i.cfg.Log("failed to record the release: %s", err)
}
return rel, nil
i.reportToRun(c, rel, nil)
}
func (i *Install) handleContext(ctx context.Context, c chan<- resultMessage, rel *release.Release) {
go func() {
<-ctx.Done()
err := ctx.Err()
i.reportToRun(c, rel, err)
}()
}
func (i *Install) reportToRun(c chan<- resultMessage, rel *release.Release, err error) {
i.Lock.Lock()
if err != nil {
rel, err = i.failRelease(rel, err)
}
c <- resultMessage{r: rel, e: err}
i.Lock.Unlock()
}
func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
if i.Atomic {
@ -663,6 +704,14 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
}
if registry.IsOCI(name) {
if version == "" {
return "", errors.New("version is explicitly required for OCI registries")
}
dl.Options = append(dl.Options, getter.WithTagName(version))
}
if c.Verify {
dl.Verify = downloader.VerifyAlways
}
@ -716,5 +765,6 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
if version != "" {
atVersion = fmt.Sprintf(" at version %q", version)
}
return filename, errors.Errorf("failed to download %q%s (hint: running `helm repo update` may help)", name, atVersion)
return filename, errors.Errorf("failed to download %q%s", name, atVersion)
}

@ -17,6 +17,7 @@ limitations under the License.
package action
import (
"context"
"fmt"
"io/ioutil"
"log"
@ -25,6 +26,7 @@ import (
"regexp"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
@ -34,7 +36,7 @@ import (
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
"helm.sh/helm/v3/pkg/time"
helmtime "helm.sh/helm/v3/pkg/time"
)
type nameTemplateTestCase struct {
@ -361,7 +363,25 @@ func TestInstallRelease_Wait(t *testing.T) {
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestInstallRelease_Wait_Interrupted(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "interrupted-release"
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitDuration = 10 * time.Second
instAction.cfg.KubeClient = failer
instAction.Wait = true
vals := map[string]interface{}{}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
time.AfterFunc(time.Second, cancel)
res, err := instAction.RunWithContext(ctx, buildChart(), vals)
is.Error(err)
is.Contains(res.Info.Description, "Release \"interrupted-release\" failed: context canceled")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestInstallRelease_WaitForJobs(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
@ -419,7 +439,33 @@ func TestInstallRelease_Atomic(t *testing.T) {
is.Contains(err.Error(), "an error occurred while uninstalling the release")
})
}
func TestInstallRelease_Atomic_Interrupted(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "interrupted-release"
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitDuration = 10 * time.Second
instAction.cfg.KubeClient = failer
instAction.Atomic = true
vals := map[string]interface{}{}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
time.AfterFunc(time.Second, cancel)
res, err := instAction.RunWithContext(ctx, buildChart(), vals)
is.Error(err)
is.Contains(err.Error(), "context canceled")
is.Contains(err.Error(), "atomic")
is.Contains(err.Error(), "uninstalled")
// Now make sure it isn't in storage any more
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
is.Equal(err, driver.ErrReleaseNotFound)
}
func TestNameTemplate(t *testing.T) {
testCases := []nameTemplateTestCase{
// Just a straight up nop please
@ -624,32 +670,32 @@ func TestNameAndChartGenerateName(t *testing.T) {
{
"local filepath",
"./chart",
fmt.Sprintf("chart-%d", time.Now().Unix()),
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
},
{
"dot filepath",
".",
fmt.Sprintf("chart-%d", time.Now().Unix()),
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
},
{
"empty filepath",
"",
fmt.Sprintf("chart-%d", time.Now().Unix()),
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
},
{
"packaged chart",
"chart.tgz",
fmt.Sprintf("chart-%d", time.Now().Unix()),
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
},
{
"packaged chart with .tar.gz extension",
"chart.tar.gz",
fmt.Sprintf("chart-%d", time.Now().Unix()),
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
},
{
"packaged chart with local extension",
"./chart.tgz",
fmt.Sprintf("chart-%d", time.Now().Unix()),
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
},
}

@ -25,6 +25,7 @@ import (
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/downloader"
@ -90,7 +91,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
RepositoryCache: p.Settings.RepositoryCache,
}
if strings.HasPrefix(chartRef, "oci://") {
if registry.IsOCI(chartRef) {
if p.Version == "" {
return out.String(), errors.Errorf("--version flag is explicitly required for OCI registries")
}

@ -17,6 +17,7 @@ limitations under the License.
package action
import (
"bytes"
"fmt"
"strings"
@ -41,6 +42,8 @@ const (
ShowValues ShowOutputFormat = "values"
// ShowReadme is the format which only shows the chart's README
ShowReadme ShowOutputFormat = "readme"
// ShowCRDs is the format which only shows the chart's CRDs
ShowCRDs ShowOutputFormat = "crds"
)
var readmeFileNames = []string{"readme.md", "readme.txt", "readme"}
@ -106,14 +109,25 @@ func (s *Show) Run(chartpath string) (string, error) {
}
if s.OutputFormat == ShowReadme || s.OutputFormat == ShowAll {
if s.OutputFormat == ShowAll {
fmt.Fprintln(&out, "---")
}
readme := findReadme(s.chart.Files)
if readme == nil {
return out.String(), nil
if readme != nil {
if s.OutputFormat == ShowAll {
fmt.Fprintln(&out, "---")
}
fmt.Fprintf(&out, "%s\n", readme.Data)
}
}
if s.OutputFormat == ShowCRDs || s.OutputFormat == ShowAll {
crds := s.chart.CRDObjects()
if len(crds) > 0 {
if s.OutputFormat == ShowAll && !bytes.HasPrefix(crds[0].File.Data, []byte("---")) {
fmt.Fprintln(&out, "---")
}
for _, crd := range crds {
fmt.Fprintf(&out, "%s\n", string(crd.File.Data))
}
}
fmt.Fprintf(&out, "%s\n", readme.Data)
}
return out.String(), nil
}

@ -28,6 +28,9 @@ func TestShow(t *testing.T) {
Metadata: &chart.Metadata{Name: "alpine"},
Files: []*chart.File{
{Name: "README.md", Data: []byte("README\n")},
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
},
Raw: []*chart.File{
{Name: "values.yaml", Data: []byte("VALUES\n")},
@ -48,6 +51,12 @@ VALUES
---
README
---
foo
---
bar
`
if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
@ -83,3 +92,61 @@ func TestShowValuesByJsonPathFormat(t *testing.T) {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
}
}
func TestShowCRDs(t *testing.T) {
client := NewShow(ShowCRDs)
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"},
Files: []*chart.File{
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
},
}
output, err := client.Run("")
if err != nil {
t.Fatal(err)
}
expect := `---
foo
---
bar
`
if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
}
}
func TestShowNoReadme(t *testing.T) {
client := NewShow(ShowAll)
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"},
Files: []*chart.File{
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
},
}
output, err := client.Run("")
if err != nil {
t.Fatal(err)
}
expect := `name: alpine
---
foo
---
bar
`
if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
}
}

@ -23,6 +23,7 @@ import (
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil"
helmtime "helm.sh/helm/v3/pkg/time"
@ -37,6 +38,7 @@ type Uninstall struct {
DisableHooks bool
DryRun bool
KeepHistory bool
Wait bool
Timeout time.Duration
Description string
}
@ -110,13 +112,21 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
u.cfg.Log("uninstall: Failed to store updated release: %s", err)
}
kept, errs := u.deleteRelease(rel)
deletedResources, kept, errs := u.deleteRelease(rel)
if kept != "" {
kept = "These resources were kept due to the resource policy:\n" + kept
}
res.Info = kept
if u.Wait {
if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceExt); ok {
if err := kubeClient.WaitForDelete(deletedResources, u.Timeout); err != nil {
errs = append(errs, err)
}
}
}
if !u.DisableHooks {
if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil {
errs = append(errs, err)
@ -172,12 +182,12 @@ func joinErrors(errs []error) string {
return strings.Join(es, "; ")
}
// deleteRelease deletes the release and returns manifests that were kept in the deletion process
func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) {
// deleteRelease deletes the release and returns list of delete resources and manifests that were kept in the deletion process
func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, string, []error) {
var errs []error
caps, err := u.cfg.getCapabilities()
if err != nil {
return rel.Manifest, []error{errors.Wrap(err, "could not get apiVersions from Kubernetes")}
return nil, rel.Manifest, []error{errors.Wrap(err, "could not get apiVersions from Kubernetes")}
}
manifests := releaseutil.SplitManifests(rel.Manifest)
@ -187,7 +197,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) {
// FIXME: One way to delete at this point would be to try a label-based
// deletion. The problem with this is that we could get a false positive
// and delete something that was not legitimately part of this release.
return rel.Manifest, []error{errors.Wrap(err, "corrupted release record. You must manually delete the resources")}
return nil, rel.Manifest, []error{errors.Wrap(err, "corrupted release record. You must manually delete the resources")}
}
filesToKeep, filesToDelete := filterManifestsToKeep(files)
@ -203,10 +213,10 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) {
resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false)
if err != nil {
return "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")}
return nil, "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")}
}
if len(resources) > 0 {
_, errs = u.cfg.KubeClient.Delete(resources)
}
return kept, errs
return resources, kept, errs
}

@ -17,9 +17,13 @@ limitations under the License.
package action
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
)
func uninstallAction(t *testing.T) *Uninstall {
@ -60,3 +64,34 @@ func TestUninstallRelease_deleteRelease(t *testing.T) {
`
is.Contains(res.Info, expected)
}
func TestUninstallRelease_Wait(t *testing.T) {
is := assert.New(t)
unAction := uninstallAction(t)
unAction.DisableHooks = true
unAction.DryRun = false
unAction.Wait = true
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Manifest = `{
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": "secret"
},
"type": "Opaque",
"data": {
"password": "password"
}
}`
unAction.cfg.Releases.Create(rel)
failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("U timed out")
unAction.cfg.KubeClient = failer
res, err := unAction.Run(rel.Name)
is.Error(err)
is.Contains(err.Error(), "U timed out")
is.Equal(res.Release.Info.Status, release.StatusUninstalled)
}

@ -21,6 +21,7 @@ import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/pkg/errors"
@ -100,6 +101,13 @@ type Upgrade struct {
DisableOpenAPIValidation bool
// Get missing dependencies
DependencyUpdate bool
// Lock to control raceconditions when the process receives a SIGTERM
Lock sync.Mutex
}
type resultMessage struct {
r *release.Release
e error
}
// NewUpgrade creates a new Upgrade object with the given configuration.
@ -109,8 +117,14 @@ func NewUpgrade(cfg *Configuration) *Upgrade {
}
}
// Run executes the upgrade on the given release.
// Run executes the upgrade on the given release
func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
ctx := context.Background()
return u.RunWithContext(ctx, name, chart, vals)
}
// Run executes the upgrade on the given release with context.
func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
if err := u.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
@ -131,7 +145,7 @@ func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface
u.cfg.Releases.MaxHistory = u.MaxHistory
u.cfg.Log("performing update for %s", name)
res, err := u.performUpgrade(currentRelease, upgradedRelease)
res, err := u.performUpgrade(ctx, currentRelease, upgradedRelease)
if err != nil {
return res, err
}
@ -243,7 +257,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return currentRelease, upgradedRelease, err
}
func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Release) (*release.Release, error) {
func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedRelease *release.Release) (*release.Release, error) {
current, err := u.cfg.KubeClient.Build(bytes.NewBufferString(originalRelease.Manifest), false)
if err != nil {
// Checking for removed Kubernetes API error so can provide a more informative error message to the user
@ -306,11 +320,43 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
if err := u.cfg.Releases.Create(upgradedRelease); err != nil {
return nil, err
}
rChan := make(chan resultMessage)
go u.releasingUpgrade(rChan, upgradedRelease, current, target, originalRelease)
go u.handleContext(ctx, rChan, upgradedRelease)
result := <-rChan
return result.r, result.e
}
// Function used to lock the Mutex, this is important for the case when the atomic flag is set.
// In that case the upgrade will finish before the rollback is finished so it is necessary to wait for the rollback to finish.
// The rollback will be trigger by the function failRelease
func (u *Upgrade) reportToPerformUpgrade(c chan<- resultMessage, rel *release.Release, created kube.ResourceList, err error) {
u.Lock.Lock()
if err != nil {
rel, err = u.failRelease(rel, created, err)
}
c <- resultMessage{r: rel, e: err}
u.Lock.Unlock()
}
// Setup listener for SIGINT and SIGTERM
func (u *Upgrade) handleContext(ctx context.Context, c chan<- resultMessage, upgradedRelease *release.Release) {
go func() {
<-ctx.Done()
err := ctx.Err()
// when the atomic flag is set the ongoing release finish first and doesn't give time for the rollback happens.
u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, err)
}()
}
func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release) {
// pre-upgrade hooks
if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.Timeout); err != nil {
return u.failRelease(upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err))
u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err))
return
}
} else {
u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name)
@ -319,7 +365,8 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
results, err := u.cfg.KubeClient.Update(current, target, u.Force)
if err != nil {
u.cfg.recordRelease(originalRelease)
return u.failRelease(upgradedRelease, results.Created, err)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
}
if u.Recreate {
@ -336,12 +383,14 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
if u.WaitForJobs {
if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease)
return u.failRelease(upgradedRelease, results.Created, err)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
}
} else {
if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease)
return u.failRelease(upgradedRelease, results.Created, err)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
}
}
}
@ -349,7 +398,8 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
// post-upgrade hooks
if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.Timeout); err != nil {
return u.failRelease(upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err))
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err))
return
}
}
@ -362,8 +412,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
} else {
upgradedRelease.Info.Description = "Upgrade complete"
}
return upgradedRelease, nil
u.reportToPerformUpgrade(c, upgradedRelease, nil, nil)
}
func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, err error) (*release.Release, error) {

@ -17,8 +17,10 @@ limitations under the License.
package action
import (
"context"
"fmt"
"testing"
"time"
"helm.sh/helm/v3/pkg/chart"
@ -27,7 +29,7 @@ import (
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/time"
helmtime "helm.sh/helm/v3/pkg/time"
)
func upgradeAction(t *testing.T) *Upgrade {
@ -225,7 +227,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) {
withValues(chartDefaultValues),
withMetadataDependency(dependency),
)
now := time.Now()
now := helmtime.Now()
existingValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": false,
@ -296,3 +298,66 @@ func TestUpgradeRelease_Pending(t *testing.T) {
_, err := upAction.Run(rel.Name, buildChart(), vals)
req.Contains(err.Error(), "progress", err)
}
func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
is := assert.New(t)
req := require.New(t)
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "interrupted-release"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitDuration = 10 * time.Second
upAction.cfg.KubeClient = failer
upAction.Wait = true
vals := map[string]interface{}{}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
time.AfterFunc(time.Second, cancel)
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
req.Error(err)
is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: context canceled")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) {
is := assert.New(t)
req := require.New(t)
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "interrupted-release"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitDuration = 5 * time.Second
upAction.cfg.KubeClient = failer
upAction.Atomic = true
vals := map[string]interface{}{}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
time.AfterFunc(time.Second, cancel)
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
req.Error(err)
is.Contains(err.Error(), "release interrupted-release failed, and has been rolled back due to atomic being set: context canceled")
// Now make sure it is actually upgraded
updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3)
is.NoError(err)
// Should have rolled back to the previous
is.Equal(updatedRes.Info.Status, release.StatusDeployed)
}

@ -62,8 +62,8 @@ func TestDefaultCapabilities(t *testing.T) {
func TestDefaultCapabilitiesHelmVersion(t *testing.T) {
hv := DefaultCapabilities.HelmVersion
if hv.Version != "v3.6" {
t.Errorf("Expected default HelmVersion to be v3.6, got %q", hv.Version)
if hv.Version != "v3.7" {
t.Errorf("Expected default HelmVersion to be v3.7, got %q", hv.Version)
}
}

@ -120,13 +120,11 @@ func coalesceGlobals(dest, src map[string]interface{}) {
// top-down.
CoalesceTables(vv, destvmap)
dg[key] = vv
continue
}
}
} else if dv, ok := dg[key]; ok && istable(dv) {
// It's not clear if this condition can actually ever trigger.
log.Printf("key %s is table. Skipping", key)
continue
} else {
// TODO: Do we need to do any additional checking on the value?
dg[key] = val
@ -159,15 +157,15 @@ func coalesceValues(c *chart.Chart, v map[string]interface{}) {
src, ok := val.(map[string]interface{})
if !ok {
// If the original value is nil, there is nothing to coalesce, so we don't print
// the warning but simply continue
// the warning
if val != nil {
log.Printf("warning: skipped value for %s: Not a table.", key)
}
continue
} else {
// Because v has higher precedence than nv, dest values override src
// values.
CoalesceTables(dest, src)
}
// Because v has higher precedence than nv, dest values override src
// values.
CoalesceTables(dest, src)
}
} else {
// If the key is not in v, copy it from nv.

@ -78,17 +78,27 @@ func TestCoalesceValues(t *testing.T) {
"right": "exists",
"scope": "moby",
"top": "nope",
"global": map[string]interface{}{
"nested2": map[string]interface{}{"l0": "moby"},
},
},
},
withDeps(&chart.Chart{
Metadata: &chart.Metadata{Name: "pequod"},
Values: map[string]interface{}{"name": "pequod", "scope": "pequod"},
Values: map[string]interface{}{
"name": "pequod",
"scope": "pequod",
"global": map[string]interface{}{
"nested2": map[string]interface{}{"l1": "pequod"},
},
},
},
&chart.Chart{
Metadata: &chart.Metadata{Name: "ahab"},
Values: map[string]interface{}{
"global": map[string]interface{}{
"nested": map[string]interface{}{"foo": "bar"},
"nested": map[string]interface{}{"foo": "bar"},
"nested2": map[string]interface{}{"l2": "ahab"},
},
"scope": "ahab",
"name": "ahab",
@ -99,7 +109,12 @@ func TestCoalesceValues(t *testing.T) {
),
&chart.Chart{
Metadata: &chart.Metadata{Name: "spouter"},
Values: map[string]interface{}{"scope": "spouter"},
Values: map[string]interface{}{
"scope": "spouter",
"global": map[string]interface{}{
"nested2": map[string]interface{}{"l1": "spouter"},
},
},
},
)
@ -152,6 +167,19 @@ func TestCoalesceValues(t *testing.T) {
{"{{.spouter.global.nested.boat}}", "true"},
{"{{.pequod.global.nested.sail}}", "true"},
{"{{.spouter.global.nested.sail}}", "<no value>"},
{"{{.global.nested2.l0}}", "moby"},
{"{{.global.nested2.l1}}", "<no value>"},
{"{{.global.nested2.l2}}", "<no value>"},
{"{{.pequod.global.nested2.l0}}", "moby"},
{"{{.pequod.global.nested2.l1}}", "pequod"},
{"{{.pequod.global.nested2.l2}}", "<no value>"},
{"{{.pequod.ahab.global.nested2.l0}}", "moby"},
{"{{.pequod.ahab.global.nested2.l1}}", "pequod"},
{"{{.pequod.ahab.global.nested2.l2}}", "ahab"},
{"{{.spouter.global.nested2.l0}}", "moby"},
{"{{.spouter.global.nested2.l1}}", "spouter"},
{"{{.spouter.global.nested2.l2}}", "<no value>"},
}
for _, tt := range tests {

@ -102,7 +102,7 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
}
name := filepath.Base(u.Path)
if u.Scheme == "oci" {
if u.Scheme == registry.OCIScheme {
name = fmt.Sprintf("%s-%s.tgz", name, version)
}

@ -16,7 +16,6 @@ limitations under the License.
package downloader
import (
"net/http"
"os"
"path/filepath"
"testing"
@ -171,19 +170,7 @@ func TestIsTar(t *testing.T) {
}
func TestDownloadTo(t *testing.T) {
// Set up a fake repo with basic auth enabled
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")
srv.Stop()
if err != nil {
t.Fatal(err)
}
srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "username" || password != "password" {
t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password)
}
}))
srv.Start()
srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/*.tgz*")
defer srv.Stop()
if err := srv.CreateIndex(); err != nil {
t.Fatal(err)

@ -249,22 +249,24 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
destPath := filepath.Join(m.ChartPath, "charts")
tmpPath := filepath.Join(m.ChartPath, "tmpcharts")
// Create 'charts' directory if it doesn't already exist.
if fi, err := os.Stat(destPath); err != nil {
// Check if 'charts' directory is not actally a directory. If it does not exist, create it.
if fi, err := os.Stat(destPath); err == nil {
if !fi.IsDir() {
return errors.Errorf("%q is not a directory", destPath)
}
} else if os.IsNotExist(err) {
if err := os.MkdirAll(destPath, 0755); err != nil {
return err
}
} else if !fi.IsDir() {
return errors.Errorf("%q is not a directory", destPath)
}
if err := fs.RenameWithFallback(destPath, tmpPath); err != nil {
return errors.Wrap(err, "unable to move current charts to tmp dir")
} else {
return fmt.Errorf("unable to retrieve file info for '%s': %v", destPath, err)
}
if err := os.MkdirAll(destPath, 0755); err != nil {
// Prepare tmpPath
if err := os.MkdirAll(tmpPath, 0755); err != nil {
return err
}
defer os.RemoveAll(tmpPath)
fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
var saveError error
@ -273,10 +275,11 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
// No repository means the chart is in charts directory
if dep.Repository == "" {
fmt.Fprintf(m.Out, "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n", dep.Name)
chartPath := filepath.Join(tmpPath, dep.Name)
// NOTE: we are only validating the local dependency conforms to the constraints. No copying to tmpPath is necessary.
chartPath := filepath.Join(destPath, dep.Name)
ch, err := loader.LoadDir(chartPath)
if err != nil {
return fmt.Errorf("unable to load chart: %v", err)
return fmt.Errorf("unable to load chart '%s': %v", chartPath, err)
}
constraint, err := semver.NewConstraint(dep.Version)
@ -299,7 +302,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
if m.Debug {
fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository)
}
ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version)
ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version, tmpPath)
if err != nil {
saveError = err
break
@ -354,8 +357,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
getter.WithTagName(version))
}
_, _, err = dl.DownloadTo(churl, version, destPath)
if err != nil {
if _, _, err = dl.DownloadTo(churl, version, tmpPath); err != nil {
saveError = errors.Wrapf(err, "could not download %s", churl)
break
}
@ -363,36 +365,14 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
churls[churl] = struct{}{}
}
// TODO: this should probably be refactored to be a []error, so we can capture and provide more information rather than "last error wins".
if saveError == nil {
fmt.Fprintln(m.Out, "Deleting outdated charts")
for _, dep := range deps {
// Chart from local charts directory stays in place
if dep.Repository != "" {
if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil {
return err
}
}
}
if err := move(tmpPath, destPath); err != nil {
// now we can move all downloaded charts to destPath and delete outdated dependencies
if err := m.safeMoveDeps(deps, tmpPath, destPath); err != nil {
return err
}
if err := os.RemoveAll(tmpPath); err != nil {
return errors.Wrapf(err, "failed to remove %v", tmpPath)
}
} else {
fmt.Fprintln(m.Out, "Save error occurred: ", saveError)
fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state")
for _, dep := range deps {
if err := m.safeDeleteDep(dep.Name, destPath); err != nil {
return err
}
}
if err := os.RemoveAll(destPath); err != nil {
return errors.Wrapf(err, "failed to remove %v", destPath)
}
if err := fs.RenameWithFallback(tmpPath, destPath); err != nil {
return errors.Wrap(err, "unable to move current charts to tmp dir")
}
return saveError
}
return nil
@ -410,36 +390,75 @@ func parseOCIRef(chartRef string) (string, string, error) {
return chartRef, tag, nil
}
// safeDeleteDep deletes any versions of the given dependency in the given directory.
// safeMoveDep moves all dependencies in the source and moves them into dest.
//
// It does this by first matching the file name to an expected pattern, then loading
// the file to verify that it is a chart with the same name as the given name.
// the file to verify that it is a chart.
//
// Because it requires tar file introspection, it is more intensive than a basic delete.
// Any charts in dest that do not exist in source are removed (barring local dependencies)
//
// Because it requires tar file introspection, it is more intensive than a basic move.
//
// This will only return errors that should stop processing entirely. Other errors
// will emit log messages or be ignored.
func (m *Manager) safeDeleteDep(name, dir string) error {
files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz"))
func (m *Manager) safeMoveDeps(deps []*chart.Dependency, source, dest string) error {
existsInSourceDirectory := map[string]bool{}
isLocalDependency := map[string]bool{}
sourceFiles, err := ioutil.ReadDir(source)
if err != nil {
// Only for ErrBadPattern
return err
}
for _, fname := range files {
ch, err := loader.LoadFile(fname)
if err != nil {
fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
// attempt to read destFiles; fail fast if we can't
destFiles, err := ioutil.ReadDir(dest)
if err != nil {
return err
}
for _, dep := range deps {
if dep.Repository == "" {
isLocalDependency[dep.Name] = true
}
}
for _, file := range sourceFiles {
if file.IsDir() {
continue
}
if ch.Name() != name {
// This is not the file you are looking for.
filename := file.Name()
sourcefile := filepath.Join(source, filename)
destfile := filepath.Join(dest, filename)
existsInSourceDirectory[filename] = true
if _, err := loader.LoadFile(sourcefile); err != nil {
fmt.Fprintf(m.Out, "Could not verify %s for moving: %s (Skipping)", sourcefile, err)
continue
}
if err := os.Remove(fname); err != nil {
fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
// NOTE: no need to delete the dest; os.Rename replaces it.
if err := fs.RenameWithFallback(sourcefile, destfile); err != nil {
fmt.Fprintf(m.Out, "Unable to move %s to charts dir %s (Skipping)", sourcefile, err)
continue
}
}
fmt.Fprintln(m.Out, "Deleting outdated charts")
// find all files that exist in dest that do not exist in source; delete them (outdated dependendencies)
for _, file := range destFiles {
if !file.IsDir() && !existsInSourceDirectory[file.Name()] {
fname := filepath.Join(dest, file.Name())
ch, err := loader.LoadFile(fname)
if err != nil {
fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
}
// local dependency - skip
if isLocalDependency[ch.Name()] {
continue
}
if err := os.Remove(fname); err != nil {
fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
continue
}
}
}
return nil
}
@ -783,6 +802,7 @@ func normalizeURL(baseURL, urlOrPath string) (string, error) {
return urlOrPath, errors.Wrap(err, "base URL failed to parse")
}
u2.RawPath = path.Join(u2.RawPath, urlOrPath)
u2.Path = path.Join(u2.Path, urlOrPath)
return u2.String(), nil
}
@ -832,10 +852,8 @@ func writeLock(chartpath string, lock *chart.Lock, legacyLockfile bool) error {
return ioutil.WriteFile(dest, data, 0644)
}
// archive a dep chart from local directory and save it into charts/
func tarFromLocalDir(chartpath, name, repo, version string) (string, error) {
destPath := filepath.Join(chartpath, "charts")
// archive a dep chart from local directory and save it into destPath
func tarFromLocalDir(chartpath, name, repo, version, destPath string) (string, error) {
if !strings.HasPrefix(repo, "file://") {
return "", errors.Errorf("wrong format: chart %s repository %s", name, repo)
}
@ -868,20 +886,6 @@ func tarFromLocalDir(chartpath, name, repo, version string) (string, error) {
return "", errors.Errorf("can't get a valid version for dependency %s", name)
}
// move files from tmppath to destpath
func move(tmpPath, destPath string) error {
files, _ := os.ReadDir(tmpPath)
for _, file := range files {
filename := file.Name()
tmpfile := filepath.Join(tmpPath, filename)
destfile := filepath.Join(destPath, filename)
if err := fs.RenameWithFallback(tmpfile, destfile); err != nil {
return errors.Wrap(err, "unable to move local charts to charts dir")
}
}
return nil
}
// The prefix to use for cache keys created by the manager for repo names
const managerKeyPrefix = "helm-manager-"

@ -17,11 +17,14 @@ package downloader
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/repo/repotest"
@ -52,6 +55,7 @@ func TestNormalizeURL(t *testing.T) {
}{
{name: "basic URL", base: "https://example.com", path: "http://helm.sh/foo", expect: "http://helm.sh/foo"},
{name: "relative path", base: "https://helm.sh/charts", path: "foo", expect: "https://helm.sh/charts/foo"},
{name: "Encoded path", base: "https://helm.sh/a%2Fb/charts", path: "foo", expect: "https://helm.sh/a%2Fb/charts/foo"},
}
for _, tt := range tests {
@ -213,6 +217,58 @@ func TestGetRepoNames(t *testing.T) {
}
}
func TestDownloadAll(t *testing.T) {
chartPath, err := ioutil.TempDir("", "test-downloadall")
if err != nil {
t.Fatalf("could not create tempdir: %v", err)
}
defer os.RemoveAll(chartPath)
m := &Manager{
Out: new(bytes.Buffer),
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
ChartPath: chartPath,
}
signtest, err := loader.LoadDir(filepath.Join("testdata", "signtest"))
if err != nil {
t.Fatal(err)
}
if err := chartutil.SaveDir(signtest, filepath.Join(chartPath, "testdata")); err != nil {
t.Fatal(err)
}
local, err := loader.LoadDir(filepath.Join("testdata", "local-subchart"))
if err != nil {
t.Fatal(err)
}
if err := chartutil.SaveDir(local, filepath.Join(chartPath, "charts")); err != nil {
t.Fatal(err)
}
signDep := &chart.Dependency{
Name: signtest.Name(),
Repository: "file://./testdata/signtest",
Version: signtest.Metadata.Version,
}
localDep := &chart.Dependency{
Name: local.Name(),
Repository: "",
Version: local.Metadata.Version,
}
// create a 'tmpcharts' directory to test #5567
if err := os.MkdirAll(filepath.Join(chartPath, "tmpcharts"), 0755); err != nil {
t.Fatal(err)
}
if err := m.downloadAll([]*chart.Dependency{signDep, localDep}); err != nil {
t.Error(err)
}
if _, err := os.Stat(filepath.Join(chartPath, "charts", "signtest-0.1.0.tgz")); os.IsNotExist(err) {
t.Error(err)
}
}
func TestUpdateBeforeBuild(t *testing.T) {
// Set up a fake repo
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")

@ -97,7 +97,7 @@ const warnStartDelim = "HELM_ERR_START"
const warnEndDelim = "HELM_ERR_END"
const recursionMaxNums = 1000
var warnRegex = regexp.MustCompile(warnStartDelim + `(.*)` + warnEndDelim)
var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim)
func warnWrap(warn string) string {
return warnStartDelim + warn + warnEndDelim
@ -344,13 +344,20 @@ func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
//
// As it recurses, it also sets the values to be appropriate for the template
// scope.
func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) {
func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} {
subCharts := make(map[string]interface{})
chartMetaData := struct {
chart.Metadata
IsRoot bool
}{*c.Metadata, c.IsRoot()}
next := map[string]interface{}{
"Chart": c.Metadata,
"Chart": chartMetaData,
"Files": newFiles(c.Files),
"Release": vals["Release"],
"Capabilities": vals["Capabilities"],
"Values": make(chartutil.Values),
"Subcharts": subCharts,
}
// If there is a {{.Values.ThisChart}} in the parent metadata,
@ -362,7 +369,7 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.
}
for _, child := range c.Dependencies() {
recAllTpls(child, templates, next)
subCharts[child.Name()] = recAllTpls(child, templates, next)
}
newParentID := c.ChartFullPath()
@ -376,6 +383,8 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.
basePath: path.Join(newParentID, "templates"),
}
}
return next
}
// isTemplateValid returns true if the template is valid for the chart type

@ -245,44 +245,65 @@ func TestParseErrors(t *testing.T) {
func TestExecErrors(t *testing.T) {
vals := chartutil.Values{"Values": map[string]interface{}{}}
tplsMissingRequired := map[string]renderable{
"missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals},
}
_, err := new(Engine).render(tplsMissingRequired)
if err == nil {
t.Fatalf("Expected failures while rendering: %s", err)
}
expected := `execution error at (missing_required:1:2): foo is required`
if err.Error() != expected {
t.Errorf("Expected '%s', got %q", expected, err.Error())
}
tplsMissingRequired = map[string]renderable{
"missing_required_with_colons": {tpl: `{{required ":this: message: has many: colons:" .Values.foo}}`, vals: vals},
}
_, err = new(Engine).render(tplsMissingRequired)
if err == nil {
t.Fatalf("Expected failures while rendering: %s", err)
}
expected = `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:`
if err.Error() != expected {
t.Errorf("Expected '%s', got %q", expected, err.Error())
}
issue6044tpl := `{{ $someEmptyValue := "" }}
cases := []struct {
name string
tpls map[string]renderable
expected string
}{
{
name: "MissingRequired",
tpls: map[string]renderable{
"missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals},
},
expected: `execution error at (missing_required:1:2): foo is required`,
},
{
name: "MissingRequiredWithColons",
tpls: map[string]renderable{
"missing_required_with_colons": {tpl: `{{required ":this: message: has many: colons:" .Values.foo}}`, vals: vals},
},
expected: `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:`,
},
{
name: "Issue6044",
tpls: map[string]renderable{
"issue6044": {
vals: vals,
tpl: `{{ $someEmptyValue := "" }}
{{ $myvar := "abc" }}
{{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}`
tplsMissingRequired = map[string]renderable{
"issue6044": {tpl: issue6044tpl, vals: vals},
}
_, err = new(Engine).render(tplsMissingRequired)
if err == nil {
t.Fatalf("Expected failures while rendering: %s", err)
{{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}`,
},
},
expected: `execution error at (issue6044:3:4): abc: something is missing`,
},
{
name: "MissingRequiredWithNewlines",
tpls: map[string]renderable{
"issue9981": {tpl: `{{required "foo is required\nmore info after the break" .Values.foo}}`, vals: vals},
},
expected: `execution error at (issue9981:1:2): foo is required
more info after the break`,
},
{
name: "FailWithNewlines",
tpls: map[string]renderable{
"issue9981": {tpl: `{{fail "something is wrong\nlinebreak"}}`, vals: vals},
},
expected: `execution error at (issue9981:1:2): something is wrong
linebreak`,
},
}
expected = `execution error at (issue6044:3:4): abc: something is missing`
if err.Error() != expected {
t.Errorf("Expected '%s', got %q", expected, err.Error())
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
_, err := new(Engine).render(tt.tpls)
if err == nil {
t.Fatalf("Expected failures while rendering: %s", err)
}
if err.Error() != tt.expected {
t.Errorf("Expected %q, got %q", tt.expected, err.Error())
}
})
}
}
@ -346,6 +367,36 @@ func TestAllTemplates(t *testing.T) {
}
}
func TestChartValuesContainsIsRoot(t *testing.T) {
ch1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Templates: []*chart.File{
{Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")},
},
}
dep1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Templates: []*chart.File{
{Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")},
},
}
ch1.AddDependency(dep1)
out, err := Render(ch1, chartutil.Values{})
if err != nil {
t.Fatalf("failed to render templates: %s", err)
}
expects := map[string]string{
"parent/charts/child/templates/isroot": "false",
"parent/templates/isroot": "true",
}
for file, expect := range expects {
if out[file] != expect {
t.Errorf("Expected %q, got %q", expect, out[file])
}
}
}
func TestRenderDependency(t *testing.T) {
deptpl := `{{define "myblock"}}World{{end}}`
toptpl := `Hello {{template "myblock"}}`
@ -384,6 +435,8 @@ func TestRenderNestedValues(t *testing.T) {
// Ensure namespacing rules are working.
deepestpath := "templates/inner.tpl"
checkrelease := "templates/release.tpl"
// Ensure subcharts scopes are working.
subchartspath := "templates/subcharts.tpl"
deepest := &chart.Chart{
Metadata: &chart.Metadata{Name: "deepest"},
@ -391,7 +444,7 @@ func TestRenderNestedValues(t *testing.T) {
{Name: deepestpath, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)},
{Name: checkrelease, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)},
},
Values: map[string]interface{}{"what": "milkshake"},
Values: map[string]interface{}{"what": "milkshake", "where": "here"},
}
inner := &chart.Chart{
@ -399,7 +452,7 @@ func TestRenderNestedValues(t *testing.T) {
Templates: []*chart.File{
{Name: innerpath, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)},
},
Values: map[string]interface{}{"who": "Robert"},
Values: map[string]interface{}{"who": "Robert", "what": "glasses"},
}
inner.AddDependency(deepest)
@ -407,12 +460,14 @@ func TestRenderNestedValues(t *testing.T) {
Metadata: &chart.Metadata{Name: "top"},
Templates: []*chart.File{
{Name: outerpath, Data: []byte(`Gather ye {{.Values.what}} while ye may`)},
{Name: subchartspath, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)},
},
Values: map[string]interface{}{
"what": "stinkweed",
"who": "me",
"herrick": map[string]interface{}{
"who": "time",
"who": "time",
"what": "Sun",
},
},
}
@ -422,7 +477,8 @@ func TestRenderNestedValues(t *testing.T) {
"what": "rosebuds",
"herrick": map[string]interface{}{
"deepest": map[string]interface{}{
"what": "flower",
"what": "flower",
"where": "Heaven",
},
},
"global": map[string]interface{}{
@ -469,6 +525,11 @@ func TestRenderNestedValues(t *testing.T) {
if out[fullcheckrelease] != "Tomorrow will be dyin" {
t.Errorf("Unexpected release: %q", out[fullcheckrelease])
}
fullchecksubcharts := "top/" + subchartspath
if out[fullchecksubcharts] != "The glorious Lamp of Heaven, the Sun" {
t.Errorf("Unexpected subcharts: %q", out[fullchecksubcharts])
}
}
func TestRenderBuiltinValues(t *testing.T) {
@ -488,6 +549,7 @@ func TestRenderBuiltinValues(t *testing.T) {
Metadata: &chart.Metadata{Name: "Troy"},
Templates: []*chart.File{
{Name: "templates/Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)},
{Name: "templates/Amata", Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)},
},
}
outer.AddDependency(inner)
@ -510,6 +572,7 @@ func TestRenderBuiltinValues(t *testing.T) {
expects := map[string]string{
"Troy/charts/Latium/templates/Lavinia": "Troy/charts/Latium/templates/LaviniaLatiumAeneid",
"Troy/templates/Aeneas": "Troy/templates/AeneasTroyAeneid",
"Troy/templates/Amata": "Latium Virgil",
"Troy/charts/Latium/templates/From": "Virgil Aeneid",
}
for file, expect := range expects {

@ -169,7 +169,7 @@ var httpProvider = Provider{
}
var ociProvider = Provider{
Schemes: []string{"oci"},
Schemes: []string{registry.OCIScheme},
New: NewOCIGetter,
}

@ -39,22 +39,30 @@ func (g *OCIGetter) Get(href string, options ...Option) (*bytes.Buffer, error) {
func (g *OCIGetter) get(href string) (*bytes.Buffer, error) {
client := g.opts.registryClient
ref := strings.TrimPrefix(href, "oci://")
ref := strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme))
var pullOpts []registry.PullOption
requestingProv := strings.HasSuffix(ref, ".prov")
if requestingProv {
ref = strings.TrimSuffix(ref, ".prov")
pullOpts = append(pullOpts,
registry.PullOptWithChart(false),
registry.PullOptWithProv(true))
}
if version := g.opts.version; version != "" {
ref = fmt.Sprintf("%s:%s", ref, version)
}
r, err := registry.ParseReference(ref)
result, err := client.Pull(ref, pullOpts...)
if err != nil {
return nil, err
}
buf, err := client.PullChart(r)
if err != nil {
return nil, err
if requestingProv {
return bytes.NewBuffer(result.Prov.Data), nil
}
return buf, nil
return bytes.NewBuffer(result.Chart.Data), nil
}
// NewOCIGetter constructs a valid http/https client as a Getter

@ -162,6 +162,15 @@ func (c *Client) WaitWithJobs(resources ResourceList, timeout time.Duration) err
return w.waitForResources(resources)
}
// WaitForDelete wait up to the given timeout for the specified resources to be deleted.
func (c *Client) WaitForDelete(resources ResourceList, timeout time.Duration) error {
w := waiter{
log: c.Log,
timeout: timeout,
}
return w.waitForDeletedResources(resources)
}
func (c *Client) namespace() string {
if c.Namespace != "" {
return c.Namespace

@ -40,6 +40,7 @@ type FailingKubeClient struct {
BuildError error
BuildUnstructuredError error
WaitAndGetCompletedPodPhaseError error
WaitDuration time.Duration
}
// Create returns the configured error if set or prints
@ -50,8 +51,9 @@ func (f *FailingKubeClient) Create(resources kube.ResourceList) (*kube.Result, e
return f.PrintingKubeClient.Create(resources)
}
// Wait returns the configured error if set or prints
// Waits the amount of time defined on f.WaitDuration, then returns the configured error if set or prints.
func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error {
time.Sleep(f.WaitDuration)
if f.WaitError != nil {
return f.WaitError
}
@ -63,7 +65,15 @@ func (f *FailingKubeClient) WaitWithJobs(resources kube.ResourceList, d time.Dur
if f.WaitError != nil {
return f.WaitError
}
return f.PrintingKubeClient.Wait(resources, d)
return f.PrintingKubeClient.WaitWithJobs(resources, d)
}
// WaitForDelete returns the configured error if set or prints
func (f *FailingKubeClient) WaitForDelete(resources kube.ResourceList, d time.Duration) error {
if f.WaitError != nil {
return f.WaitError
}
return f.PrintingKubeClient.WaitForDelete(resources, d)
}
// Delete returns the configured error if set or prints

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

Loading…
Cancel
Save