diff --git a/OWNERS b/OWNERS index 536744035..aab2709e5 100644 --- a/OWNERS +++ b/OWNERS @@ -1,7 +1,6 @@ maintainers: - adamreese - bacongobbler - - fibonacci1729 - hickeyma - jdolitsky - marckhouzam @@ -9,8 +8,8 @@ maintainers: - prydonius - SlickNik - technosophos - - viglesiasce emeritus: + - fibonacci1729 - jascott1 - michelleN - migmartri @@ -19,3 +18,4 @@ emeritus: - seh - thomastaylor312 - vaikas-google + - viglesiasce diff --git a/cmd/helm/chart_list.go b/cmd/helm/chart_list.go index a9d01c9bd..e81882ccc 100644 --- a/cmd/helm/chart_list.go +++ b/cmd/helm/chart_list.go @@ -31,14 +31,18 @@ Charts are sorted by ref name, alphabetically. ` func newChartListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { - return &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 action.NewChartList(cfg).Run(out) + return chartList.Run(out) }, } + f := cmd.Flags() + f.UintVar(&chartList.ColumnWidth, "max-col-width", 60, "maximum column width for output table") + return cmd } diff --git a/cmd/helm/completion.go b/cmd/helm/completion.go index 5aea0592e..880e77c57 100644 --- a/cmd/helm/completion.go +++ b/cmd/helm/completion.go @@ -33,33 +33,41 @@ const bashCompDesc = ` Generate the autocompletion script for Helm for the bash shell. To load completions in your current shell session: -$ source <(helm completion bash) + + source <(helm completion bash) To load completions for every new session, execute once: -Linux: - $ helm completion bash > /etc/bash_completion.d/helm -MacOS: - $ helm completion bash > /usr/local/etc/bash_completion.d/helm +- Linux: + + helm completion bash > /etc/bash_completion.d/helm + +- MacOS: + + helm completion bash > /usr/local/etc/bash_completion.d/helm ` const zshCompDesc = ` Generate the autocompletion script for Helm for the zsh shell. To load completions in your current shell session: -$ source <(helm completion zsh) + + source <(helm completion zsh) To load completions for every new session, execute once: -$ helm completion zsh > "${fpath[1]}/_helm" + + helm completion zsh > "${fpath[1]}/_helm" ` const fishCompDesc = ` Generate the autocompletion script for Helm for the fish shell. To load completions in your current shell session: -$ helm completion fish | source + + helm completion fish | source To load completions for every new session, execute once: -$ helm completion fish > ~/.config/fish/completions/helm.fish + + helm completion fish > ~/.config/fish/completions/helm.fish You will need to start a new shell for this setup to take effect. ` diff --git a/cmd/helm/dependency.go b/cmd/helm/dependency.go index 6bb82e217..03874742c 100644 --- a/cmd/helm/dependency.go +++ b/cmd/helm/dependency.go @@ -100,7 +100,6 @@ func newDependencyCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func newDependencyListCmd(out io.Writer) *cobra.Command { client := action.NewDependency() - cmd := &cobra.Command{ Use: "list CHART", Aliases: []string{"ls"}, @@ -115,5 +114,9 @@ func newDependencyListCmd(out io.Writer) *cobra.Command { return client.List(chartpath, out) }, } + + f := cmd.Flags() + + f.UintVar(&client.ColumnWidth, "max-col-width", 80, "maximum column width for output table") return cmd } diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go index fe653625d..aefa836c7 100644 --- a/cmd/helm/flags.go +++ b/cmd/helm/flags.go @@ -57,6 +57,7 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file") f.BoolVar(&c.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download") f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains") } // bindOutputFlag will add the output flag to the given command and bind the diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 2994a2291..0b9d1a383 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -145,7 +145,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal f.StringVar(&client.NameTemplate, "name-template", "", "specify template used to name the release") f.StringVar(&client.Description, "description", "", "add a custom description") f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") - f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "run helm dependency update before installing the chart") + f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart") f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema") f.BoolVar(&client.Atomic, "atomic", false, "if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used") f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed. By default, CRDs are installed if not already present") diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index 6e99bb20b..4464d45ae 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -18,10 +18,36 @@ package main import ( "fmt" + "net/http" + "net/http/httptest" "testing" + + "helm.sh/helm/v3/pkg/repo/repotest" ) func TestInstall(t *testing.T) { + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*") + if err != nil { + t.Fatal(err) + } + defer srv.Stop() + + srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if !ok || username != "username" || password != "password" { + t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password) + } + })) + + srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.FileServer(http.Dir(srv.Root())).ServeHTTP(w, r) + })) + defer srv2.Close() + + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + tests := []cmdTestCase{ // Install, base case { @@ -207,6 +233,17 @@ func TestInstall(t *testing.T) { name: "install chart with only crds", cmd: "install crd-test testdata/testcharts/chart-with-only-crds --namespace default", }, + // Verify the user/pass works + { + name: "basic install with credentials", + cmd: "install aeneas reqtest --namespace default --repo " + srv.URL() + " --username username --password password", + golden: "output/install.txt", + }, + { + name: "basic install with credentials", + cmd: "install aeneas reqtest --namespace default --repo " + srv2.URL + " --username username --password password --pass-credentials", + golden: "output/install.txt", + }, // Install hiding secret values { name: "install chart hiding secret values", diff --git a/cmd/helm/pull_test.go b/cmd/helm/pull_test.go index 51cdfdfa4..4d86a5029 100644 --- a/cmd/helm/pull_test.go +++ b/cmd/helm/pull_test.go @@ -18,6 +18,8 @@ package main import ( "fmt" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" @@ -250,6 +252,115 @@ func TestPullCmd(t *testing.T) { } } +func TestPullWithCredentialsCmd(t *testing.T) { + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*") + if err != nil { + t.Fatal(err) + } + defer srv.Stop() + + srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if !ok || username != "username" || password != "password" { + t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password) + } + })) + + srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.FileServer(http.Dir(srv.Root())).ServeHTTP(w, r) + })) + defer srv2.Close() + + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + // all flags will get "-d outdir" appended. + tests := []struct { + name string + args string + existFile string + existDir string + wantError bool + wantErrorMsg string + expectFile string + expectDir bool + }{ + { + name: "Chart fetch using repo URL", + expectFile: "./signtest-0.1.0.tgz", + args: "signtest --repo " + srv.URL() + " --username username --password password", + }, + { + name: "Fail fetching non-existent chart on repo URL", + args: "someChart --repo " + srv.URL() + " --username username --password password", + wantError: true, + }, + { + name: "Specific version chart fetch using repo URL", + expectFile: "./signtest-0.1.0.tgz", + args: "signtest --version=0.1.0 --repo " + srv.URL() + " --username username --password password", + }, + { + name: "Specific version chart fetch using repo URL", + args: "signtest --version=0.2.0 --repo " + srv.URL() + " --username username --password password", + wantError: true, + }, + { + name: "Chart located on different domain with credentials passed", + args: "reqtest --repo " + srv2.URL + " --username username --password password --pass-credentials", + expectFile: "./reqtest-0.1.0.tgz", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + outdir := srv.Root() + cmd := fmt.Sprintf("pull %s -d '%s' --repository-config %s --repository-cache %s --registry-config %s", + tt.args, + outdir, + filepath.Join(outdir, "repositories.yaml"), + outdir, + filepath.Join(outdir, "config.json"), + ) + // Create file or Dir before helm pull --untar, see: https://github.com/helm/helm/issues/7182 + if tt.existFile != "" { + file := filepath.Join(outdir, tt.existFile) + _, err := os.Create(file) + if err != nil { + t.Fatal(err) + } + } + if tt.existDir != "" { + file := filepath.Join(outdir, tt.existDir) + err := os.Mkdir(file, 0755) + if err != nil { + t.Fatal(err) + } + } + _, _, err := executeActionCommand(cmd) + if err != nil { + if tt.wantError { + if tt.wantErrorMsg != "" && tt.wantErrorMsg == err.Error() { + t.Fatalf("Actual error %s, not equal to expected error %s", err, tt.wantErrorMsg) + } + return + } + t.Fatalf("%q reported error: %s", tt.name, err) + } + + ef := filepath.Join(outdir, tt.expectFile) + fi, err := os.Stat(ef) + if err != nil { + t.Errorf("%q: expected a file at %s. %s", tt.name, ef, err) + } + if fi.IsDir() != tt.expectDir { + t.Errorf("%q: expected directory=%t, but it's not.", tt.name, tt.expectDir) + } + }) + } +} + func TestPullVersionCompletion(t *testing.T) { repoFile := "testdata/helmhome/helm/repositories.yaml" repoCache := "testdata/helmhome/helm/repository" diff --git a/cmd/helm/repo_add.go b/cmd/helm/repo_add.go index 52cd020f5..beafb31cf 100644 --- a/cmd/helm/repo_add.go +++ b/cmd/helm/repo_add.go @@ -48,6 +48,7 @@ type repoAddOptions struct { url string username string password string + passCredentialsAll bool forceUpdate bool allowDeprecatedRepos bool @@ -91,6 +92,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command { f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository") f.BoolVar(&o.allowDeprecatedRepos, "allow-deprecated-repos", false, "by default, this command will not allow adding official repos that have been permanently deleted. This disables that behavior") + f.BoolVar(&o.passCredentialsAll, "pass-credentials", false, "pass credentials to all domains") return cmd } @@ -112,7 +114,14 @@ func (o *repoAddOptions) run(out io.Writer) error { } // Acquire a file lock for process synchronization - fileLock := flock.New(strings.Replace(o.repoFile, filepath.Ext(o.repoFile), ".lock", 1)) + repoFileExt := filepath.Ext(o.repoFile) + var lockPath string + if len(repoFileExt) > 0 && len(repoFileExt) < len(o.repoFile) { + lockPath = strings.Replace(o.repoFile, repoFileExt, ".lock", 1) + } else { + lockPath = o.repoFile + ".lock" + } + fileLock := flock.New(lockPath) lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() locked, err := fileLock.TryLockContext(lockCtx, time.Second) @@ -149,6 +158,7 @@ func (o *repoAddOptions) run(out io.Writer) error { URL: o.url, Username: o.username, Password: o.password, + PassCredentialsAll: o.passCredentialsAll, CertFile: o.certFile, KeyFile: o.keyFile, CAFile: o.caFile, diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go index f3bc54985..739173ee7 100644 --- a/cmd/helm/repo_add_test.go +++ b/cmd/helm/repo_add_test.go @@ -142,6 +142,18 @@ func TestRepoAddConcurrentDirNotExist(t *testing.T) { repoAddConcurrent(t, testName, repoFile) } +func TestRepoAddConcurrentNoFileExtension(t *testing.T) { + const testName = "test-name-3" + repoFile := filepath.Join(ensure.TempDir(t), "repositories") + repoAddConcurrent(t, testName, repoFile) +} + +func TestRepoAddConcurrentHiddenFile(t *testing.T) { + const testName = "test-name-4" + repoFile := filepath.Join(ensure.TempDir(t), ".repositories") + repoAddConcurrent(t, testName, repoFile) +} + func repoAddConcurrent(t *testing.T, testName, repoFile string) { ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") if err != nil { diff --git a/cmd/helm/testdata/output/upgrade-with-dependency-update.txt b/cmd/helm/testdata/output/upgrade-with-dependency-update.txt new file mode 100644 index 000000000..0e7e5842e --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-dependency-update.txt @@ -0,0 +1,9 @@ +Release "funny-bunny" has been upgraded. Happy Helming! +NAME: funny-bunny +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 3 +TEST SUITE: None +NOTES: +PARENT NOTES diff --git a/cmd/helm/testdata/output/version-client-shorthand.txt b/cmd/helm/testdata/output/version-client-shorthand.txt index 9dc0a8cfa..02e210fb7 100644 --- a/cmd/helm/testdata/output/version-client-shorthand.txt +++ b/cmd/helm/testdata/output/version-client-shorthand.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v3.5", GitCommit:"", GitTreeState:"", GoVersion:""} +version.BuildInfo{Version:"v3.6", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/cmd/helm/testdata/output/version-client.txt b/cmd/helm/testdata/output/version-client.txt index 9dc0a8cfa..02e210fb7 100644 --- a/cmd/helm/testdata/output/version-client.txt +++ b/cmd/helm/testdata/output/version-client.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v3.5", GitCommit:"", GitTreeState:"", GoVersion:""} +version.BuildInfo{Version:"v3.6", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/cmd/helm/testdata/output/version-short.txt b/cmd/helm/testdata/output/version-short.txt index 3c81e0c56..cde1fbc45 100644 --- a/cmd/helm/testdata/output/version-short.txt +++ b/cmd/helm/testdata/output/version-short.txt @@ -1 +1 @@ -v3.5 +v3.6 diff --git a/cmd/helm/testdata/output/version-template.txt b/cmd/helm/testdata/output/version-template.txt index 68945e7a4..84b18182b 100644 --- a/cmd/helm/testdata/output/version-template.txt +++ b/cmd/helm/testdata/output/version-template.txt @@ -1 +1 @@ -Version: v3.5 \ No newline at end of file +Version: v3.6 \ No newline at end of file diff --git a/cmd/helm/testdata/output/version.txt b/cmd/helm/testdata/output/version.txt index 9dc0a8cfa..02e210fb7 100644 --- a/cmd/helm/testdata/output/version.txt +++ b/cmd/helm/testdata/output/version.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v3.5", GitCommit:"", GitTreeState:"", GoVersion:""} +version.BuildInfo{Version:"v3.6", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.lock b/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.lock new file mode 100644 index 000000000..31cda6bd6 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: subchart-with-notes + repository: file://../chart-with-subchart-notes/charts/subchart-with-notes + version: 0.0.1 +digest: sha256:8ca45f73ae3f6170a09b64a967006e98e13cd91eb51e5ab0599bb87296c7df0a +generated: "2021-05-02T15:07:22.1099921+02:00" diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index c3961dfc5..c58a9175a 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -30,6 +30,7 @@ import ( "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/storage/driver" ) @@ -132,7 +133,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return err } - vals, err := valueOpts.MergeValues(getter.All(settings)) + p := getter.All(settings) + vals, err := valueOpts.MergeValues(p) if err != nil { return err } @@ -144,7 +146,27 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } if req := ch.Metadata.Dependencies; req != nil { if err := action.CheckDependencies(ch, req); err != nil { - return err + if client.DependencyUpdate { + man := &downloader.Manager{ + Out: out, + ChartPath: chartPath, + Keyring: client.ChartPathOptions.Keyring, + SkipUpdate: false, + Getters: p, + RepositoryConfig: settings.RepositoryConfig, + RepositoryCache: settings.RepositoryCache, + Debug: settings.Debug, + } + if err := man.Update(); err != nil { + return err + } + // Reload the chart with the updated Chart.lock file. + if ch, err = loader.Load(chartPath); err != nil { + return errors.Wrap(err, "failed reloading chart after repo update") + } + } else { + return err + } } } @@ -186,6 +208,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this upgrade when upgrade fails") f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent") f.StringVar(&client.Description, "description", "", "add a custom description") + f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart") addChartPathOptionsFlags(f, &client.ChartPathOptions) addValueOptionsFlags(f, valueOpts) bindOutputFlag(cmd, &outfmt) diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index e952a5933..fc2a22d7d 100644 --- a/cmd/helm/upgrade_test.go +++ b/cmd/helm/upgrade_test.go @@ -32,6 +32,7 @@ import ( ) func TestUpgradeCmd(t *testing.T) { + tmpChart := ensure.TempDir(t) cfile := &chart.Chart{ Metadata: &chart.Metadata{ @@ -79,6 +80,7 @@ func TestUpgradeCmd(t *testing.T) { missingDepsPath := "testdata/testcharts/chart-missing-deps" badDepsPath := "testdata/testcharts/chart-bad-requirements" + presentDepsPath := "testdata/testcharts/chart-with-subchart-update" relWithStatusMock := func(n string, v int, ch *chart.Chart, status release.Status) *release.Release { return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch, Status: status}) @@ -149,6 +151,12 @@ func TestUpgradeCmd(t *testing.T) { golden: "output/upgrade-with-bad-dependencies.txt", wantError: true, }, + { + name: "upgrade a release with resolving missing dependencies", + cmd: fmt.Sprintf("upgrade --dependency-update funny-bunny %s", presentDepsPath), + golden: "output/upgrade-with-dependency-update.txt", + rels: []*release.Release{relMock("funny-bunny", 2, ch2)}, + }, { name: "upgrade a non-existent release", cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), diff --git a/internal/experimental/registry/client.go b/internal/experimental/registry/client.go index c889ee913..26556533c 100644 --- a/internal/experimental/registry/client.go +++ b/internal/experimental/registry/client.go @@ -51,6 +51,7 @@ type ( authorizer *Authorizer resolver *Resolver cache *Cache + columnWidth uint } ) @@ -95,6 +96,10 @@ func NewClient(opts ...ClientOption) (*Client, error) { } client.cache = cache } + + if client.columnWidth == 0 { + client.columnWidth = 60 + } return client, nil } @@ -279,7 +284,7 @@ func (c *Client) RemoveChart(ref *Reference) error { // PrintChartTable prints a list of locally stored charts func (c *Client) PrintChartTable() error { table := uitable.New() - table.MaxColWidth = 60 + table.MaxColWidth = c.columnWidth table.AddRow("REF", "NAME", "VERSION", "DIGEST", "SIZE", "CREATED") rows, err := c.getChartTableRows() if err != nil { diff --git a/internal/experimental/registry/client_opts.go b/internal/experimental/registry/client_opts.go index e2f742aec..266cf3f6f 100644 --- a/internal/experimental/registry/client_opts.go +++ b/internal/experimental/registry/client_opts.go @@ -67,3 +67,10 @@ func ClientOptCredentialsFile(credentialsFile string) ClientOption { 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 + } +} diff --git a/internal/version/version.go b/internal/version/version.go index 15822e914..4db8c0549 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -29,7 +29,7 @@ var ( // // Increment major number for new feature additions and behavioral changes. // Increment minor number for bug fixes and performance enhancements. - version = "v3.5" + version = "v3.6" // metadata is extra build time data metadata = "" diff --git a/pkg/action/action.go b/pkg/action/action.go index 38ba638e4..f093ed7f8 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -102,11 +102,11 @@ type Configuration struct { // TODO: This function is badly in need of a refactor. // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed // This code has to do with writing files to disk. -func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun bool) ([]*release.Hook, *bytes.Buffer, string, error) { +func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun bool) ([]*release.Hook, *bytes.Buffer, string, error) { hs := []*release.Hook{} b := bytes.NewBuffer(nil) - caps, err := c.getCapabilities() + caps, err := cfg.getCapabilities() if err != nil { return hs, b, "", err } @@ -125,12 +125,12 @@ func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values // is mocked. It is not up to the template author to decide when the user wants to // connect to the cluster. So when the user says to dry run, respect the user's // wishes and do not connect to the cluster. - if !dryRun && c.RESTClientGetter != nil { - rest, err := c.RESTClientGetter.ToRESTConfig() + if !dryRun && cfg.RESTClientGetter != nil { + restConfig, err := cfg.RESTClientGetter.ToRESTConfig() if err != nil { return hs, b, "", err } - files, err2 = engine.RenderWithClient(ch, values, rest) + files, err2 = engine.RenderWithClient(ch, values, restConfig) } else { files, err2 = engine.Render(ch, values) } @@ -236,11 +236,11 @@ type RESTClientGetter interface { type DebugLog func(format string, v ...interface{}) // capabilities builds a Capabilities from discovery information. -func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) { - if c.Capabilities != nil { - return c.Capabilities, nil +func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) { + if cfg.Capabilities != nil { + return cfg.Capabilities, nil } - dc, err := c.RESTClientGetter.ToDiscoveryClient() + dc, err := cfg.RESTClientGetter.ToDiscoveryClient() if err != nil { return nil, errors.Wrap(err, "could not get Kubernetes discovery client") } @@ -258,14 +258,14 @@ func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) { apiVersions, err := GetVersionSet(dc) if err != nil { if discovery.IsGroupDiscoveryFailedError(err) { - c.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err) - c.Log("WARNING: To fix this, kubectl delete apiservice ") + cfg.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err) + cfg.Log("WARNING: To fix this, kubectl delete apiservice ") } else { return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes") } } - c.Capabilities = &chartutil.Capabilities{ + cfg.Capabilities = &chartutil.Capabilities{ APIVersions: apiVersions, KubeVersion: chartutil.KubeVersion{ Version: kubeVersion.GitVersion, @@ -273,12 +273,12 @@ func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) { Minor: kubeVersion.Minor, }, } - return c.Capabilities, nil + return cfg.Capabilities, nil } // KubernetesClientSet creates a new kubernetes ClientSet based on the configuration -func (c *Configuration) KubernetesClientSet() (kubernetes.Interface, error) { - conf, err := c.RESTClientGetter.ToRESTConfig() +func (cfg *Configuration) KubernetesClientSet() (kubernetes.Interface, error) { + conf, err := cfg.RESTClientGetter.ToRESTConfig() if err != nil { return nil, errors.Wrap(err, "unable to generate config for kubernetes client") } @@ -290,20 +290,20 @@ func (c *Configuration) KubernetesClientSet() (kubernetes.Interface, error) { // // If the configuration has a Timestamper on it, that will be used. // Otherwise, this will use time.Now(). -func (c *Configuration) Now() time.Time { +func (cfg *Configuration) Now() time.Time { return Timestamper() } -func (c *Configuration) releaseContent(name string, version int) (*release.Release, error) { +func (cfg *Configuration) releaseContent(name string, version int) (*release.Release, error) { if err := chartutil.ValidateReleaseName(name); err != nil { return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name) } if version <= 0 { - return c.Releases.Last(name) + return cfg.Releases.Last(name) } - return c.Releases.Get(name, version) + return cfg.Releases.Get(name, version) } // GetVersionSet retrieves a set of available k8s API versions @@ -355,14 +355,14 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.Version } // recordRelease with an update operation in case reuse has been set. -func (c *Configuration) recordRelease(r *release.Release) { - if err := c.Releases.Update(r); err != nil { - c.Log("warning: Failed to update release %s: %s", r.Name, err) +func (cfg *Configuration) recordRelease(r *release.Release) { + if err := cfg.Releases.Update(r); err != nil { + cfg.Log("warning: Failed to update release %s: %s", r.Name, err) } } // Init initializes the action configuration -func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error { +func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error { kc := kube.New(getter) kc.Log = log @@ -383,8 +383,8 @@ func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespac store = storage.Init(d) case "memory": var d *driver.Memory - if c.Releases != nil { - if mem, ok := c.Releases.Driver.(*driver.Memory); ok { + if cfg.Releases != nil { + if mem, ok := cfg.Releases.Driver.(*driver.Memory); ok { // This function can be called more than once (e.g., helm list --all-namespaces). // If a memory driver was already initialized, re-use it but set the possibly new namespace. // We re-use it in case some releases where already created in the existing memory driver. @@ -411,10 +411,10 @@ func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespac panic("Unknown driver in HELM_DRIVER: " + helmDriver) } - c.RESTClientGetter = getter - c.KubeClient = kc - c.Releases = store - c.Log = log + cfg.RESTClientGetter = getter + cfg.KubeClient = kc + cfg.Releases = store + cfg.Log = log return nil } diff --git a/pkg/action/chart_list.go b/pkg/action/chart_list.go index db764b3a3..ba49a887d 100644 --- a/pkg/action/chart_list.go +++ b/pkg/action/chart_list.go @@ -18,11 +18,14 @@ package action import ( "io" + + "helm.sh/helm/v3/internal/experimental/registry" ) // ChartList performs a chart list operation. type ChartList struct { - cfg *Configuration + cfg *Configuration + ColumnWidth uint } // NewChartList creates a new ChartList object with the given configuration. @@ -34,5 +37,8 @@ func NewChartList(cfg *Configuration) *ChartList { // Run executes the chart list operation func (a *ChartList) Run(out io.Writer) error { - return a.cfg.RegistryClient.PrintChartTable() + client := a.cfg.RegistryClient + opt := registry.ClientOptColumnWidth(a.ColumnWidth) + opt(client) + return client.PrintChartTable() } diff --git a/pkg/action/dependency.go b/pkg/action/dependency.go index 578a46aec..6a284d762 100644 --- a/pkg/action/dependency.go +++ b/pkg/action/dependency.go @@ -37,11 +37,14 @@ type Dependency struct { Verify bool Keyring string SkipRefresh bool + ColumnWidth uint } // NewDependency creates a new Dependency object with the given configuration. func NewDependency() *Dependency { - return &Dependency{} + return &Dependency{ + ColumnWidth: 80, + } } // List executes 'helm dependency list'. @@ -181,7 +184,7 @@ func statArchiveForStatus(archive string, dep *chart.Dependency) string { // printDependencies prints all of the dependencies in the yaml file. func (d *Dependency) printDependencies(chartpath string, out io.Writer, c *chart.Chart) { table := uitable.New() - table.MaxColWidth = 80 + table.MaxColWidth = d.ColumnWidth table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") for _, row := range c.Metadata.Dependencies { table.AddRow(row.Name, row.Version, row.Repository, d.dependencyStatus(chartpath, row, c)) diff --git a/pkg/action/install.go b/pkg/action/install.go index af99717d1..933747d26 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -20,6 +20,7 @@ import ( "bytes" "fmt" "io/ioutil" + "net/url" "os" "path" "path/filepath" @@ -113,6 +114,7 @@ type ChartPathOptions struct { InsecureSkipTLSverify bool // --insecure-skip-verify Keyring string // --keyring Password string // --password + PassCredentialsAll bool // --pass-credentials RepoURL string // --repo Username string // --username Verify bool // --verify @@ -225,7 +227,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. return nil, err } - //special case for helm template --is-upgrade + // special case for helm template --is-upgrade isUpgrade := i.IsUpgrade && i.DryRun options := chartutil.ReleaseOptions{ Name: i.ReleaseName, @@ -654,7 +656,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( Keyring: c.Keyring, Getters: getter.All(settings), Options: []getter.Option{ - getter.WithBasicAuth(c.Username, c.Password), + getter.WithPassCredentialsAll(c.PassCredentialsAll), getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile), getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify), }, @@ -665,12 +667,34 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( dl.Verify = downloader.VerifyAlways } if c.RepoURL != "" { - chartURL, err := repo.FindChartInAuthAndTLSRepoURL(c.RepoURL, c.Username, c.Password, name, version, - c.CertFile, c.KeyFile, c.CaFile, c.InsecureSkipTLSverify, getter.All(settings)) + chartURL, err := repo.FindChartInAuthAndTLSAndPassRepoURL(c.RepoURL, c.Username, c.Password, name, version, + c.CertFile, c.KeyFile, c.CaFile, c.InsecureSkipTLSverify, c.PassCredentialsAll, getter.All(settings)) if err != nil { return "", err } name = chartURL + + // Only pass the user/pass on when the user has said to or when the + // location of the chart repo and the chart are the same domain. + u1, err := url.Parse(c.RepoURL) + if err != nil { + return "", err + } + u2, err := url.Parse(chartURL) + if err != nil { + return "", err + } + + // Host on URL (returned from url.Parse) contains the port if present. + // This check ensures credentials are not passed between different + // services on different ports. + if c.PassCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) { + dl.Options = append(dl.Options, getter.WithBasicAuth(c.Username, c.Password)) + } else { + dl.Options = append(dl.Options, getter.WithBasicAuth("", "")) + } + } else { + dl.Options = append(dl.Options, getter.WithBasicAuth(c.Username, c.Password)) } if err := os.MkdirAll(settings.RepositoryCache, 0755); err != nil { diff --git a/pkg/action/pull.go b/pkg/action/pull.go index 04faa3b6b..fa1247054 100644 --- a/pkg/action/pull.go +++ b/pkg/action/pull.go @@ -82,6 +82,7 @@ func (p *Pull) Run(chartRef string) (string, error) { Getters: getter.All(p.Settings), Options: []getter.Option{ getter.WithBasicAuth(p.Username, p.Password), + getter.WithPassCredentialsAll(p.PassCredentialsAll), getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile), getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSverify), }, @@ -118,7 +119,7 @@ func (p *Pull) Run(chartRef string) (string, error) { } if p.RepoURL != "" { - chartURL, err := repo.FindChartInAuthAndTLSRepoURL(p.RepoURL, p.Username, p.Password, chartRef, p.Version, p.CertFile, p.KeyFile, p.CaFile, p.InsecureSkipTLSverify, getter.All(p.Settings)) + chartURL, err := repo.FindChartInAuthAndTLSAndPassRepoURL(p.RepoURL, p.Username, p.Password, chartRef, p.Version, p.CertFile, p.KeyFile, p.CaFile, p.InsecureSkipTLSverify, p.PassCredentialsAll, getter.All(p.Settings)) if err != nil { return out.String(), err } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 3b3dd3f1c..07d9cb40e 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -98,6 +98,8 @@ type Upgrade struct { PostRenderer postrender.PostRenderer // DisableOpenAPIValidation controls whether OpenAPI validation is enforced. DisableOpenAPIValidation bool + // Get missing dependencies + DependencyUpdate bool } // NewUpgrade creates a new Upgrade object with the given configuration. diff --git a/pkg/chartutil/capabilities_test.go b/pkg/chartutil/capabilities_test.go index a68d16b45..fad20f1dc 100644 --- a/pkg/chartutil/capabilities_test.go +++ b/pkg/chartutil/capabilities_test.go @@ -62,8 +62,8 @@ func TestDefaultCapabilities(t *testing.T) { func TestDefaultCapabilitiesHelmVersion(t *testing.T) { hv := DefaultCapabilities.HelmVersion - if hv.Version != "v3.5" { - t.Errorf("Expected default HelmVersion to be v3.5, got %q", hv.Version) + if hv.Version != "v3.6" { + t.Errorf("Expected default HelmVersion to be v3.6, got %q", hv.Version) } } diff --git a/pkg/chartutil/coalesce.go b/pkg/chartutil/coalesce.go index e086d8b6e..7b1ee1d80 100644 --- a/pkg/chartutil/coalesce.go +++ b/pkg/chartutil/coalesce.go @@ -127,9 +127,10 @@ func coalesceGlobals(dest, src map[string]interface{}) { // It's not clear if this condition can actually ever trigger. log.Printf("key %s is table. Skipping", key) continue + } else { + // TODO: Do we need to do any additional checking on the value? + dg[key] = val } - // TODO: Do we need to do any additional checking on the value? - dg[key] = val } dest[GlobalKey] = dg } diff --git a/pkg/chartutil/coalesce_test.go b/pkg/chartutil/coalesce_test.go index 5a4656d71..fc55e0a25 100644 --- a/pkg/chartutil/coalesce_test.go +++ b/pkg/chartutil/coalesce_test.go @@ -87,6 +87,9 @@ func TestCoalesceValues(t *testing.T) { &chart.Chart{ Metadata: &chart.Metadata{Name: "ahab"}, Values: map[string]interface{}{ + "global": map[string]interface{}{ + "nested": map[string]interface{}{"foo": "bar"}, + }, "scope": "ahab", "name": "ahab", "boat": true, @@ -135,9 +138,11 @@ func TestCoalesceValues(t *testing.T) { {"{{.pequod.ahab.scope}}", "whale"}, {"{{.pequod.ahab.nested.foo}}", "true"}, {"{{.pequod.ahab.global.name}}", "Ishmael"}, + {"{{.pequod.ahab.global.nested.foo}}", "bar"}, {"{{.pequod.ahab.global.subject}}", "Queequeg"}, {"{{.pequod.ahab.global.harpooner}}", "Tashtego"}, {"{{.pequod.global.name}}", "Ishmael"}, + {"{{.pequod.global.nested.foo}}", ""}, {"{{.pequod.global.subject}}", "Queequeg"}, {"{{.spouter.global.name}}", "Ishmael"}, {"{{.spouter.global.harpooner}}", ""}, diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 6c600bebb..2c0d55a55 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -195,6 +195,7 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er c.Options = append( c.Options, getter.WithBasicAuth(rc.Username, rc.Password), + getter.WithPassCredentialsAll(rc.PassCredentialsAll), ) } return u, nil @@ -224,7 +225,10 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er c.Options = append(c.Options, getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile)) } if r.Config.Username != "" && r.Config.Password != "" { - c.Options = append(c.Options, getter.WithBasicAuth(r.Config.Username, r.Config.Password)) + c.Options = append(c.Options, + getter.WithBasicAuth(r.Config.Username, r.Config.Password), + getter.WithPassCredentialsAll(r.Config.PassCredentialsAll), + ) } } diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go index 334d7aaa1..38a06671c 100644 --- a/pkg/downloader/chart_downloader_test.go +++ b/pkg/downloader/chart_downloader_test.go @@ -205,6 +205,7 @@ func TestDownloadTo(t *testing.T) { }), Options: []getter.Option{ getter.WithBasicAuth("username", "password"), + getter.WithPassCredentialsAll(false), }, } cname := "/signtest-0.1.0.tgz" diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index e89ac7c02..22db8bfdd 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -276,21 +276,21 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { chartPath := filepath.Join(tmpPath, dep.Name) ch, err := loader.LoadDir(chartPath) if err != nil { - return fmt.Errorf("Unable to load chart: %v", err) + return fmt.Errorf("unable to load chart: %v", err) } constraint, err := semver.NewConstraint(dep.Version) if err != nil { - return fmt.Errorf("Dependency %s has an invalid version/constraint format: %s", dep.Name, err) + return fmt.Errorf("dependency %s has an invalid version/constraint format: %s", dep.Name, err) } v, err := semver.NewVersion(ch.Metadata.Version) if err != nil { - return fmt.Errorf("Invalid version %s for dependency %s: %s", dep.Version, dep.Name, err) + return fmt.Errorf("invalid version %s for dependency %s: %s", dep.Version, dep.Name, err) } if !constraint.Check(v) { - saveError = fmt.Errorf("Dependency %s at version %s does not satisfy the constraint %s", dep.Name, ch.Metadata.Version, dep.Version) + saveError = fmt.Errorf("dependency %s at version %s does not satisfy the constraint %s", dep.Name, ch.Metadata.Version, dep.Version) break } continue @@ -310,7 +310,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { // Any failure to resolve/download a chart should fail: // https://github.com/helm/helm/issues/1439 - churl, username, password, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos) + churl, username, password, insecureskiptlsverify, passcredentialsall, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos) if err != nil { saveError = errors.Wrapf(err, "could not find %s", churl) break @@ -332,6 +332,8 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { Getters: m.Getters, Options: []getter.Option{ getter.WithBasicAuth(username, password), + getter.WithPassCredentialsAll(passcredentialsall), + getter.WithInsecureSkipVerifyTLS(insecureskiptlsverify), }, } @@ -685,9 +687,9 @@ func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { // repoURL is the repository to search // // If it finds a URL that is "relative", it will prepend the repoURL. -func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, err error) { +func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, insecureskiptlsverify, passcredentialsall bool, err error) { if strings.HasPrefix(repoURL, "oci://") { - return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", nil + return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", false, false, nil } for _, cr := range repos { @@ -709,15 +711,17 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]* } username = cr.Config.Username password = cr.Config.Password + passcredentialsall = cr.Config.PassCredentialsAll + insecureskiptlsverify = cr.Config.InsecureSkipTLSverify return } } url, err = repo.FindChartInRepoURL(repoURL, name, version, "", "", "", m.Getters) if err == nil { - return url, username, password, err + return url, username, password, false, false, err } err = errors.Errorf("chart %s not found in %s: %s", name, repoURL, err) - return url, username, password, err + return url, username, password, false, false, err } // findEntryByName finds an entry in the chart repository whose name matches the given name. diff --git a/pkg/downloader/manager_test.go b/pkg/downloader/manager_test.go index fc8d9abb2..0cc6d6f12 100644 --- a/pkg/downloader/manager_test.go +++ b/pkg/downloader/manager_test.go @@ -81,10 +81,11 @@ func TestFindChartURL(t *testing.T) { version := "0.1.0" repoURL := "http://example.com/charts" - churl, username, password, err := m.findChartURL(name, version, repoURL, repos) + churl, username, password, insecureSkipTLSVerify, passcredentialsall, err := m.findChartURL(name, version, repoURL, repos) if err != nil { t.Fatal(err) } + if churl != "https://charts.helm.sh/stable/alpine-0.1.0.tgz" { t.Errorf("Unexpected URL %q", churl) } @@ -94,6 +95,37 @@ func TestFindChartURL(t *testing.T) { if password != "" { t.Errorf("Unexpected password %q", password) } + if passcredentialsall != false { + t.Errorf("Unexpected passcredentialsall %t", passcredentialsall) + } + if insecureSkipTLSVerify { + t.Errorf("Unexpected insecureSkipTLSVerify %t", insecureSkipTLSVerify) + } + + name = "tlsfoo" + version = "1.2.3" + repoURL = "https://example-https-insecureskiptlsverify.com" + + churl, username, password, insecureSkipTLSVerify, passcredentialsall, err = m.findChartURL(name, version, repoURL, repos) + if err != nil { + t.Fatal(err) + } + + if !insecureSkipTLSVerify { + t.Errorf("Unexpected insecureSkipTLSVerify %t", insecureSkipTLSVerify) + } + if churl != "https://example.com/tlsfoo-1.2.3.tgz" { + t.Errorf("Unexpected URL %q", churl) + } + if username != "" { + t.Errorf("Unexpected username %q", username) + } + if password != "" { + t.Errorf("Unexpected password %q", password) + } + if passcredentialsall != false { + t.Errorf("Unexpected passcredentialsall %t", passcredentialsall) + } } func TestGetRepoNames(t *testing.T) { diff --git a/pkg/downloader/testdata/repositories.yaml b/pkg/downloader/testdata/repositories.yaml index 430865269..32bc395a0 100644 --- a/pkg/downloader/testdata/repositories.yaml +++ b/pkg/downloader/testdata/repositories.yaml @@ -21,3 +21,6 @@ repositories: certFile: "cert" keyFile: "key" caFile: "ca" + - name: testing-https-insecureskip-tls-verify + url: "https://example-https-insecureskiptlsverify.com" + insecure_skip_tls_verify: true diff --git a/pkg/downloader/testdata/repository/testing-https-insecureskip-tls-verify-index.yaml b/pkg/downloader/testdata/repository/testing-https-insecureskip-tls-verify-index.yaml new file mode 100644 index 000000000..11cfa629c --- /dev/null +++ b/pkg/downloader/testdata/repository/testing-https-insecureskip-tls-verify-index.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +entries: + tlsfoo: + - name: tlsfoo + description: TLS FOO Chart + home: https://helm.sh/helm + keywords: [] + maintainers: [] + sources: + - https://github.com/helm/charts + urls: + - https://example.com/tlsfoo-1.2.3.tgz + version: 1.2.3 + checksum: 0e6661f193211d7a5206918d42f5c2a9470b7373 diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 155d50a38..4da64f5cc 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -173,6 +173,16 @@ func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]render return val, nil } + // Override sprig fail function for linting and wrapping message + funcMap["fail"] = func(msg string) (string, error) { + if e.LintMode { + // Don't fail when linting + log.Printf("[INFO] Fail: %s", msg) + return "", nil + } + return "", errors.New(warnWrap(msg)) + } + // If we are not linting and have a cluster connection, provide a Kubernetes-backed // implementation. if !e.LintMode && e.config != nil { diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index d2da7a77a..72ee02626 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -286,6 +286,35 @@ func TestExecErrors(t *testing.T) { } } +func TestFailErrors(t *testing.T) { + vals := chartutil.Values{"Values": map[string]interface{}{}} + + failtpl := `All your base are belong to us{{ fail "This is an error" }}` + tplsFailed := map[string]renderable{ + "failtpl": {tpl: failtpl, vals: vals}, + } + _, err := new(Engine).render(tplsFailed) + if err == nil { + t.Fatalf("Expected failures while rendering: %s", err) + } + expected := `execution error at (failtpl:1:33): This is an error` + if err.Error() != expected { + t.Errorf("Expected '%s', got %q", expected, err.Error()) + } + + var e Engine + e.LintMode = true + out, err := e.render(tplsFailed) + if err != nil { + t.Fatal(err) + } + + expectStr := "All your base are belong to us" + if gotStr := out["failtpl"]; gotStr != expectStr { + t.Errorf("Expected %q, got %q (%v)", expectStr, gotStr, out) + } +} + func TestAllTemplates(t *testing.T) { ch1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "ch1"}, diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index cd3883c0f..d1bf1105a 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -63,7 +63,7 @@ func NewLookupFunction(config *rest.Config) lookupFunc { } return obj.UnstructuredContent(), nil } - //this will return a list + // this will return a list obj, err := client.List(context.Background(), metav1.ListOptions{}) if err != nil { if apierrors.IsNotFound(err) { @@ -112,7 +112,7 @@ func getAPIResourceForGVK(gvk schema.GroupVersionKind, config *rest.Config) (met return res, err } for _, resource := range resList.APIResources { - //if a resource contains a "/" it's referencing a subresource. we don't support suberesource for now. + // if a resource contains a "/" it's referencing a subresource. we don't support suberesource for now. if resource.Kind == gvk.Kind && !strings.Contains(resource.Name, "/") { res = resource res.Group = gvk.Group diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go index 465348456..78add728a 100644 --- a/pkg/getter/getter.go +++ b/pkg/getter/getter.go @@ -38,6 +38,7 @@ type options struct { insecureSkipVerifyTLS bool username string password string + passCredentialsAll bool userAgent string version string registryClient *registry.Client @@ -64,6 +65,12 @@ func WithBasicAuth(username, password string) Option { } } +func WithPassCredentialsAll(pass bool) Option { + return func(opts *options) { + opts.passCredentialsAll = pass + } +} + // WithUserAgent sets the request's User-Agent header to use the provided agent name. func WithUserAgent(userAgent string) Option { return func(opts *options) { diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index bd60629ae..94b64381c 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -20,6 +20,7 @@ import ( "crypto/tls" "io" "net/http" + "net/url" "github.com/pkg/errors" @@ -33,7 +34,7 @@ type HTTPGetter struct { opts options } -//Get performs a Get from repo.Getter and returns the body. +// Get performs a Get from repo.Getter and returns the body. func (g *HTTPGetter) Get(href string, options ...Option) (*bytes.Buffer, error) { for _, opt := range options { opt(&g.opts) @@ -56,8 +57,24 @@ func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) { req.Header.Set("User-Agent", g.opts.userAgent) } - if g.opts.username != "" && g.opts.password != "" { - req.SetBasicAuth(g.opts.username, g.opts.password) + // Before setting the basic auth credentials, make sure the URL associated + // with the basic auth is the one being fetched. + u1, err := url.Parse(g.opts.url) + if err != nil { + return buf, errors.Wrap(err, "Unable to parse getter URL") + } + u2, err := url.Parse(href) + if err != nil { + return buf, errors.Wrap(err, "Unable to parse URL getting from") + } + + // Host on URL (returned from url.Parse) contains the port if present. + // This check ensures credentials are not passed between different + // services on different ports. + if g.opts.passCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) { + if g.opts.username != "" && g.opts.password != "" { + req.SetBasicAuth(g.opts.username, g.opts.password) + } } client, err := g.httpClient() diff --git a/pkg/getter/httpgetter_test.go b/pkg/getter/httpgetter_test.go index ad97898cb..d823aec0d 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -54,6 +54,7 @@ func TestHTTPGetter(t *testing.T) { // Test with options g, err = NewHTTPGetter( WithBasicAuth("I", "Am"), + WithPassCredentialsAll(false), WithUserAgent("Groot"), WithTLSClientConfig(pub, priv, ca), WithInsecureSkipVerifyTLS(insecure), @@ -76,6 +77,10 @@ func TestHTTPGetter(t *testing.T) { t.Errorf("Expected NewHTTPGetter to contain %q as the password, got %q", "Am", hg.opts.password) } + if hg.opts.passCredentialsAll != false { + t.Errorf("Expected NewHTTPGetter to contain %t as PassCredentialsAll, got %t", false, hg.opts.passCredentialsAll) + } + if hg.opts.userAgent != "Groot" { t.Errorf("Expected NewHTTPGetter to contain %q as the user agent, got %q", "Groot", hg.opts.userAgent) } @@ -118,6 +123,28 @@ func TestHTTPGetter(t *testing.T) { if hg.opts.insecureSkipVerifyTLS != insecure { t.Errorf("Expected NewHTTPGetter to contain %t as InsecureSkipVerifyTLs flag, got %t", insecure, hg.opts.insecureSkipVerifyTLS) } + + // Checking false by default + if hg.opts.passCredentialsAll != false { + t.Errorf("Expected NewHTTPGetter to contain %t as PassCredentialsAll, got %t", false, hg.opts.passCredentialsAll) + } + + // Test setting PassCredentialsAll + g, err = NewHTTPGetter( + WithBasicAuth("I", "Am"), + WithPassCredentialsAll(true), + ) + if err != nil { + t.Fatal(err) + } + + hg, ok = g.(*HTTPGetter) + if !ok { + t.Fatal("expected NewHTTPGetter to produce an *HTTPGetter") + } + if hg.opts.passCredentialsAll != true { + t.Errorf("Expected NewHTTPGetter to contain %t as PassCredentialsAll, got %t", true, hg.opts.passCredentialsAll) + } } func TestDownload(t *testing.T) { @@ -163,6 +190,7 @@ func TestDownload(t *testing.T) { httpgetter, err := NewHTTPGetter( WithURL(u.String()), WithBasicAuth("username", "password"), + WithPassCredentialsAll(false), WithUserAgent(expectedUserAgent), ) if err != nil { @@ -176,6 +204,76 @@ func TestDownload(t *testing.T) { if got.String() != expect { t.Errorf("Expected %q, got %q", expect, got.String()) } + + // test with Get URL differing from withURL + crossAuthSrv := httptest.NewServer(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 not include but got '%v', '%s', '%s'", ok, username, password) + } + fmt.Fprint(w, expect) + })) + + defer crossAuthSrv.Close() + + u, _ = url.ParseRequestURI(crossAuthSrv.URL) + + // A different host is provided for the WithURL from the one used for Get + u2, _ := url.ParseRequestURI(crossAuthSrv.URL) + host := strings.Split(u2.Host, ":") + host[0] = host[0] + "a" + u2.Host = strings.Join(host, ":") + httpgetter, err = NewHTTPGetter( + WithURL(u2.String()), + WithBasicAuth("username", "password"), + WithPassCredentialsAll(false), + ) + if err != nil { + t.Fatal(err) + } + got, err = httpgetter.Get(u.String()) + if err != nil { + t.Fatal(err) + } + + if got.String() != expect { + t.Errorf("Expected %q, got %q", expect, got.String()) + } + + // test with Get URL differing from withURL and should pass creds + crossAuthSrv = httptest.NewServer(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) + } + fmt.Fprint(w, expect) + })) + + defer crossAuthSrv.Close() + + u, _ = url.ParseRequestURI(crossAuthSrv.URL) + + // A different host is provided for the WithURL from the one used for Get + u2, _ = url.ParseRequestURI(crossAuthSrv.URL) + host = strings.Split(u2.Host, ":") + host[0] = host[0] + "a" + u2.Host = strings.Join(host, ":") + httpgetter, err = NewHTTPGetter( + WithURL(u2.String()), + WithBasicAuth("username", "password"), + WithPassCredentialsAll(true), + ) + if err != nil { + t.Fatal(err) + } + got, err = httpgetter.Get(u.String()) + if err != nil { + t.Fatal(err) + } + + if got.String() != expect { + t.Errorf("Expected %q, got %q", expect, got.String()) + } } func TestDownloadTLS(t *testing.T) { diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index f70c997cf..daeab4cfc 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -113,7 +113,7 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1463 // Check that all the templates have a matching value - //linter.RunLinterRule(support.WarningSev, fpath, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate)) + // linter.RunLinterRule(support.WarningSev, fpath, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate)) // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1037 // linter.RunLinterRule(support.WarningSev, fpath, validateQuotes(string(preExecutedTemplate))) diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 9ead561b2..1399b7116 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -152,7 +152,7 @@ func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string, error) { parts = strings.Split(os.ExpandEnv(p.Metadata.Command), " ") } if len(parts) == 0 || parts[0] == "" { - return "", nil, fmt.Errorf("No plugin command is applicable") + return "", nil, fmt.Errorf("no plugin command is applicable") } main := parts[0] diff --git a/pkg/repo/chartrepo.go b/pkg/repo/chartrepo.go index 09b94fd42..67ede93fd 100644 --- a/pkg/repo/chartrepo.go +++ b/pkg/repo/chartrepo.go @@ -48,6 +48,7 @@ type Entry struct { KeyFile string `json:"keyFile"` CAFile string `json:"caFile"` InsecureSkipTLSverify bool `json:"insecure_skip_tls_verify"` + PassCredentialsAll bool `json:"pass_credentials_all"` } // ChartRepository represents a chart repository @@ -129,6 +130,7 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) { getter.WithInsecureSkipVerifyTLS(r.Config.InsecureSkipTLSverify), getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile), getter.WithBasicAuth(r.Config.Username, r.Config.Password), + getter.WithPassCredentialsAll(r.Config.PassCredentialsAll), ) if err != nil { return "", err @@ -217,6 +219,15 @@ func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion // but it also receives credentials and TLS verify flag for the chart repository. // TODO Helm 4, FindChartInAuthAndTLSRepoURL should be integrated into FindChartInAuthRepoURL. func FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, insecureSkipTLSverify bool, getters getter.Providers) (string, error) { + return FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile, false, false, getters) +} + +// FindChartInAuthAndTLSAndPassRepoURL finds chart in chart repository pointed by repoURL +// without adding repo to repositories, like FindChartInRepoURL, +// but it also receives credentials, TLS verify flag, and if credentials should +// be passed on to other domains. +// TODO Helm 4, FindChartInAuthAndTLSAndPassRepoURL should be integrated into FindChartInAuthRepoURL. +func FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, insecureSkipTLSverify, passCredentialsAll bool, getters getter.Providers) (string, error) { // Download and write the index file to a temporary location buf := make([]byte, 20) @@ -227,6 +238,7 @@ func FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartV URL: repoURL, Username: username, Password: password, + PassCredentialsAll: passCredentialsAll, CertFile: certFile, KeyFile: keyFile, CAFile: caFile, diff --git a/pkg/repo/chartrepo_test.go b/pkg/repo/chartrepo_test.go index 7bd563460..85401284e 100644 --- a/pkg/repo/chartrepo_test.go +++ b/pkg/repo/chartrepo_test.go @@ -292,14 +292,14 @@ func startLocalTLSServerForTests(handler http.Handler) (*httptest.Server, error) return httptest.NewTLSServer(handler), nil } -func TestFindChartInAuthAndTLSRepoURL(t *testing.T) { +func TestFindChartInAuthAndTLSAndPassRepoURL(t *testing.T) { srv, err := startLocalTLSServerForTests(nil) if err != nil { t.Fatal(err) } defer srv.Close() - chartURL, err := FindChartInAuthAndTLSRepoURL(srv.URL, "", "", "nginx", "", "", "", "", true, getter.All(&cli.EnvSettings{})) + chartURL, err := FindChartInAuthAndTLSAndPassRepoURL(srv.URL, "", "", "nginx", "", "", "", "", true, false, getter.All(&cli.EnvSettings{})) if err != nil { t.Fatalf("%v", err) } @@ -308,10 +308,10 @@ func TestFindChartInAuthAndTLSRepoURL(t *testing.T) { } // If the insecureSkipTLsverify is false, it will return an error that contains "x509: certificate signed by unknown authority". - _, err = FindChartInAuthAndTLSRepoURL(srv.URL, "", "", "nginx", "0.1.0", "", "", "", false, getter.All(&cli.EnvSettings{})) + _, err = FindChartInAuthAndTLSAndPassRepoURL(srv.URL, "", "", "nginx", "0.1.0", "", "", "", false, false, getter.All(&cli.EnvSettings{})) if !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { - t.Errorf("Expected TLS error for function FindChartInAuthAndTLSRepoURL not found, but got a different error (%v)", err) + t.Errorf("Expected TLS error for function FindChartInAuthAndTLSAndPassRepoURL not found, but got a different error (%v)", err) } } diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go index 13538aba0..c8a6ae04f 100644 --- a/pkg/storage/driver/sql.go +++ b/pkg/storage/driver/sql.go @@ -310,6 +310,10 @@ func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { return nil, err } + if len(records) == 0 { + return nil, ErrReleaseNotFound + } + var releases []*rspb.Release for _, record := range records { release, err := decodeRelease(record.Body) diff --git a/pkg/storage/driver/sql_test.go b/pkg/storage/driver/sql_test.go index 1562a90aa..87b6315b8 100644 --- a/pkg/storage/driver/sql_test.go +++ b/pkg/storage/driver/sql_test.go @@ -292,6 +292,11 @@ func TestSqlUpdate(t *testing.T) { func TestSqlQuery(t *testing.T) { // Reflect actual use cases in ../storage.go + labelSetUnknown := map[string]string{ + "name": "smug-pigeon", + "owner": sqlReleaseDefaultOwner, + "status": "unknown", + } labelSetDeployed := map[string]string{ "name": "smug-pigeon", "owner": sqlReleaseDefaultOwner, @@ -320,6 +325,15 @@ func TestSqlQuery(t *testing.T) { sqlReleaseTableNamespaceColumn, ) + mock. + ExpectQuery(regexp.QuoteMeta(query)). + WithArgs("smug-pigeon", sqlReleaseDefaultOwner, "unknown", "default"). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableBodyColumn, + }), + ).RowsWillBeClosed() + mock. ExpectQuery(regexp.QuoteMeta(query)). WithArgs("smug-pigeon", sqlReleaseDefaultOwner, "deployed", "default"). @@ -353,6 +367,13 @@ func TestSqlQuery(t *testing.T) { ), ).RowsWillBeClosed() + _, err := sqlDriver.Query(labelSetUnknown) + if err == nil { + t.Errorf("Expected error {%v}, got nil", ErrReleaseNotFound) + } else if err != ErrReleaseNotFound { + t.Fatalf("failed to query for unknown smug-pigeon release: %v", err) + } + results, err := sqlDriver.Query(labelSetDeployed) if err != nil { t.Fatalf("failed to query for deployed smug-pigeon release: %v", err) diff --git a/scripts/get b/scripts/get index 38c791223..8ad10459a 100755 --- a/scripts/get +++ b/scripts/get @@ -76,14 +76,8 @@ verifySupported() { # checkDesiredVersion checks if the desired version is available. checkDesiredVersion() { if [ "x$DESIRED_VERSION" == "x" ]; then - # Get tag from release URL - local release_url="https://github.com/helm/helm/releases" - if type "curl" > /dev/null; then - - TAG=$(curl -Ls $release_url | grep 'href="/helm/helm/releases/tag/v2.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') - elif type "wget" > /dev/null; then - TAG=$(wget $release_url -O - 2>&1 | grep 'href="/helm/helm/releases/tag/v2.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') - fi + # Pinning tag to v2.17.0 as per https://github.com/helm/helm/issues/9607 + TAG=v2.17.0 else TAG=$DESIRED_VERSION fi