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

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

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

@ -17,8 +17,13 @@ limitations under the License.
package main package main
import ( import (
"context"
"fmt"
"io" "io"
"log" "log"
"os"
"os/signal"
"syscall"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -119,7 +124,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
rel, err := runInstall(args, client, valueOpts, out) rel, err := runInstall(args, client, valueOpts, out)
if err != nil { if err != nil {
return err return errors.Wrap(err, "INSTALLATION FAILED")
} }
return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}) return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false})
@ -182,6 +187,10 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
} }
client.ReleaseName = name client.ReleaseName = name
if err := checkOCI(chart); err != nil {
return nil, err
}
cp, err := client.ChartPathOptions.LocateChart(chart, settings) cp, err := client.ChartPathOptions.LocateChart(chart, settings)
if err != nil { if err != nil {
return nil, err return nil, err
@ -239,7 +248,21 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
} }
client.Namespace = settings.Namespace() client.Namespace = settings.Namespace()
return client.Run(chartRequested, vals)
// Create context and prepare the handle of SIGTERM
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
// 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 // checkIfInstallable validates if a chart can be installed

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

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

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

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

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

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

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

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

@ -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") var errNoRepositories = errors.New("no repositories found. You must add one before updating")
type repoUpdateOptions struct { type repoUpdateOptions struct {
update func([]*repo.ChartRepository, io.Writer) update func([]*repo.ChartRepository, io.Writer, bool) error
repoFile string repoFile string
repoCache string repoCache string
names []string names []string
failOnRepoUpdateFail bool
} }
func newRepoUpdateCmd(out io.Writer) *cobra.Command { func newRepoUpdateCmd(out io.Writer) *cobra.Command {
@ -66,6 +67,13 @@ func newRepoUpdateCmd(out io.Writer) *cobra.Command {
return o.run(out) return o.run(out)
}, },
} }
f := cmd.Flags()
// Adding this flag for Helm 3 as stop gap functionality for https://github.com/helm/helm/issues/10016.
// This should be deprecated in Helm 4 by update to the behaviour of `helm repo update` command.
f.BoolVar(&o.failOnRepoUpdateFail, "fail-on-repo-update-fail", false, "update fails if any of the repository updates fail")
return cmd return cmd
} }
@ -103,26 +111,34 @@ func (o *repoUpdateOptions) run(out io.Writer) error {
} }
} }
o.update(repos, out) return o.update(repos, out, o.failOnRepoUpdateFail)
return nil
} }
func updateCharts(repos []*repo.ChartRepository, out io.Writer) { func updateCharts(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup var wg sync.WaitGroup
var repoFailList []string
for _, re := range repos { for _, re := range repos {
wg.Add(1) wg.Add(1)
go func(re *repo.ChartRepository) { go func(re *repo.ChartRepository) {
defer wg.Done() defer wg.Done()
if _, err := re.DownloadIndexFile(); err != nil { if _, err := re.DownloadIndexFile(); err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err) fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err)
repoFailList = append(repoFailList, re.Config.URL)
} else { } else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name) fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name)
} }
}(re) }(re)
} }
wg.Wait() wg.Wait()
if len(repoFailList) > 0 && failOnRepoUpdateFail {
return errors.New(fmt.Sprintf("Failed to update the following repositories: %s",
repoFailList))
}
fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
return nil
} }
func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) error { func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) error {

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

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

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

@ -51,6 +51,11 @@ This command inspects a chart (directory, file, or URL) and displays the content
of the README file of the README file
` `
const showCRDsDesc = `
This command inspects a chart (directory, file, or URL) and displays the contents
of the CustomResourceDefintion files
`
func newShowCmd(out io.Writer) *cobra.Command { func newShowCmd(out io.Writer) *cobra.Command {
client := action.NewShow(action.ShowAll) client := action.NewShow(action.ShowAll)
@ -139,7 +144,24 @@ func newShowCmd(out io.Writer) *cobra.Command {
}, },
} }
cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd} crdsSubCmd := &cobra.Command{
Use: "crds [CHART]",
Short: "show the chart's CRDs",
Long: showCRDsDesc,
Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc,
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowCRDs
output, err := runShow(args, client)
if err != nil {
return err
}
fmt.Fprint(out, output)
return nil
},
}
cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd, crdsSubCmd}
for _, subCmd := range cmds { for _, subCmd := range cmds {
addShowFlags(subCmd, client) addShowFlags(subCmd, client)
showCommand.AddCommand(subCmd) showCommand.AddCommand(subCmd)
@ -176,6 +198,10 @@ func runShow(args []string, client *action.Show) (string, error) {
client.Version = ">0.0.0-0" client.Version = ">0.0.0-0"
} }
if err := checkOCI(args[0]); err != nil {
return "", err
}
cp, err := client.ChartPathOptions.LocateChart(args[0], settings) cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
if err != nil { if err != nil {
return "", err return "", err

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

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

@ -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: empty:
- age: Must be greater than or equal to 0 - age: Must be greater than or equal to 0

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

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

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

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

@ -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.DryRun, "dry-run", false, "simulate a uninstall")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation")
f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history") f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history")
f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all the resources are deleted before returning. It will wait for as long as --timeout")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.StringVar(&client.Description, "description", "", "add a custom description") f.StringVar(&client.Description, "description", "", "add a custom description")

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

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

@ -10,13 +10,11 @@ require (
github.com/Masterminds/squirrel v1.5.0 github.com/Masterminds/squirrel v1.5.0
github.com/Masterminds/vcs v1.13.1 github.com/Masterminds/vcs v1.13.1
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/containerd/containerd v1.4.4 github.com/containerd/containerd v1.5.4
github.com/cyphar/filepath-securejoin v0.2.2 github.com/cyphar/filepath-securejoin v0.2.2
github.com/deislabs/oras v0.11.1 github.com/distribution/distribution/v3 v3.0.0-20210804104954-38ab4c606ee3
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible 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.11.0+incompatible
github.com/evanphx/json-patch v4.9.0+incompatible
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.8.0 github.com/gofrs/flock v0.8.0
github.com/gosuri/uitable v0.0.4 github.com/gosuri/uitable v0.0.4
@ -24,27 +22,27 @@ require (
github.com/lib/pq v1.10.0 github.com/lib/pq v1.10.0
github.com/mattn/go-shellwords v1.0.11 github.com/mattn/go-shellwords v1.0.11
github.com/mitchellh/copystructure v1.1.1 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/opencontainers/image-spec v1.0.1
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.2.1 github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.0.0-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 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
k8s.io/api v0.21.0 k8s.io/api v0.22.1
k8s.io/apiextensions-apiserver v0.21.0 k8s.io/apiextensions-apiserver v0.22.1
k8s.io/apimachinery v0.21.0 k8s.io/apimachinery v0.22.1
k8s.io/apiserver v0.21.0 k8s.io/apiserver v0.22.1
k8s.io/cli-runtime v0.21.0 k8s.io/cli-runtime v0.22.1
k8s.io/client-go v0.21.0 k8s.io/client-go v0.22.1
k8s.io/klog/v2 v2.8.0 k8s.io/klog/v2 v2.9.0
k8s.io/kubectl v0.21.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 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 ( import (
"io" "io"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/action"
) )
// RegistryLogin performs a registry login operation. // RegistryLogin performs a registry login operation.
type RegistryLogin struct { type RegistryLogin struct {
cfg *Configuration cfg *action.Configuration
} }
// NewRegistryLogin creates a new RegistryLogin object with the given configuration. // NewRegistryLogin creates a new RegistryLogin object with the given configuration.
func NewRegistryLogin(cfg *Configuration) *RegistryLogin { func NewRegistryLogin(cfg *action.Configuration) *RegistryLogin {
return &RegistryLogin{ return &RegistryLogin{
cfg: cfg, cfg: cfg,
} }
@ -34,5 +37,8 @@ func NewRegistryLogin(cfg *Configuration) *RegistryLogin {
// Run executes the registry login operation // Run executes the registry login operation
func (a *RegistryLogin) Run(out io.Writer, hostname string, username string, password string, insecure bool) error { func (a *RegistryLogin) Run(out io.Writer, hostname string, username string, password string, insecure bool) error {
return a.cfg.RegistryClient.Login(hostname, username, password, insecure) return a.cfg.RegistryClient.Login(
hostname,
registry.LoginOptBasicAuth(username, password),
registry.LoginOptInsecure(insecure))
} }

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

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

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

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

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

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

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

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

@ -17,325 +17,458 @@ limitations under the License.
package registry // import "helm.sh/helm/v3/internal/experimental/registry" package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import ( import (
"bytes" "encoding/json"
"context"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"sort" "strings"
auth "github.com/deislabs/oras/pkg/auth/docker" "github.com/containerd/containerd/remotes"
"github.com/deislabs/oras/pkg/content"
"github.com/deislabs/oras/pkg/oras"
"github.com/gosuri/uitable"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"oras.land/oras-go/pkg/auth"
dockerauth "oras.land/oras-go/pkg/auth/docker"
"oras.land/oras-go/pkg/content"
"oras.land/oras-go/pkg/oras"
"helm.sh/helm/v3/internal/version"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v3/pkg/helmpath"
) )
const (
// CredentialsFileBasename is the filename for auth credentials file
CredentialsFileBasename = "config.json"
)
type ( type (
// Client works with OCI-compliant registries and local Helm chart cache // Client works with OCI-compliant registries
Client struct { Client struct {
debug bool debug bool
// path to repository config file e.g. ~/.docker/config.json // path to repository config file e.g. ~/.docker/config.json
credentialsFile string credentialsFile string
out io.Writer out io.Writer
authorizer *Authorizer authorizer auth.Client
resolver *Resolver resolver remotes.Resolver
cache *Cache
columnWidth uint
} }
// ClientOption allows specifying various settings configurable by the user for overriding the defaults
// used when creating a new default client
ClientOption func(*Client)
) )
// NewClient returns a new registry client with config // NewClient returns a new registry client with config
func NewClient(opts ...ClientOption) (*Client, error) { func NewClient(options ...ClientOption) (*Client, error) {
client := &Client{ client := &Client{
out: ioutil.Discard, out: ioutil.Discard,
} }
for _, opt := range opts { for _, option := range options {
opt(client) option(client)
} }
// set defaults if fields are missing
if client.credentialsFile == "" { if client.credentialsFile == "" {
client.credentialsFile = helmpath.CachePath("registry", CredentialsFileBasename) client.credentialsFile = helmpath.CachePath("registry", CredentialsFileBasename)
} }
if client.authorizer == nil { if client.authorizer == nil {
authClient, err := auth.NewClient(client.credentialsFile) authClient, err := dockerauth.NewClient(client.credentialsFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
client.authorizer = &Authorizer{ client.authorizer = authClient
Client: authClient,
}
} }
if client.resolver == nil { if client.resolver == nil {
resolver, err := client.authorizer.Resolver(context.Background(), http.DefaultClient, false) headers := http.Header{}
headers.Set("User-Agent", version.GetUserAgent())
opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)}
resolver, err := client.authorizer.ResolverWithOpts(opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
client.resolver = &Resolver{ client.resolver = resolver
Resolver: resolver,
}
} }
if client.cache == nil { return client, nil
cache, err := NewCache( }
CacheOptDebug(client.debug),
CacheOptWriter(client.out), // ClientOptDebug returns a function that sets the debug setting on client options set
CacheOptRoot(helmpath.CachePath("registry", CacheRootDir)), func ClientOptDebug(debug bool) ClientOption {
) return func(client *Client) {
if err != nil { client.debug = debug
return nil, err
}
client.cache = cache
} }
}
if client.columnWidth == 0 { // ClientOptWriter returns a function that sets the writer setting on client options set
client.columnWidth = 60 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 // Login logs into a registry
func (c *Client) Login(hostname string, username string, password string, insecure bool) error { func (c *Client) Login(host string, options ...LoginOption) error {
err := c.authorizer.Login(ctx(c.out, c.debug), hostname, username, password, insecure) operation := &loginOperation{}
if err != nil { for _, option := range options {
option(operation)
}
authorizerLoginOpts := []auth.LoginOption{
auth.WithLoginContext(ctx(c.out, c.debug)),
auth.WithLoginHostname(host),
auth.WithLoginUsername(operation.username),
auth.WithLoginSecret(operation.password),
auth.WithLoginUserAgent(version.GetUserAgent()),
}
if operation.insecure {
authorizerLoginOpts = append(authorizerLoginOpts, auth.WithLoginInsecure())
}
if err := c.authorizer.LoginWithOpts(authorizerLoginOpts...); err != nil {
return err return err
} }
fmt.Fprintf(c.out, "Login succeeded\n") fmt.Fprintln(c.out, "Login Succeeded")
return nil return nil
} }
// Logout logs out of a registry // LoginOptBasicAuth returns a function that sets the username/password settings on login
func (c *Client) Logout(hostname string) error { func LoginOptBasicAuth(username string, password string) LoginOption {
err := c.authorizer.Logout(ctx(c.out, c.debug), hostname) return func(operation *loginOperation) {
if err != nil { operation.username = username
return err operation.password = password
} }
fmt.Fprintln(c.out, "Logout succeeded")
return nil
} }
// PushChart uploads a chart to a registry // LoginOptInsecure returns a function that sets the insecure setting on login
func (c *Client) PushChart(ref *Reference) error { func LoginOptInsecure(insecure bool) LoginOption {
r, err := c.cache.FetchReference(ref) return func(operation *loginOperation) {
if err != nil { operation.insecure = insecure
return err
} }
if !r.Exists { }
return errors.New(fmt.Sprintf("Chart not found: %s", r.Name))
type (
// LogoutOption allows specifying various settings on logout
LogoutOption func(*logoutOperation)
logoutOperation struct{}
)
// Logout logs out of a registry
func (c *Client) Logout(host string, opts ...LogoutOption) error {
operation := &logoutOperation{}
for _, opt := range opts {
opt(operation)
} }
fmt.Fprintf(c.out, "The push refers to repository [%s]\n", r.Repo) if err := c.authorizer.Logout(ctx(c.out, c.debug), host); err != nil {
c.printCacheRefSummary(r)
layers := []ocispec.Descriptor{*r.ContentLayer}
_, err = oras.Push(ctx(c.out, c.debug), c.resolver, r.Name, c.cache.Provider(), layers,
oras.WithConfig(*r.Config), oras.WithNameValidation(nil))
if err != nil {
return err return err
} }
s := "" fmt.Fprintf(c.out, "Removing login credentials for %s\n", host)
numLayers := len(layers)
if 1 < numLayers {
s = "s"
}
fmt.Fprintf(c.out,
"%s: pushed to remote (%d layer%s, %s total)\n", r.Tag, numLayers, s, byteCountBinary(r.Size))
return nil return nil
} }
// PullChart downloads a chart from a registry type (
func (c *Client) PullChart(ref *Reference) (*bytes.Buffer, error) { // PullOption allows specifying various settings on pull
buf := bytes.NewBuffer(nil) PullOption func(*pullOperation)
if ref.Tag == "" { // PullResult is the result returned upon successful pull.
return buf, errors.New("tag explicitly required") PullResult struct {
Manifest *descriptorPullSummary `json:"manifest"`
Config *descriptorPullSummary `json:"config"`
Chart *descriptorPullSummaryWithMeta `json:"chart"`
Prov *descriptorPullSummary `json:"prov"`
Ref string `json:"ref"`
} }
fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo) descriptorPullSummary struct {
Data []byte `json:"-"`
store := content.NewMemoryStore() Digest string `json:"digest"`
fullname := ref.FullName() Size int64 `json:"size"`
_ = fullname
_, layerDescriptors, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref.FullName(), store,
oras.WithPullEmptyNameAllowed(),
oras.WithAllowedMediaTypes(KnownMediaTypes()))
if err != nil {
return buf, err
} }
numLayers := len(layerDescriptors) descriptorPullSummaryWithMeta struct {
if numLayers < 1 { descriptorPullSummary
return buf, errors.New( Meta *chart.Metadata `json:"meta"`
fmt.Sprintf("manifest does not contain at least 1 layer (total: %d)", numLayers))
} }
var contentLayer *ocispec.Descriptor pullOperation struct {
for _, layer := range layerDescriptors { withChart bool
layer := layer withProv bool
switch layer.MediaType { ignoreMissingProv bool
case HelmChartContentLayerMediaType:
contentLayer = &layer
}
} }
)
if contentLayer == nil { // Pull downloads a chart from a registry
return buf, errors.New( func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
fmt.Sprintf("manifest does not contain a layer with mediatype %s", operation := &pullOperation{
HelmChartContentLayerMediaType)) withChart: true, // By default, always download the chart layer
} }
for _, option := range options {
_, b, ok := store.Get(*contentLayer) option(operation)
if !ok {
return buf, errors.Errorf("Unable to retrieve blob with digest %s", contentLayer.Digest)
} }
if !operation.withChart && !operation.withProv {
buf = bytes.NewBuffer(b) return nil, errors.New(
return buf, nil "must specify at least one layer to pull (chart/prov)")
}
// 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")
} }
existing, err := c.cache.FetchReference(ref) store := content.NewMemoryStore()
if err != nil { allowedMediaTypes := []string{
return err 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) if operation.withProv {
manifest, _, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref.FullName(), c.cache.Ingester(), 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.WithPullEmptyNameAllowed(),
oras.WithAllowedMediaTypes(KnownMediaTypes()), oras.WithAllowedMediaTypes(allowedMediaTypes))
oras.WithContentProvideIngester(c.cache.ProvideIngester()))
if err != nil { if err != nil {
return err return nil, err
} }
err = c.cache.AddManifest(ref, &manifest) numDescriptors := len(descriptors)
if err != nil { if numDescriptors < minNumDescriptors {
return err 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 configDescriptor == nil {
if err != nil { return nil, errors.New(
return err fmt.Sprintf("could not load config with mediatype %s", ConfigMediaType))
} }
if !r.Exists { if operation.withChart && chartDescriptor == nil {
return errors.New(fmt.Sprintf("Chart not found: %s", r.Name)) 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) result := &PullResult{
if !existing.Exists { Manifest: &descriptorPullSummary{
fmt.Fprintf(c.out, "Status: Downloaded newer chart for %s\n", ref.FullName()) 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 { } else {
fmt.Fprintf(c.out, "Status: Chart is up to date for %s\n", ref.FullName()) result.Manifest.Data = manifestData
} }
return err if getManifestErr != nil {
} return nil, getManifestErr
// 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
} }
c.printCacheRefSummary(r) var getConfigDescriptorErr error
err = c.cache.AddManifest(ref, r.Manifest) if _, configData, ok := store.Get(*configDescriptor); !ok {
if err != nil { getConfigDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", configDescriptor.Digest)
return err } 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) if operation.withProv && !provMissing {
return nil 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 // PullOptWithChart returns a function that sets the withChart setting on pull
func (c *Client) LoadChart(ref *Reference) (*chart.Chart, error) { func PullOptWithChart(withChart bool) PullOption {
r, err := c.cache.FetchReference(ref) return func(operation *pullOperation) {
if err != nil { operation.withChart = withChart
return nil, err
} }
if !r.Exists {
return nil, errors.New(fmt.Sprintf("Chart not found: %s", ref.FullName()))
}
c.printCacheRefSummary(r)
return r.Chart, nil
} }
// RemoveChart deletes a locally saved chart // PullOptWithProv returns a function that sets the withProv setting on pull
func (c *Client) RemoveChart(ref *Reference) error { func PullOptWithProv(withProv bool) PullOption {
r, err := c.cache.DeleteReference(ref) return func(operation *pullOperation) {
if err != nil { operation.withProv = withProv
return err
} }
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 type (
func (c *Client) PrintChartTable() error { // PushOption allows specifying various settings on push
table := uitable.New() PushOption func(*pushOperation)
table.MaxColWidth = c.columnWidth
table.AddRow("REF", "NAME", "VERSION", "DIGEST", "SIZE", "CREATED") // PushResult is the result returned upon successful push.
rows, err := c.getChartTableRows() PushResult struct {
if err != nil { Manifest *descriptorPushSummary `json:"manifest"`
return err 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 descriptorPushSummaryWithMeta struct {
func (c *Client) printCacheRefSummary(r *CacheRefSummary) { descriptorPushSummary
fmt.Fprintf(c.out, "ref: %s\n", r.Name) Meta *chart.Metadata `json:"meta"`
fmt.Fprintf(c.out, "digest: %s\n", r.Manifest.Digest.Hex()) }
fmt.Fprintf(c.out, "size: %s\n", byteCountBinary(r.Size))
fmt.Fprintf(c.out, "name: %s\n", r.Chart.Metadata.Name)
fmt.Fprintf(c.out, "version: %s\n", r.Chart.Metadata.Version)
}
// getChartTableRows returns rows in uitable-friendly format pushOperation struct {
func (c *Client) getChartTableRows() ([][]interface{}, error) { provData []byte
rr, err := c.cache.ListReferences() 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 { if err != nil {
return nil, err return nil, err
} }
refsMap := map[string]map[string]string{} if operation.strictMode {
for _, r := range rr { if !strings.HasSuffix(ref, fmt.Sprintf("/%s:%s", meta.Name, meta.Version)) {
refsMap[r.Name] = map[string]string{ return nil, errors.New(
"name": r.Chart.Metadata.Name, "strict mode enabled, ref basename and tag must match the chart name and version")
"version": r.Chart.Metadata.Version,
"digest": shortDigest(r.Manifest.Digest.Hex()),
"size": byteCountBinary(r.Size),
"created": timeAgo(r.CreatedAt),
} }
} }
// Sort and convert to format expected by uitable store := content.NewMemoryStore()
rows := make([][]interface{}, len(refsMap)) chartDescriptor := store.Add("", ChartLayerMediaType, data)
keys := make([]string, 0, len(refsMap)) configData, err := json.Marshal(meta)
for key := range refsMap { if err != nil {
keys = append(keys, key) return nil, err
} }
sort.Strings(keys) configDescriptor := store.Add("", ConfigMediaType, configData)
for i, key := range keys { descriptors := []ocispec.Descriptor{chartDescriptor}
rows[i] = make([]interface{}, 6) var provDescriptor ocispec.Descriptor
rows[i][0] = key if operation.provData != nil {
ref := refsMap[key] provDescriptor = store.Add("", ProvLayerMediaType, operation.provData)
for j, k := range []string{"name", "version", "digest", "size", "created"} { descriptors = append(descriptors, provDescriptor)
rows[i][j+1] = ref[k] }
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" "time"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
auth "github.com/deislabs/oras/pkg/auth/docker" "github.com/distribution/distribution/v3/configuration"
"github.com/docker/distribution/configuration" "github.com/distribution/distribution/v3/registry"
"github.com/docker/distribution/registry" _ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
_ "github.com/docker/distribution/registry/auth/htpasswd" _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
_ "github.com/docker/distribution/registry/storage/driver/inmemory"
"github.com/phayes/freeport" "github.com/phayes/freeport"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"helm.sh/helm/v3/pkg/chart"
) )
var ( var (
testCacheRootDir = "helm-registry-test" testWorkspaceDir = "helm-registry-test"
testHtpasswdFileBasename = "authtest.htpasswd" testHtpasswdFileBasename = "authtest.htpasswd"
testUsername = "myuser" testUsername = "myuser"
testPassword = "mypass" testPassword = "mypass"
@ -56,51 +53,32 @@ type RegistryClientTestSuite struct {
Out io.Writer Out io.Writer
DockerRegistryHost string DockerRegistryHost string
CompromisedRegistryHost string CompromisedRegistryHost string
CacheRootDir string WorkspaceDir string
RegistryClient *Client RegistryClient *Client
} }
func (suite *RegistryClientTestSuite) SetupSuite() { func (suite *RegistryClientTestSuite) SetupSuite() {
suite.CacheRootDir = testCacheRootDir suite.WorkspaceDir = testWorkspaceDir
os.RemoveAll(suite.CacheRootDir) os.RemoveAll(suite.WorkspaceDir)
os.Mkdir(suite.CacheRootDir, 0700) os.Mkdir(suite.WorkspaceDir, 0700)
var out bytes.Buffer var out bytes.Buffer
suite.Out = &out suite.Out = &out
credentialsFile := filepath.Join(suite.CacheRootDir, CredentialsFileBasename) credentialsFile := filepath.Join(suite.WorkspaceDir, CredentialsFileBasename)
client, err := auth.NewClient(credentialsFile)
suite.Nil(err, "no error creating auth client")
resolver, err := client.Resolver(context.Background(), http.DefaultClient, false)
suite.Nil(err, "no error creating resolver")
// create cache
cache, err := NewCache(
CacheOptDebug(true),
CacheOptWriter(suite.Out),
CacheOptRoot(filepath.Join(suite.CacheRootDir, CacheRootDir)),
)
suite.Nil(err, "no error creating cache")
// init test client // init test client
var err error
suite.RegistryClient, err = NewClient( suite.RegistryClient, err = NewClient(
ClientOptDebug(true), ClientOptDebug(true),
ClientOptWriter(suite.Out), ClientOptWriter(suite.Out),
ClientOptAuthorizer(&Authorizer{ ClientOptCredentialsFile(credentialsFile),
Client: client,
}),
ClientOptResolver(&Resolver{
Resolver: resolver,
}),
ClientOptCache(cache),
) )
suite.Nil(err, "no error creating registry client") suite.Nil(err, "no error creating registry client")
// create htpasswd file (w BCrypt, which is required) // create htpasswd file (w BCrypt, which is required)
pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost)
suite.Nil(err, "no error generating bcrypt password for test htpasswd file") suite.Nil(err, "no error generating bcrypt password for test htpasswd file")
htpasswdPath := filepath.Join(suite.CacheRootDir, testHtpasswdFileBasename) htpasswdPath := filepath.Join(suite.WorkspaceDir, testHtpasswdFileBasename)
err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644) err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644)
suite.Nil(err, "no error creating test htpasswd file") suite.Nil(err, "no error creating test htpasswd file")
@ -109,7 +87,7 @@ func (suite *RegistryClientTestSuite) SetupSuite() {
port, err := freeport.GetFreePort() port, err := freeport.GetFreePort()
suite.Nil(err, "no error finding free port for test registry") suite.Nil(err, "no error finding free port for test registry")
suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port) suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port)
config.HTTP.Addr = fmt.Sprintf(":%d", port) config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port)
config.HTTP.DrainTimeout = time.Duration(10) * time.Second config.HTTP.DrainTimeout = time.Duration(10) * time.Second
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
config.Auth = configuration.Auth{ config.Auth = configuration.Auth{
@ -128,110 +106,195 @@ func (suite *RegistryClientTestSuite) SetupSuite() {
} }
func (suite *RegistryClientTestSuite) TearDownSuite() { func (suite *RegistryClientTestSuite) TearDownSuite() {
os.RemoveAll(suite.CacheRootDir) os.RemoveAll(suite.WorkspaceDir)
} }
func (suite *RegistryClientTestSuite) Test_0_Login() { func (suite *RegistryClientTestSuite) Test_0_Login() {
err := suite.RegistryClient.Login(suite.DockerRegistryHost, "badverybad", "ohsobad", false) err := suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth("badverybad", "ohsobad"),
LoginOptInsecure(false))
suite.NotNil(err, "error logging into registry with bad credentials") suite.NotNil(err, "error logging into registry with bad credentials")
err = suite.RegistryClient.Login(suite.DockerRegistryHost, "badverybad", "ohsobad", true) err = suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth("badverybad", "ohsobad"),
LoginOptInsecure(true))
suite.NotNil(err, "error logging into registry with bad credentials, insecure mode") suite.NotNil(err, "error logging into registry with bad credentials, insecure mode")
err = suite.RegistryClient.Login(suite.DockerRegistryHost, testUsername, testPassword, false) err = suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth(testUsername, testPassword),
LoginOptInsecure(false))
suite.Nil(err, "no error logging into registry with good credentials") suite.Nil(err, "no error logging into registry with good credentials")
err = suite.RegistryClient.Login(suite.DockerRegistryHost, testUsername, testPassword, true) err = suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth(testUsername, testPassword),
LoginOptInsecure(true))
suite.Nil(err, "no error logging into registry with good credentials, insecure mode") suite.Nil(err, "no error logging into registry with good credentials, insecure mode")
} }
func (suite *RegistryClientTestSuite) Test_1_SaveChart() { func (suite *RegistryClientTestSuite) Test_1_Push() {
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) // Bad bytes
suite.Nil(err) ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)
_, err := suite.RegistryClient.Push([]byte("hello"), ref)
// empty chart suite.NotNil(err, "error pushing non-chart bytes")
err = suite.RegistryClient.SaveChart(&chart.Chart{}, ref)
suite.NotNil(err) // Load a test chart
chartData, err := ioutil.ReadFile("../../../pkg/repo/repotest/testdata/examplechart-0.1.0.tgz")
// valid chart suite.Nil(err, "no error loading test chart")
ch := &chart.Chart{} meta, err := extractChartMeta(chartData)
ch.Metadata = &chart.Metadata{ suite.Nil(err, "no error extracting chart meta")
APIVersion: "v1",
Name: "testchart", // non-strict ref (chart name)
Version: "1.2.3", ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version)
} _, err = suite.RegistryClient.Push(chartData, ref)
err = suite.RegistryClient.SaveChart(ch, ref) suite.NotNil(err, "error pushing non-strict ref (bad basename)")
suite.Nil(err)
// non-strict ref (chart name), with strict mode disabled
_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false))
suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled")
// non-strict ref (chart version)
ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name)
_, err = suite.RegistryClient.Push(chartData, ref)
suite.NotNil(err, "error pushing non-strict ref (bad tag)")
// non-strict ref (chart version), with strict mode disabled
_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false))
suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled")
// basic push, good ref
chartData, err = ioutil.ReadFile("../../../pkg/downloader/testdata/local-subchart-0.1.0.tgz")
suite.Nil(err, "no error loading test chart")
meta, err = extractChartMeta(chartData)
suite.Nil(err, "no error extracting chart meta")
ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
_, err = suite.RegistryClient.Push(chartData, ref)
suite.Nil(err, "no error pushing good ref")
_, err = suite.RegistryClient.Pull(ref)
suite.Nil(err, "no error pulling a simple chart")
// Load another test chart
chartData, err = ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz")
suite.Nil(err, "no error loading test chart")
meta, err = extractChartMeta(chartData)
suite.Nil(err, "no error extracting chart meta")
// Load prov file
provData, err := ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz.prov")
suite.Nil(err, "no error loading test prov")
// push with prov
ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData))
suite.Nil(err, "no error pushing good ref with prov")
_, err = suite.RegistryClient.Pull(ref)
suite.Nil(err, "no error pulling a simple chart")
// Validate the output
// Note: these digests/sizes etc may change if the test chart/prov files are modified,
// or if the format of the OCI manifest changes
suite.Equal(ref, result.Ref)
suite.Equal(meta.Name, result.Chart.Meta.Name)
suite.Equal(meta.Version, result.Chart.Meta.Version)
suite.Equal(int64(512), result.Manifest.Size)
suite.Equal(int64(99), result.Config.Size)
suite.Equal(int64(973), result.Chart.Size)
suite.Equal(int64(695), result.Prov.Size)
suite.Equal(
"sha256: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() { func (suite *RegistryClientTestSuite) Test_2_Pull() {
// bad/missing ref
// non-existent ref ref := fmt.Sprintf("%s/testrepo/no-existy:1.2.3", suite.DockerRegistryHost)
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) _, err := suite.RegistryClient.Pull(ref)
suite.Nil(err) suite.NotNil(err, "error on bad/missing ref")
_, err = suite.RegistryClient.LoadChart(ref)
suite.NotNil(err) // Load test chart (to build ref pushed in previous test)
chartData, err := ioutil.ReadFile("../../../pkg/downloader/testdata/local-subchart-0.1.0.tgz")
// existing ref suite.Nil(err, "no error loading test chart")
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) meta, err := extractChartMeta(chartData)
suite.Nil(err) suite.Nil(err, "no error extracting chart meta")
ch, err := suite.RegistryClient.LoadChart(ref) ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
suite.Nil(err)
suite.Equal("testchart", ch.Metadata.Name) // Simple pull, chart only
suite.Equal("1.2.3", ch.Metadata.Version) _, err = suite.RegistryClient.Pull(ref)
} suite.Nil(err, "no error pulling a simple chart")
func (suite *RegistryClientTestSuite) Test_3_PushChart() { // Simple pull with prov (no prov uploaded)
_, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true))
// non-existent ref suite.NotNil(err, "error pulling a chart with prov when no prov exists")
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
suite.Nil(err) // Simple pull with prov, ignoring missing prov
err = suite.RegistryClient.PushChart(ref) _, err = suite.RegistryClient.Pull(ref,
suite.NotNil(err) PullOptWithProv(true),
PullOptIgnoreMissingProv(true))
// existing ref suite.Nil(err,
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) "no error pulling a chart with prov when no prov exists, ignoring missing")
suite.Nil(err)
err = suite.RegistryClient.PushChart(ref) // Load test chart (to build ref pushed in previous test)
suite.Nil(err) chartData, err = ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz")
} suite.Nil(err, "no error loading test chart")
meta, err = extractChartMeta(chartData)
func (suite *RegistryClientTestSuite) Test_4_PullChart() { suite.Nil(err, "no error extracting chart meta")
ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
// non-existent ref
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) // Load prov file
suite.Nil(err) provData, err := ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz.prov")
_, err = suite.RegistryClient.PullChart(ref) suite.Nil(err, "no error loading test prov")
suite.NotNil(err)
// no chart and no prov causes error
// existing ref _, err = suite.RegistryClient.Pull(ref,
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) PullOptWithChart(false),
suite.Nil(err) PullOptWithProv(false))
_, err = suite.RegistryClient.PullChart(ref) suite.NotNil(err, "error on both no chart and no prov")
suite.Nil(err)
} // full pull with chart and prov
result, err := suite.RegistryClient.Pull(ref, PullOptWithProv(true))
func (suite *RegistryClientTestSuite) Test_5_PrintChartTable() { suite.Nil(err, "no error pulling a chart with prov")
err := suite.RegistryClient.PrintChartTable()
suite.Nil(err) // Validate the output
} // Note: these digests/sizes etc may change if the test chart/prov files are modified,
// or if the format of the OCI manifest changes
func (suite *RegistryClientTestSuite) Test_6_RemoveChart() { suite.Equal(ref, result.Ref)
suite.Equal(meta.Name, result.Chart.Meta.Name)
// non-existent ref suite.Equal(meta.Version, result.Chart.Meta.Version)
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) suite.Equal(int64(512), result.Manifest.Size)
suite.Nil(err) suite.Equal(int64(99), result.Config.Size)
err = suite.RegistryClient.RemoveChart(ref) suite.Equal(int64(973), result.Chart.Size)
suite.NotNil(err) suite.Equal(int64(695), result.Prov.Size)
suite.Equal(
// existing ref "sha256:c4fd4ca31f12f50a7f704bb1dfdf2e768b1e8bdeac3991b534b6bdb3f535aab1",
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) result.Manifest.Digest)
suite.Nil(err) suite.Equal(
err = suite.RegistryClient.RemoveChart(ref) "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
suite.Nil(err) result.Config.Digest)
suite.Equal(
"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
result.Chart.Digest)
suite.Equal(
"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
result.Prov.Digest)
suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.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") err := suite.RegistryClient.Logout("this-host-aint-real:5000")
suite.NotNil(err, "error logging out of registry that has no entry") suite.NotNil(err, "error logging out of registry that has no entry")
@ -239,12 +302,11 @@ func (suite *RegistryClientTestSuite) Test_7_Logout() {
suite.Nil(err, "no error logging out of registry") suite.Nil(err, "no error logging out of registry")
} }
func (suite *RegistryClientTestSuite) Test_8_ManInTheMiddle() { func (suite *RegistryClientTestSuite) Test_4_ManInTheMiddle() {
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/supposedlysafechart:9.9.9", suite.CompromisedRegistryHost)) ref := fmt.Sprintf("%s/testrepo/supposedlysafechart:9.9.9", suite.CompromisedRegistryHost)
suite.Nil(err)
// returns content that does not match the expected digest // returns content that does not match the expected digest
_, err = suite.RegistryClient.PullChart(ref) _, err := suite.RegistryClient.Pull(ref)
suite.NotNil(err) suite.NotNil(err)
suite.True(errdefs.IsFailedPrecondition(err)) suite.True(errdefs.IsFailedPrecondition(err))
} }
@ -261,19 +323,19 @@ func initCompromisedRegistryTestServer() string {
// layers[0] is the blob []byte("a") // layers[0] is the blob []byte("a")
w.Write([]byte( w.Write([]byte(
`{ "schemaVersion": 2, "config": { fmt.Sprintf(`{ "schemaVersion": 2, "config": {
"mediaType": "application/vnd.cncf.helm.config.v1+json", "mediaType": "%s",
"digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133", "digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133",
"size": 181 "size": 181
}, },
"layers": [ "layers": [
{ {
"mediaType": "application/tar+gzip", "mediaType": "%s",
"digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", "digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
"size": 1 "size": 1
} }
] ]
}`)) }`, ConfigMediaType, ChartLayerMediaType)))
} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" { } else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200) w.WriteHeader(200)
@ -281,7 +343,7 @@ func initCompromisedRegistryTestServer() string {
"an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" + "an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" +
"\"application\"}")) "\"application\"}"))
} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" { } else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" {
w.Header().Set("Content-Type", "application/tar+gzip") w.Header().Set("Content-Type", ChartLayerMediaType)
w.WriteHeader(200) w.WriteHeader(200)
w.Write([]byte("b")) w.Write([]byte("b"))
} else { } else {

@ -17,17 +17,18 @@ limitations under the License.
package registry // import "helm.sh/helm/v3/internal/experimental/registry" package registry // import "helm.sh/helm/v3/internal/experimental/registry"
const ( const (
// HelmChartConfigMediaType is the reserved media type for the Helm chart manifest config // OCIScheme is the URL scheme for OCI-based requests
HelmChartConfigMediaType = "application/vnd.cncf.helm.config.v1+json" OCIScheme = "oci"
// HelmChartContentLayerMediaType is the reserved media type for Helm chart package content // CredentialsFileBasename is the filename for auth credentials file
HelmChartContentLayerMediaType = "application/tar+gzip" CredentialsFileBasename = "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 // ChartLayerMediaType is the reserved media type for Helm chart package content
func KnownMediaTypes() []string { ChartLayerMediaType = "application/vnd.cncf.helm.chart.content.v1.tar+gzip"
return []string{
HelmChartConfigMediaType, // ProvLayerMediaType is the reserved media type for Helm chart provenance files
HelmChartContentLayerMediaType, 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" package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
"time" "strings"
orascontext "github.com/deislabs/oras/pkg/context"
units "github.com/docker/go-units"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
orascontext "oras.land/oras-go/pkg/context"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
) )
// byteCountBinary produces a human-readable file size // IsOCI determines whether or not a URL is to be treated as an OCI URL
func byteCountBinary(b int64) string { func IsOCI(url string) bool {
const unit = 1024 return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme))
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
} }
// shortDigest returns first 7 characters of a sha256 digest // extractChartMeta is used to extract a chart metadata from a byte array
func shortDigest(digest string) string { func extractChartMeta(chartData []byte) (*chart.Metadata, error) {
if len(digest) == 64 { ch, err := loader.LoadArchive(bytes.NewReader(chartData))
return digest[:7] if err != nil {
return nil, err
} }
return digest return ch.Metadata, nil
}
// timeAgo returns a human-readable timestamp representing time that has passed
func timeAgo(t time.Time) string {
return units.HumanDuration(time.Now().UTC().Sub(t))
} }
// ctx retrieves a fresh context. // ctx retrieves a fresh context.

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

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

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

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

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

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

@ -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 ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
@ -25,6 +26,7 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"text/template" "text/template"
"time" "time"
@ -36,6 +38,7 @@ import (
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/cli"
@ -104,6 +107,8 @@ type Install struct {
// OutputDir/<ReleaseName> // OutputDir/<ReleaseName>
UseReleaseName bool UseReleaseName bool
PostRenderer postrender.PostRenderer 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 // 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 // Run executes the installation
// //
// If DryRun is set to true, this will prepare the release, but not install it // 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) { 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`) // Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`)
if !i.ClientOnly { if !i.ClientOnly {
if err := i.cfg.KubeClient.IsReachable(); err != nil { 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. // not working.
return rel, err 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 // pre-install hooks
if !i.DisableHooks { if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil { 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. // to true, since that is basically an upgrade operation.
if len(toBeAdopted) == 0 && len(resources) > 0 { if len(toBeAdopted) == 0 && len(resources) > 0 {
if _, err := i.cfg.KubeClient.Create(resources); err != nil { 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 { } else if len(resources) > 0 {
if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, false); err != nil { 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.Wait {
if i.WaitForJobs { if i.WaitForJobs {
if err := i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout); err != nil { if err := i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout); err != nil {
return i.failRelease(rel, err) i.reportToRun(c, rel, err)
return
} }
} else { } else {
if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil { 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 !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil { 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) 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) { 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())) rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
if i.Atomic { if i.Atomic {
@ -663,6 +704,14 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
RepositoryConfig: settings.RepositoryConfig, RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache, 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 { if c.Verify {
dl.Verify = downloader.VerifyAlways dl.Verify = downloader.VerifyAlways
} }
@ -716,5 +765,6 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
if version != "" { if version != "" {
atVersion = fmt.Sprintf(" at version %q", 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 package action
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@ -25,6 +26,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -34,7 +36,7 @@ import (
kubefake "helm.sh/helm/v3/pkg/kube/fake" kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver" "helm.sh/helm/v3/pkg/storage/driver"
"helm.sh/helm/v3/pkg/time" helmtime "helm.sh/helm/v3/pkg/time"
) )
type nameTemplateTestCase struct { type nameTemplateTestCase struct {
@ -361,7 +363,25 @@ func TestInstallRelease_Wait(t *testing.T) {
is.Contains(res.Info.Description, "I timed out") is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed) 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) { func TestInstallRelease_WaitForJobs(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(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") 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) { func TestNameTemplate(t *testing.T) {
testCases := []nameTemplateTestCase{ testCases := []nameTemplateTestCase{
// Just a straight up nop please // Just a straight up nop please
@ -624,32 +670,32 @@ func TestNameAndChartGenerateName(t *testing.T) {
{ {
"local filepath", "local filepath",
"./chart", "./chart",
fmt.Sprintf("chart-%d", time.Now().Unix()), fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
}, },
{ {
"dot filepath", "dot filepath",
".", ".",
fmt.Sprintf("chart-%d", time.Now().Unix()), fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
}, },
{ {
"empty filepath", "empty filepath",
"", "",
fmt.Sprintf("chart-%d", time.Now().Unix()), fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
}, },
{ {
"packaged chart", "packaged chart",
"chart.tgz", "chart.tgz",
fmt.Sprintf("chart-%d", time.Now().Unix()), fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
}, },
{ {
"packaged chart with .tar.gz extension", "packaged chart with .tar.gz extension",
"chart.tar.gz", "chart.tar.gz",
fmt.Sprintf("chart-%d", time.Now().Unix()), fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
}, },
{ {
"packaged chart with local extension", "packaged chart with local extension",
"./chart.tgz", "./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" "github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v3/pkg/downloader"
@ -90,7 +91,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
RepositoryCache: p.Settings.RepositoryCache, RepositoryCache: p.Settings.RepositoryCache,
} }
if strings.HasPrefix(chartRef, "oci://") { if registry.IsOCI(chartRef) {
if p.Version == "" { if p.Version == "" {
return out.String(), errors.Errorf("--version flag is explicitly required for OCI registries") return out.String(), errors.Errorf("--version flag is explicitly required for OCI registries")
} }

@ -17,6 +17,7 @@ limitations under the License.
package action package action
import ( import (
"bytes"
"fmt" "fmt"
"strings" "strings"
@ -41,6 +42,8 @@ const (
ShowValues ShowOutputFormat = "values" ShowValues ShowOutputFormat = "values"
// ShowReadme is the format which only shows the chart's README // ShowReadme is the format which only shows the chart's README
ShowReadme ShowOutputFormat = "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"} 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 == ShowReadme || s.OutputFormat == ShowAll {
if s.OutputFormat == ShowAll {
fmt.Fprintln(&out, "---")
}
readme := findReadme(s.chart.Files) readme := findReadme(s.chart.Files)
if readme == nil { if readme != nil {
return out.String(), 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 return out.String(), nil
} }

@ -28,6 +28,9 @@ func TestShow(t *testing.T) {
Metadata: &chart.Metadata{Name: "alpine"}, Metadata: &chart.Metadata{Name: "alpine"},
Files: []*chart.File{ Files: []*chart.File{
{Name: "README.md", Data: []byte("README\n")}, {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{ Raw: []*chart.File{
{Name: "values.yaml", Data: []byte("VALUES\n")}, {Name: "values.yaml", Data: []byte("VALUES\n")},
@ -48,6 +51,12 @@ VALUES
--- ---
README README
---
foo
---
bar
` `
if output != expect { if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) 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) 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" "github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil" "helm.sh/helm/v3/pkg/releaseutil"
helmtime "helm.sh/helm/v3/pkg/time" helmtime "helm.sh/helm/v3/pkg/time"
@ -37,6 +38,7 @@ type Uninstall struct {
DisableHooks bool DisableHooks bool
DryRun bool DryRun bool
KeepHistory bool KeepHistory bool
Wait bool
Timeout time.Duration Timeout time.Duration
Description string 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) u.cfg.Log("uninstall: Failed to store updated release: %s", err)
} }
kept, errs := u.deleteRelease(rel) deletedResources, kept, errs := u.deleteRelease(rel)
if kept != "" { if kept != "" {
kept = "These resources were kept due to the resource policy:\n" + kept kept = "These resources were kept due to the resource policy:\n" + kept
} }
res.Info = 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 !u.DisableHooks {
if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil { if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil {
errs = append(errs, err) errs = append(errs, err)
@ -172,12 +182,12 @@ func joinErrors(errs []error) string {
return strings.Join(es, "; ") return strings.Join(es, "; ")
} }
// deleteRelease deletes the release and returns manifests that were kept in the deletion process // 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) (string, []error) { func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, string, []error) {
var errs []error var errs []error
caps, err := u.cfg.getCapabilities() caps, err := u.cfg.getCapabilities()
if err != nil { 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) 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 // 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 // deletion. The problem with this is that we could get a false positive
// and delete something that was not legitimately part of this release. // 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) 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) resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false)
if err != nil { 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 { if len(resources) > 0 {
_, errs = u.cfg.KubeClient.Delete(resources) _, errs = u.cfg.KubeClient.Delete(resources)
} }
return kept, errs return resources, kept, errs
} }

@ -17,9 +17,13 @@ limitations under the License.
package action package action
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/assert" "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 { func uninstallAction(t *testing.T) *Uninstall {
@ -60,3 +64,34 @@ func TestUninstallRelease_deleteRelease(t *testing.T) {
` `
is.Contains(res.Info, expected) 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" "context"
"fmt" "fmt"
"strings" "strings"
"sync"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -100,6 +101,13 @@ type Upgrade struct {
DisableOpenAPIValidation bool DisableOpenAPIValidation bool
// Get missing dependencies // Get missing dependencies
DependencyUpdate bool 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. // 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) { 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 { if err := u.cfg.KubeClient.IsReachable(); err != nil {
return nil, err 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.Releases.MaxHistory = u.MaxHistory
u.cfg.Log("performing update for %s", name) u.cfg.Log("performing update for %s", name)
res, err := u.performUpgrade(currentRelease, upgradedRelease) res, err := u.performUpgrade(ctx, currentRelease, upgradedRelease)
if err != nil { if err != nil {
return res, err return res, err
} }
@ -243,7 +257,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return currentRelease, upgradedRelease, err 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) current, err := u.cfg.KubeClient.Build(bytes.NewBufferString(originalRelease.Manifest), false)
if err != nil { if err != nil {
// Checking for removed Kubernetes API error so can provide a more informative error message to the user // 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 { if err := u.cfg.Releases.Create(upgradedRelease); err != nil {
return nil, err 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 // pre-upgrade hooks
if !u.DisableHooks { if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.Timeout); err != nil { 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 { } else {
u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name) 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) results, err := u.cfg.KubeClient.Update(current, target, u.Force)
if err != nil { if err != nil {
u.cfg.recordRelease(originalRelease) u.cfg.recordRelease(originalRelease)
return u.failRelease(upgradedRelease, results.Created, err) u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
} }
if u.Recreate { if u.Recreate {
@ -336,12 +383,14 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
if u.WaitForJobs { if u.WaitForJobs {
if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil { if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease) u.cfg.recordRelease(originalRelease)
return u.failRelease(upgradedRelease, results.Created, err) u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
} }
} else { } else {
if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil { if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease) 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 // post-upgrade hooks
if !u.DisableHooks { if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.Timeout); err != nil { 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 { } else {
upgradedRelease.Info.Description = "Upgrade complete" upgradedRelease.Info.Description = "Upgrade complete"
} }
u.reportToPerformUpgrade(c, upgradedRelease, nil, nil)
return upgradedRelease, nil
} }
func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, err error) (*release.Release, error) { 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 package action
import ( import (
"context"
"fmt" "fmt"
"testing" "testing"
"time"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
@ -27,7 +29,7 @@ import (
kubefake "helm.sh/helm/v3/pkg/kube/fake" kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/time" helmtime "helm.sh/helm/v3/pkg/time"
) )
func upgradeAction(t *testing.T) *Upgrade { func upgradeAction(t *testing.T) *Upgrade {
@ -225,7 +227,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) {
withValues(chartDefaultValues), withValues(chartDefaultValues),
withMetadataDependency(dependency), withMetadataDependency(dependency),
) )
now := time.Now() now := helmtime.Now()
existingValues := map[string]interface{}{ existingValues := map[string]interface{}{
"subchart": map[string]interface{}{ "subchart": map[string]interface{}{
"enabled": false, "enabled": false,
@ -296,3 +298,66 @@ func TestUpgradeRelease_Pending(t *testing.T) {
_, err := upAction.Run(rel.Name, buildChart(), vals) _, err := upAction.Run(rel.Name, buildChart(), vals)
req.Contains(err.Error(), "progress", err) 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) { func TestDefaultCapabilitiesHelmVersion(t *testing.T) {
hv := DefaultCapabilities.HelmVersion hv := DefaultCapabilities.HelmVersion
if hv.Version != "v3.6" { if hv.Version != "v3.7" {
t.Errorf("Expected default HelmVersion to be v3.6, got %q", hv.Version) 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. // top-down.
CoalesceTables(vv, destvmap) CoalesceTables(vv, destvmap)
dg[key] = vv dg[key] = vv
continue
} }
} }
} else if dv, ok := dg[key]; ok && istable(dv) { } else if dv, ok := dg[key]; ok && istable(dv) {
// It's not clear if this condition can actually ever trigger. // It's not clear if this condition can actually ever trigger.
log.Printf("key %s is table. Skipping", key) log.Printf("key %s is table. Skipping", key)
continue
} else { } else {
// TODO: Do we need to do any additional checking on the value? // TODO: Do we need to do any additional checking on the value?
dg[key] = val dg[key] = val
@ -159,15 +157,15 @@ func coalesceValues(c *chart.Chart, v map[string]interface{}) {
src, ok := val.(map[string]interface{}) src, ok := val.(map[string]interface{})
if !ok { if !ok {
// If the original value is nil, there is nothing to coalesce, so we don't print // 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 { if val != nil {
log.Printf("warning: skipped value for %s: Not a table.", key) 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 { } else {
// If the key is not in v, copy it from nv. // If the key is not in v, copy it from nv.

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

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

@ -16,7 +16,6 @@ limitations under the License.
package downloader package downloader
import ( import (
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -171,19 +170,7 @@ func TestIsTar(t *testing.T) {
} }
func TestDownloadTo(t *testing.T) { func TestDownloadTo(t *testing.T) {
// Set up a fake repo with basic auth enabled srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/*.tgz*")
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()
defer srv.Stop() defer srv.Stop()
if err := srv.CreateIndex(); err != nil { if err := srv.CreateIndex(); err != nil {
t.Fatal(err) t.Fatal(err)

@ -249,22 +249,24 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
destPath := filepath.Join(m.ChartPath, "charts") destPath := filepath.Join(m.ChartPath, "charts")
tmpPath := filepath.Join(m.ChartPath, "tmpcharts") tmpPath := filepath.Join(m.ChartPath, "tmpcharts")
// Create 'charts' directory if it doesn't already exist. // 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, 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 { if err := os.MkdirAll(destPath, 0755); err != nil {
return err return err
} }
} else if !fi.IsDir() { } else {
return errors.Errorf("%q is not a directory", destPath) return fmt.Errorf("unable to retrieve file info for '%s': %v", destPath, err)
}
if err := fs.RenameWithFallback(destPath, tmpPath); err != nil {
return errors.Wrap(err, "unable to move current charts to tmp dir")
} }
if err := os.MkdirAll(destPath, 0755); err != nil { // Prepare tmpPath
if err := os.MkdirAll(tmpPath, 0755); err != nil {
return err return err
} }
defer os.RemoveAll(tmpPath)
fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
var saveError error var saveError error
@ -273,10 +275,11 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
// No repository means the chart is in charts directory // No repository means the chart is in charts directory
if dep.Repository == "" { if dep.Repository == "" {
fmt.Fprintf(m.Out, "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n", dep.Name) 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) ch, err := loader.LoadDir(chartPath)
if err != nil { 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) constraint, err := semver.NewConstraint(dep.Version)
@ -299,7 +302,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
if m.Debug { if m.Debug {
fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository) 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 { if err != nil {
saveError = err saveError = err
break break
@ -354,8 +357,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
getter.WithTagName(version)) getter.WithTagName(version))
} }
_, _, err = dl.DownloadTo(churl, version, destPath) if _, _, err = dl.DownloadTo(churl, version, tmpPath); err != nil {
if err != nil {
saveError = errors.Wrapf(err, "could not download %s", churl) saveError = errors.Wrapf(err, "could not download %s", churl)
break break
} }
@ -363,36 +365,14 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
churls[churl] = struct{}{} 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 { if saveError == nil {
fmt.Fprintln(m.Out, "Deleting outdated charts") // now we can move all downloaded charts to destPath and delete outdated dependencies
for _, dep := range deps { if err := m.safeMoveDeps(deps, tmpPath, destPath); err != nil {
// 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 {
return err return err
} }
if err := os.RemoveAll(tmpPath); err != nil {
return errors.Wrapf(err, "failed to remove %v", tmpPath)
}
} else { } else {
fmt.Fprintln(m.Out, "Save error occurred: ", saveError) 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 saveError
} }
return nil return nil
@ -410,36 +390,75 @@ func parseOCIRef(chartRef string) (string, string, error) {
return chartRef, tag, nil 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 // 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 // This will only return errors that should stop processing entirely. Other errors
// will emit log messages or be ignored. // will emit log messages or be ignored.
func (m *Manager) safeDeleteDep(name, dir string) error { func (m *Manager) safeMoveDeps(deps []*chart.Dependency, source, dest string) error {
files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz")) existsInSourceDirectory := map[string]bool{}
isLocalDependency := map[string]bool{}
sourceFiles, err := ioutil.ReadDir(source)
if err != nil { if err != nil {
// Only for ErrBadPattern
return err return err
} }
for _, fname := range files { // attempt to read destFiles; fail fast if we can't
ch, err := loader.LoadFile(fname) destFiles, err := ioutil.ReadDir(dest)
if err != nil { if err != nil {
fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err) return err
}
for _, dep := range deps {
if dep.Repository == "" {
isLocalDependency[dep.Name] = true
}
}
for _, file := range sourceFiles {
if file.IsDir() {
continue continue
} }
if ch.Name() != name { filename := file.Name()
// This is not the file you are looking for. 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 continue
} }
if err := os.Remove(fname); err != nil { // NOTE: no need to delete the dest; os.Rename replaces it.
fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err) if err := fs.RenameWithFallback(sourcefile, destfile); err != nil {
fmt.Fprintf(m.Out, "Unable to move %s to charts dir %s (Skipping)", sourcefile, err)
continue 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 return nil
} }
@ -783,6 +802,7 @@ func normalizeURL(baseURL, urlOrPath string) (string, error) {
return urlOrPath, errors.Wrap(err, "base URL failed to parse") return urlOrPath, errors.Wrap(err, "base URL failed to parse")
} }
u2.RawPath = path.Join(u2.RawPath, urlOrPath)
u2.Path = path.Join(u2.Path, urlOrPath) u2.Path = path.Join(u2.Path, urlOrPath)
return u2.String(), nil return u2.String(), nil
} }
@ -832,10 +852,8 @@ func writeLock(chartpath string, lock *chart.Lock, legacyLockfile bool) error {
return ioutil.WriteFile(dest, data, 0644) return ioutil.WriteFile(dest, data, 0644)
} }
// archive a dep chart from local directory and save it into charts/ // archive a dep chart from local directory and save it into destPath
func tarFromLocalDir(chartpath, name, repo, version string) (string, error) { func tarFromLocalDir(chartpath, name, repo, version, destPath string) (string, error) {
destPath := filepath.Join(chartpath, "charts")
if !strings.HasPrefix(repo, "file://") { if !strings.HasPrefix(repo, "file://") {
return "", errors.Errorf("wrong format: chart %s repository %s", name, repo) 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) 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 // The prefix to use for cache keys created by the manager for repo names
const managerKeyPrefix = "helm-manager-" const managerKeyPrefix = "helm-manager-"

@ -17,11 +17,14 @@ package downloader
import ( import (
"bytes" "bytes"
"io/ioutil"
"os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"testing" "testing"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/repo/repotest" "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: "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: "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 { 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) { func TestUpdateBeforeBuild(t *testing.T) {
// Set up a fake repo // Set up a fake repo
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")

@ -97,7 +97,7 @@ const warnStartDelim = "HELM_ERR_START"
const warnEndDelim = "HELM_ERR_END" const warnEndDelim = "HELM_ERR_END"
const recursionMaxNums = 1000 const recursionMaxNums = 1000
var warnRegex = regexp.MustCompile(warnStartDelim + `(.*)` + warnEndDelim) var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim)
func warnWrap(warn string) string { func warnWrap(warn string) string {
return warnStartDelim + warn + warnEndDelim 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 // As it recurses, it also sets the values to be appropriate for the template
// scope. // 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{}{ next := map[string]interface{}{
"Chart": c.Metadata, "Chart": chartMetaData,
"Files": newFiles(c.Files), "Files": newFiles(c.Files),
"Release": vals["Release"], "Release": vals["Release"],
"Capabilities": vals["Capabilities"], "Capabilities": vals["Capabilities"],
"Values": make(chartutil.Values), "Values": make(chartutil.Values),
"Subcharts": subCharts,
} }
// If there is a {{.Values.ThisChart}} in the parent metadata, // 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() { for _, child := range c.Dependencies() {
recAllTpls(child, templates, next) subCharts[child.Name()] = recAllTpls(child, templates, next)
} }
newParentID := c.ChartFullPath() newParentID := c.ChartFullPath()
@ -376,6 +383,8 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.
basePath: path.Join(newParentID, "templates"), basePath: path.Join(newParentID, "templates"),
} }
} }
return next
} }
// isTemplateValid returns true if the template is valid for the chart type // 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) { func TestExecErrors(t *testing.T) {
vals := chartutil.Values{"Values": map[string]interface{}{}} vals := chartutil.Values{"Values": map[string]interface{}{}}
cases := []struct {
tplsMissingRequired := map[string]renderable{ name string
"missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals}, tpls map[string]renderable
} expected string
_, err := new(Engine).render(tplsMissingRequired) }{
if err == nil { {
t.Fatalf("Expected failures while rendering: %s", err) name: "MissingRequired",
} tpls: map[string]renderable{
expected := `execution error at (missing_required:1:2): foo is required` "missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals},
if err.Error() != expected { },
t.Errorf("Expected '%s', got %q", expected, err.Error()) expected: `execution error at (missing_required:1:2): foo is required`,
} },
{
tplsMissingRequired = map[string]renderable{ name: "MissingRequiredWithColons",
"missing_required_with_colons": {tpl: `{{required ":this: message: has many: colons:" .Values.foo}}`, vals: vals}, tpls: 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 { expected: `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:`,
t.Fatalf("Expected failures while rendering: %s", err) },
} {
expected = `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:` name: "Issue6044",
if err.Error() != expected { tpls: map[string]renderable{
t.Errorf("Expected '%s', got %q", expected, err.Error()) "issue6044": {
} vals: vals,
tpl: `{{ $someEmptyValue := "" }}
issue6044tpl := `{{ $someEmptyValue := "" }}
{{ $myvar := "abc" }} {{ $myvar := "abc" }}
{{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}` {{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}`,
tplsMissingRequired = map[string]renderable{ },
"issue6044": {tpl: issue6044tpl, vals: vals}, },
} expected: `execution error at (issue6044:3:4): abc: something is missing`,
_, err = new(Engine).render(tplsMissingRequired) },
if err == nil { {
t.Fatalf("Expected failures while rendering: %s", err) 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 { for _, tt := range cases {
t.Errorf("Expected '%s', got %q", expected, err.Error()) 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) { func TestRenderDependency(t *testing.T) {
deptpl := `{{define "myblock"}}World{{end}}` deptpl := `{{define "myblock"}}World{{end}}`
toptpl := `Hello {{template "myblock"}}` toptpl := `Hello {{template "myblock"}}`
@ -384,6 +435,8 @@ func TestRenderNestedValues(t *testing.T) {
// Ensure namespacing rules are working. // Ensure namespacing rules are working.
deepestpath := "templates/inner.tpl" deepestpath := "templates/inner.tpl"
checkrelease := "templates/release.tpl" checkrelease := "templates/release.tpl"
// Ensure subcharts scopes are working.
subchartspath := "templates/subcharts.tpl"
deepest := &chart.Chart{ deepest := &chart.Chart{
Metadata: &chart.Metadata{Name: "deepest"}, 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: deepestpath, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)},
{Name: checkrelease, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)}, {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{ inner := &chart.Chart{
@ -399,7 +452,7 @@ func TestRenderNestedValues(t *testing.T) {
Templates: []*chart.File{ Templates: []*chart.File{
{Name: innerpath, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)}, {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) inner.AddDependency(deepest)
@ -407,12 +460,14 @@ func TestRenderNestedValues(t *testing.T) {
Metadata: &chart.Metadata{Name: "top"}, Metadata: &chart.Metadata{Name: "top"},
Templates: []*chart.File{ Templates: []*chart.File{
{Name: outerpath, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, {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{}{ Values: map[string]interface{}{
"what": "stinkweed", "what": "stinkweed",
"who": "me", "who": "me",
"herrick": map[string]interface{}{ "herrick": map[string]interface{}{
"who": "time", "who": "time",
"what": "Sun",
}, },
}, },
} }
@ -422,7 +477,8 @@ func TestRenderNestedValues(t *testing.T) {
"what": "rosebuds", "what": "rosebuds",
"herrick": map[string]interface{}{ "herrick": map[string]interface{}{
"deepest": map[string]interface{}{ "deepest": map[string]interface{}{
"what": "flower", "what": "flower",
"where": "Heaven",
}, },
}, },
"global": map[string]interface{}{ "global": map[string]interface{}{
@ -469,6 +525,11 @@ func TestRenderNestedValues(t *testing.T) {
if out[fullcheckrelease] != "Tomorrow will be dyin" { if out[fullcheckrelease] != "Tomorrow will be dyin" {
t.Errorf("Unexpected release: %q", out[fullcheckrelease]) 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) { func TestRenderBuiltinValues(t *testing.T) {
@ -488,6 +549,7 @@ func TestRenderBuiltinValues(t *testing.T) {
Metadata: &chart.Metadata{Name: "Troy"}, Metadata: &chart.Metadata{Name: "Troy"},
Templates: []*chart.File{ Templates: []*chart.File{
{Name: "templates/Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, {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) outer.AddDependency(inner)
@ -510,6 +572,7 @@ func TestRenderBuiltinValues(t *testing.T) {
expects := map[string]string{ expects := map[string]string{
"Troy/charts/Latium/templates/Lavinia": "Troy/charts/Latium/templates/LaviniaLatiumAeneid", "Troy/charts/Latium/templates/Lavinia": "Troy/charts/Latium/templates/LaviniaLatiumAeneid",
"Troy/templates/Aeneas": "Troy/templates/AeneasTroyAeneid", "Troy/templates/Aeneas": "Troy/templates/AeneasTroyAeneid",
"Troy/templates/Amata": "Latium Virgil",
"Troy/charts/Latium/templates/From": "Virgil Aeneid", "Troy/charts/Latium/templates/From": "Virgil Aeneid",
} }
for file, expect := range expects { for file, expect := range expects {

@ -169,7 +169,7 @@ var httpProvider = Provider{
} }
var ociProvider = Provider{ var ociProvider = Provider{
Schemes: []string{"oci"}, Schemes: []string{registry.OCIScheme},
New: NewOCIGetter, 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) { func (g *OCIGetter) get(href string) (*bytes.Buffer, error) {
client := g.opts.registryClient 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 != "" { if version := g.opts.version; version != "" {
ref = fmt.Sprintf("%s:%s", ref, version) ref = fmt.Sprintf("%s:%s", ref, version)
} }
r, err := registry.ParseReference(ref) result, err := client.Pull(ref, pullOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
buf, err := client.PullChart(r) if requestingProv {
if err != nil { return bytes.NewBuffer(result.Prov.Data), nil
return nil, err
} }
return bytes.NewBuffer(result.Chart.Data), nil
return buf, nil
} }
// NewOCIGetter constructs a valid http/https client as a Getter // 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) 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 { func (c *Client) namespace() string {
if c.Namespace != "" { if c.Namespace != "" {
return c.Namespace return c.Namespace

@ -40,6 +40,7 @@ type FailingKubeClient struct {
BuildError error BuildError error
BuildUnstructuredError error BuildUnstructuredError error
WaitAndGetCompletedPodPhaseError error WaitAndGetCompletedPodPhaseError error
WaitDuration time.Duration
} }
// Create returns the configured error if set or prints // 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) 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 { func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error {
time.Sleep(f.WaitDuration)
if f.WaitError != nil { if f.WaitError != nil {
return f.WaitError return f.WaitError
} }
@ -63,7 +65,15 @@ func (f *FailingKubeClient) WaitWithJobs(resources kube.ResourceList, d time.Dur
if f.WaitError != nil { if f.WaitError != nil {
return f.WaitError 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 // 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