diff --git a/cmd/helm/dependency.go b/cmd/helm/dependency.go index 228c73c80..19b648604 100644 --- a/cmd/helm/dependency.go +++ b/cmd/helm/dependency.go @@ -20,6 +20,7 @@ import ( "path/filepath" "github.com/spf13/cobra" + "github.com/spf13/pflag" "helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/pkg/action" @@ -93,7 +94,7 @@ func newDependencyCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cmd.AddCommand(newDependencyListCmd(out)) cmd.AddCommand(newDependencyUpdateCmd(cfg, out)) - cmd.AddCommand(newDependencyBuildCmd(cfg, out)) + cmd.AddCommand(newDependencyBuildCmd(out)) return cmd } @@ -120,3 +121,16 @@ func newDependencyListCmd(out io.Writer) *cobra.Command { f.UintVar(&client.ColumnWidth, "max-col-width", 80, "maximum column width for output table") return cmd } + +func addDependencySubcommandFlags(f *pflag.FlagSet, client *action.Dependency) { + f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") + f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + f.StringVar(&client.Username, "username", "", "chart repository username where to locate the requested chart") + f.StringVar(&client.Password, "password", "", "chart repository password where to locate the requested chart") + f.StringVar(&client.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&client.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.BoolVar(&client.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download") + f.BoolVar(&client.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download") + f.StringVar(&client.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") +} diff --git a/cmd/helm/dependency_build.go b/cmd/helm/dependency_build.go index 2cf0c6c81..5b5598498 100644 --- a/cmd/helm/dependency_build.go +++ b/cmd/helm/dependency_build.go @@ -41,7 +41,7 @@ If no lock file is found, 'helm dependency build' will mirror the behavior of 'helm dependency update'. ` -func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { +func newDependencyBuildCmd(out io.Writer) *cobra.Command { client := action.NewDependency() cmd := &cobra.Command{ @@ -54,13 +54,19 @@ func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Comm if len(args) > 0 { chartpath = filepath.Clean(args[0]) } + registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) + } + man := &downloader.Manager{ Out: out, ChartPath: chartpath, Keyring: client.Keyring, SkipUpdate: client.SkipRefresh, Getters: getter.All(settings), - RegistryClient: cfg.RegistryClient, + RegistryClient: registryClient, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, Debug: settings.Debug, @@ -68,7 +74,7 @@ func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Comm if client.Verify { man.Verify = downloader.VerifyIfPossible } - err := man.Build() + err = man.Build() if e, ok := err.(downloader.ErrRepoNotFound); ok { return fmt.Errorf("%s. Please add the missing repos via 'helm repo add'", e.Error()) } @@ -77,9 +83,7 @@ func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Comm } f := cmd.Flags() - f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") - f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") - f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + addDependencySubcommandFlags(f, client) return cmd } diff --git a/cmd/helm/dependency_update.go b/cmd/helm/dependency_update.go index cb6e9c0cc..3ac39adff 100644 --- a/cmd/helm/dependency_update.go +++ b/cmd/helm/dependency_update.go @@ -16,6 +16,7 @@ limitations under the License. package main import ( + "fmt" "io" "path/filepath" @@ -43,7 +44,7 @@ in the Chart.yaml file, but (b) at the wrong version. ` // newDependencyUpdateCmd creates a new dependency update command. -func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { +func newDependencyUpdateCmd(_ *action.Configuration, out io.Writer) *cobra.Command { client := action.NewDependency() cmd := &cobra.Command{ @@ -57,13 +58,19 @@ func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Com if len(args) > 0 { chartpath = filepath.Clean(args[0]) } + registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) + } + man := &downloader.Manager{ Out: out, ChartPath: chartpath, Keyring: client.Keyring, SkipUpdate: client.SkipRefresh, Getters: getter.All(settings), - RegistryClient: cfg.RegistryClient, + RegistryClient: registryClient, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, Debug: settings.Debug, @@ -76,9 +83,7 @@ func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Com } f := cmd.Flags() - f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") - f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") - f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + addDependencySubcommandFlags(f, client) return cmd } diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 23ff29d95..15ae6952f 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -141,7 +141,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { }, RunE: func(_ *cobra.Command, args []string) error { registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, - client.InsecureSkipTLSverify, client.PlainHTTP) + client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) } diff --git a/cmd/helm/package.go b/cmd/helm/package.go index b96110ee8..19ab3dc7f 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -47,7 +47,7 @@ If '--keyring' is not specified, Helm usually defaults to the public keyring unless your environment is otherwise configured. ` -func newPackageCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { +func newPackageCmd(out io.Writer) *cobra.Command { client := action.NewPackage() valueOpts := &values.Options{} @@ -75,6 +75,12 @@ func newPackageCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return err } + registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) + } + for i := 0; i < len(args); i++ { path, err := filepath.Abs(args[i]) if err != nil { @@ -91,7 +97,7 @@ func newPackageCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Keyring: client.Keyring, Getters: p, Debug: settings.Debug, - RegistryClient: cfg.RegistryClient, + RegistryClient: registryClient, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, } @@ -119,6 +125,13 @@ func newPackageCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.StringVar(&client.AppVersion, "app-version", "", "set the appVersion on the chart to this version") f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.") f.BoolVarP(&client.DependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`) + f.StringVar(&client.Username, "username", "", "chart repository username where to locate the requested chart") + f.StringVar(&client.Password, "password", "", "chart repository password where to locate the requested chart") + f.StringVar(&client.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&client.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.BoolVar(&client.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download") + f.BoolVar(&client.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download") + f.StringVar(&client.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") return cmd } diff --git a/cmd/helm/pull.go b/cmd/helm/pull.go index e0dd1effe..de4918d72 100644 --- a/cmd/helm/pull.go +++ b/cmd/helm/pull.go @@ -65,7 +65,7 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, - client.InsecureSkipTLSverify, client.PlainHTTP) + client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) } diff --git a/cmd/helm/push.go b/cmd/helm/push.go index 6ec26441d..9a67fa086 100644 --- a/cmd/helm/push.go +++ b/cmd/helm/push.go @@ -40,6 +40,8 @@ type registryPushOptions struct { caFile string insecureSkipTLSverify bool plainHTTP bool + password string + username string } func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { @@ -68,7 +70,10 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return noMoreArgsComp() }, RunE: func(_ *cobra.Command, args []string) error { - registryClient, err := newRegistryClient(o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSverify, o.plainHTTP) + registryClient, err := newRegistryClient( + o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSverify, o.plainHTTP, o.username, o.password, + ) + if err != nil { return fmt.Errorf("missing registry client: %w", err) } @@ -96,6 +101,8 @@ func newPushCmd(cfg *action.Configuration, 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 chart upload") f.BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the chart upload") + f.StringVar(&o.username, "username", "", "chart repository username where to locate the requested chart") + f.StringVar(&o.password, "password", "", "chart repository password where to locate the requested chart") return cmd } diff --git a/cmd/helm/root.go b/cmd/helm/root.go index 57738b111..2ba8a882e 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "log" + "net/http" "os" "strings" @@ -29,6 +30,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/clientcmd" + "helm.sh/helm/v3/internal/tlsutil" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/repo" @@ -153,7 +155,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string flags.ParseErrorsWhitelist.UnknownFlags = true flags.Parse(args) - registryClient, err := newDefaultRegistryClient(false) + registryClient, err := newDefaultRegistryClient(false, "", "") if err != nil { return nil, err } @@ -167,7 +169,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string newPullCmd(actionConfig, out), newShowCmd(actionConfig, out), newLintCmd(out), - newPackageCmd(actionConfig, out), + newPackageCmd(out), newRepoCmd(out), newSearchCmd(out), newVerifyCmd(out), @@ -255,27 +257,30 @@ func checkForExpiredRepos(repofile string) { } -func newRegistryClient(certFile, keyFile, caFile string, insecureSkipTLSverify, plainHTTP bool) (*registry.Client, error) { +func newRegistryClient( + certFile, keyFile, caFile string, insecureSkipTLSverify, plainHTTP bool, username, password string, +) (*registry.Client, error) { if certFile != "" && keyFile != "" || caFile != "" || insecureSkipTLSverify { - registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSverify) + registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSverify, username, password) if err != nil { return nil, err } return registryClient, nil } - registryClient, err := newDefaultRegistryClient(plainHTTP) + registryClient, err := newDefaultRegistryClient(plainHTTP, username, password) if err != nil { return nil, err } return registryClient, nil } -func newDefaultRegistryClient(plainHTTP bool) (*registry.Client, error) { +func newDefaultRegistryClient(plainHTTP bool, username, password string) (*registry.Client, error) { opts := []registry.ClientOption{ registry.ClientOptDebug(settings.Debug), registry.ClientOptEnableCache(true), registry.ClientOptWriter(os.Stderr), registry.ClientOptCredentialsFile(settings.RegistryConfig), + registry.ClientOptBasicAuth(username, password), } if plainHTTP { opts = append(opts, registry.ClientOptPlainHTTP()) @@ -289,10 +294,26 @@ func newDefaultRegistryClient(plainHTTP bool) (*registry.Client, error) { return registryClient, nil } -func newRegistryClientWithTLS(certFile, keyFile, caFile string, insecureSkipTLSverify bool) (*registry.Client, error) { +func newRegistryClientWithTLS( + certFile, keyFile, caFile string, insecureSkipTLSverify bool, username, password string, +) (*registry.Client, error) { + tlsConf, err := tlsutil.NewClientTLS(certFile, keyFile, caFile, insecureSkipTLSverify) + if err != nil { + return nil, fmt.Errorf("can't create TLS config for client: %w", err) + } + // Create a new registry client - registryClient, err := registry.NewRegistryClientWithTLS(os.Stderr, certFile, keyFile, caFile, insecureSkipTLSverify, - settings.RegistryConfig, settings.Debug, + registryClient, err := registry.NewClient( + registry.ClientOptDebug(settings.Debug), + registry.ClientOptEnableCache(true), + registry.ClientOptWriter(os.Stderr), + registry.ClientOptCredentialsFile(settings.RegistryConfig), + registry.ClientOptHTTPClient(&http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConf, + }, + }), + registry.ClientOptBasicAuth(username, password), ) if err != nil { return nil, err diff --git a/cmd/helm/show.go b/cmd/helm/show.go index 7c06a2297..b850af099 100644 --- a/cmd/helm/show.go +++ b/cmd/helm/show.go @@ -226,7 +226,7 @@ func runShow(args []string, client *action.Show) (string, error) { func addRegistryClient(client *action.Show) error { registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, - client.InsecureSkipTLSverify, client.PlainHTTP) + client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) } diff --git a/cmd/helm/template.go b/cmd/helm/template.go index b53ed6b1c..ff6621a49 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -74,7 +74,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, - client.InsecureSkipTLSverify, client.PlainHTTP) + client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) } diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 108550cbf..1585a78da 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -103,7 +103,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.Namespace = settings.Namespace() registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, - client.InsecureSkipTLSverify, client.PlainHTTP) + client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) } diff --git a/pkg/action/dependency.go b/pkg/action/dependency.go index 3265f1f17..19305fee8 100644 --- a/pkg/action/dependency.go +++ b/pkg/action/dependency.go @@ -34,10 +34,17 @@ import ( // // It provides the implementation of 'helm dependency' and its respective subcommands. type Dependency struct { - Verify bool - Keyring string - SkipRefresh bool - ColumnWidth uint + Verify bool + Keyring string + SkipRefresh bool + ColumnWidth uint + Username string + Password string + CertFile string + KeyFile string + CaFile string + InsecureSkipTLSverify bool + PlainHTTP bool } // NewDependency creates a new Dependency object with the given configuration. diff --git a/pkg/action/install.go b/pkg/action/install.go index 7ca40c88a..ae732f4fd 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -770,6 +770,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile), getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify), getter.WithPlainHTTP(c.PlainHTTP), + getter.WithBasicAuth(c.Username, c.Password), }, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, diff --git a/pkg/action/package.go b/pkg/action/package.go index 013b32f55..2357e3882 100644 --- a/pkg/action/package.go +++ b/pkg/action/package.go @@ -44,8 +44,15 @@ type Package struct { Destination string DependencyUpdate bool - RepositoryConfig string - RepositoryCache string + RepositoryConfig string + RepositoryCache string + PlainHTTP bool + Username string + Password string + CertFile string + KeyFile string + CaFile string + InsecureSkipTLSverify bool } // NewPackage creates a new Package object with the given configuration. diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 42f736816..9e58e1c16 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -18,6 +18,7 @@ package registry // import "helm.sh/helm/v3/pkg/registry" import ( "context" + "encoding/base64" "encoding/json" "fmt" "io" @@ -56,6 +57,8 @@ type ( enableCache bool // path to repository config file e.g. ~/.docker/config.json credentialsFile string + username string + password string out io.Writer authorizer auth.Client registryAuthorizer *registryauth.Client @@ -105,6 +108,19 @@ func NewClient(options ...ClientOption) (*Client, error) { if client.plainHTTP { opts = append(opts, auth.WithResolverPlainHTTP()) } + + // if username and password are set, use them for authentication + // by adding the basic auth Authorization header to the resolver + if client.username != "" && client.password != "" { + concat := client.username + ":" + client.password + encodedAuth := base64.StdEncoding.EncodeToString([]byte(concat)) + opts = append(opts, auth.WithResolverHeaders( + http.Header{ + "Authorization": []string{"Basic " + encodedAuth}, + }, + )) + } + resolver, err := client.authorizer.ResolverWithOpts(opts...) if err != nil { return nil, err @@ -125,6 +141,13 @@ func NewClient(options ...ClientOption) (*Client, error) { }, Cache: cache, Credential: func(_ context.Context, reg string) (registryauth.Credential, error) { + if client.username != "" && client.password != "" { + return registryauth.Credential{ + Username: client.username, + Password: client.password, + }, nil + } + dockerClient, ok := client.authorizer.(*dockerauth.Client) if !ok { return registryauth.EmptyCredential, errors.New("unable to obtain docker client") @@ -168,6 +191,14 @@ func ClientOptEnableCache(enableCache bool) ClientOption { } } +// ClientOptBasicAuth returns a function that sets the username and password setting on client options set +func ClientOptBasicAuth(username, password string) ClientOption { + return func(client *Client) { + client.username = username + client.password = password + } +} + // ClientOptWriter returns a function that sets the writer setting on client options set func ClientOptWriter(out io.Writer) ClientOption { return func(client *Client) { diff --git a/pkg/registry/utils_test.go b/pkg/registry/utils_test.go index d7aba2bb7..ee78ea76f 100644 --- a/pkg/registry/utils_test.go +++ b/pkg/registry/utils_test.go @@ -89,6 +89,7 @@ func setup(suite *TestSuite, tlsEnabled, insecure bool) *registry.Registry { ClientOptWriter(suite.Out), ClientOptCredentialsFile(credentialsFile), ClientOptResolver(nil), + ClientOptBasicAuth(testUsername, testPassword), } if tlsEnabled { @@ -128,11 +129,12 @@ func setup(suite *TestSuite, tlsEnabled, insecure bool) *registry.Registry { // This is required because Docker enforces HTTP if the registry // host is localhost/127.0.0.1. suite.DockerRegistryHost = fmt.Sprintf("helm-test-registry:%d", port) - suite.srv, _ = mockdns.NewServer(map[string]mockdns.Zone{ + suite.srv, err = mockdns.NewServer(map[string]mockdns.Zone{ "helm-test-registry.": { A: []string{"127.0.0.1"}, }, }, false) + suite.Nil(err, "no error creating mock DNS server") suite.srv.PatchNet(net.DefaultResolver) config.HTTP.Addr = fmt.Sprintf(":%d", port) @@ -349,7 +351,7 @@ func testPull(suite *TestSuite) { // full pull with chart and prov result, err := suite.RegistryClient.Pull(ref, PullOptWithProv(true)) - suite.Nil(err, "no error pulling a chart with prov") + suite.Require().Nil(err, "no error pulling a chart with prov") // Validate the output // Note: these digests/sizes etc may change if the test chart/prov files are modified,