diff --git a/cmd/helm/chart_pull.go b/cmd/helm/chart_pull.go index 5c18dc3e3..d923a9921 100644 --- a/cmd/helm/chart_pull.go +++ b/cmd/helm/chart_pull.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/internal/experimental/registry" "helm.sh/helm/v3/pkg/action" ) @@ -33,6 +34,7 @@ This will store the chart in the local registry cache to be used later. func newChartPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { var insecureOpt, plainHTTPOpt bool + var caFile, certFile, keyFile string cmd := &cobra.Command{ Use: "pull [ref]", Short: "pull a chart from remote", @@ -41,13 +43,30 @@ func newChartPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Hidden: !FeatureGateOCI.IsEnabled(), RunE: func(cmd *cobra.Command, args []string) error { ref := args[0] - return action.NewChartPull(cfg).Run(out, ref, insecureOpt, plainHTTPOpt) + registryClient, err := registry.NewClient( + registry.ClientOptDebug(settings.Debug), + registry.ClientOptWriter(out), + registry.ClientOptCredentialsFile(settings.RegistryConfig), + registry.ClientOptPlainHTTP(plainHTTPOpt), + registry.ClientOptInsecureSkipVerifyTLS(insecureOpt), + registry.ClientOptCAFile(caFile), + registry.ClientOptCertKeyFiles(certFile, keyFile), + ) + if err != nil { + return err + } + + cfg.RegistryClient = registryClient + return action.NewChartPull(cfg).Run(out, ref) }, } f := cmd.Flags() - f.BoolVarP(&insecureOpt, "insecure", "", false, "allow connections to TLS registry without certs") - f.BoolVarP(&plainHTTPOpt, "plain-http", "", false, "use plain http and not https") + f.BoolVarP(&insecureOpt, "insecure-skip-tls-verify", "", false, "skip registry tls certificate checks") + f.BoolVarP(&plainHTTPOpt, "plain-http", "", false, "use plain http to connect to the registry instead of https") + f.StringVar(&certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&keyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.StringVar(&caFile, "ca-file", "", "verify certificates of HTTPS-enabled registry using this CA bundle") return cmd } diff --git a/cmd/helm/chart_push.go b/cmd/helm/chart_push.go index 9f70b579d..925c7b1c7 100644 --- a/cmd/helm/chart_push.go +++ b/cmd/helm/chart_push.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/internal/experimental/registry" "helm.sh/helm/v3/pkg/action" ) @@ -35,6 +36,7 @@ Must first run "helm chart save" or "helm chart pull". func newChartPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { var insecureOpt, plainHTTPOpt bool + var caFile, certFile, keyFile string cmd := &cobra.Command{ Use: "push [ref]", Short: "push a chart to remote", @@ -43,13 +45,29 @@ func newChartPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Hidden: !FeatureGateOCI.IsEnabled(), RunE: func(cmd *cobra.Command, args []string) error { ref := args[0] - return action.NewChartPush(cfg).Run(out, ref, insecureOpt, plainHTTPOpt) + registryClient, err := registry.NewClient( + registry.ClientOptDebug(settings.Debug), + registry.ClientOptWriter(out), + registry.ClientOptCredentialsFile(settings.RegistryConfig), + registry.ClientOptPlainHTTP(plainHTTPOpt), + registry.ClientOptInsecureSkipVerifyTLS(insecureOpt), + registry.ClientOptCAFile(caFile), + registry.ClientOptCertKeyFiles(certFile, keyFile), + ) + if err != nil { + return err + } + + cfg.RegistryClient = registryClient + return action.NewChartPush(cfg).Run(out, ref) }, } f := cmd.Flags() - f.BoolVarP(&insecureOpt, "insecure", "", false, "allow connections to TLS registry without certs") - f.BoolVarP(&plainHTTPOpt, "plain-http", "", false, "use plain http and not https") - + f.BoolVarP(&insecureOpt, "insecure-skip-tls-verify", "", false, "skip registry tls certificate checks") + f.BoolVarP(&plainHTTPOpt, "plain-http", "", false, "use plain http to connect to the registry instead of https") + f.StringVar(&certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&keyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.StringVar(&caFile, "ca-file", "", "verify certificates of HTTPS-enabled registry using this CA bundle") return cmd } diff --git a/internal/experimental/registry/client.go b/internal/experimental/registry/client.go index e2dbf6ddf..95d1d55e8 100644 --- a/internal/experimental/registry/client.go +++ b/internal/experimental/registry/client.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "crypto/tls" + "crypto/x509" "fmt" "io" "io/ioutil" @@ -53,6 +54,13 @@ type ( authorizer *Authorizer resolver *Resolver cache *Cache + + // registry setting + certFile string + keyFile string + caFile string + insecureSkipVerifyTLS bool + plainHTTP bool } ) @@ -78,7 +86,7 @@ func NewClient(opts ...ClientOption) (*Client, error) { } } if client.resolver == nil { - resolver, err := client.newResolver(false, false) + resolver, err := client.newResolver() if err != nil { return nil, err } @@ -121,17 +129,7 @@ func (c *Client) Logout(hostname string) error { } // PushChart uploads a chart to a registry -func (c *Client) PushChart(ref *Reference, insecure bool, plainHTTP bool) error { - if insecure || plainHTTP { - resolver, err := c.newResolver(insecure, plainHTTP) - if err != nil { - return err - } - c.resolver = &Resolver{ - Resolver: resolver, - } - } - +func (c *Client) PushChart(ref *Reference) error { r, err := c.cache.FetchReference(ref) if err != nil { return err @@ -211,17 +209,7 @@ func (c *Client) PullChart(ref *Reference) (*bytes.Buffer, error) { // 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, insecure bool, plainHTTP bool) error { - if insecure || plainHTTP { - resolver, err := c.newResolver(insecure, plainHTTP) - if err != nil { - return err - } - c.resolver = &Resolver{ - Resolver: resolver, - } - } - +func (c *Client) PullChartToCache(ref *Reference) error { if ref.Tag == "" { return errors.New("tag explicitly required") } @@ -357,16 +345,40 @@ func (c *Client) getChartTableRows() ([][]interface{}, error) { return rows, nil } -func (c *Client) newResolver(insecure bool, plainHTTP bool) (resolver remotes.Resolver, err error) { - client := http.DefaultClient - if insecure { - client.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, +func (c *Client) newResolver() (resolver remotes.Resolver, err error) { + config := &tls.Config{} + + if c.caFile != "" { + caCert, err := ioutil.ReadFile(c.caFile) + if err != nil { + return nil, err } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + config.RootCAs = caCertPool } - resolver, err = c.authorizer.Resolver(context.Background(), client, plainHTTP) - return + if c.certFile != "" && c.keyFile != "" { + cert, err := tls.LoadX509KeyPair(c.certFile, c.keyFile) + if err != nil { + return nil, err + } + + config.Certificates = []tls.Certificate{cert} + } + + if c.insecureSkipVerifyTLS { + config.InsecureSkipVerify = true + } + + return c.authorizer.Resolver( + context.Background(), + &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: config, + }, + }, + c.plainHTTP, + ) } diff --git a/internal/experimental/registry/client_opts.go b/internal/experimental/registry/client_opts.go index e2f742aec..c1573ee05 100644 --- a/internal/experimental/registry/client_opts.go +++ b/internal/experimental/registry/client_opts.go @@ -61,9 +61,38 @@ func ClientOptCache(cache *Cache) ClientOption { } } -// ClientOptCredentialsFile returns a function that sets the cache setting on a client options set +// ClientOptCredentialsFile returns a function that sets the credentials file setting on a client options set func ClientOptCredentialsFile(credentialsFile string) ClientOption { return func(client *Client) { client.credentialsFile = credentialsFile } } + +// ClientOptCaFile returns a function that sets the CA file setting on a client options set +func ClientOptCAFile(caFile string) ClientOption { + return func(client *Client) { + client.caFile = caFile + } +} + +// ClientOptCaFile returns a function that sets the cert/key file setting on a client options set +func ClientOptCertKeyFiles(certFile, keyFile string) ClientOption { + return func(client *Client) { + client.certFile = certFile + client.keyFile = keyFile + } +} + +// ClientOptCaFile returns a function that sets the insecure setting on a client options set +func ClientOptInsecureSkipVerifyTLS(insecureSkipVerifyTLS bool) ClientOption { + return func(client *Client) { + client.insecureSkipVerifyTLS = insecureSkipVerifyTLS + } +} + +// ClientOptCaFile returns a function that sets the plain http setting on a client options set +func ClientOptPlainHTTP(plainHTTP bool) ClientOption { + return func(client *Client) { + client.plainHTTP = plainHTTP + } +} diff --git a/internal/experimental/registry/client_test.go b/internal/experimental/registry/client_test.go index 96aa2edc5..a9936ba13 100644 --- a/internal/experimental/registry/client_test.go +++ b/internal/experimental/registry/client_test.go @@ -186,13 +186,13 @@ func (suite *RegistryClientTestSuite) Test_3_PushChart() { // non-existent ref ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) suite.Nil(err) - err = suite.RegistryClient.PushChart(ref, false, false) + err = suite.RegistryClient.PushChart(ref) suite.NotNil(err) // existing ref ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) suite.Nil(err) - err = suite.RegistryClient.PushChart(ref, false, false) + err = suite.RegistryClient.PushChart(ref) suite.Nil(err) } diff --git a/pkg/action/chart_pull.go b/pkg/action/chart_pull.go index a446c49d2..896755201 100644 --- a/pkg/action/chart_pull.go +++ b/pkg/action/chart_pull.go @@ -35,10 +35,10 @@ func NewChartPull(cfg *Configuration) *ChartPull { } // Run executes the chart pull operation -func (a *ChartPull) Run(out io.Writer, ref string, insecure bool, plainHTTP bool) error { +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, insecure, plainHTTP) + return a.cfg.RegistryClient.PullChartToCache(r) } diff --git a/pkg/action/chart_push.go b/pkg/action/chart_push.go index 707d6a59b..91ec49d38 100644 --- a/pkg/action/chart_push.go +++ b/pkg/action/chart_push.go @@ -35,10 +35,10 @@ func NewChartPush(cfg *Configuration) *ChartPush { } // Run executes the chart push operation -func (a *ChartPush) Run(out io.Writer, ref string, insecure bool, plainHTTP bool) error { +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, insecure, plainHTTP) + return a.cfg.RegistryClient.PushChart(r) } diff --git a/pkg/action/install.go b/pkg/action/install.go index 4de0b64e6..c55e72adc 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -656,6 +656,15 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, } + + if strings.HasPrefix(name, "oci://") { + if version == "" { + return "", errors.Errorf("--version flag is explicitly required for OCI registries") + } + + dl.Options = append(dl.Options, getter.WithTagName(version)) + } + if c.Verify { dl.Verify = downloader.VerifyAlways } diff --git a/pkg/getter/ocigetter.go b/pkg/getter/ocigetter.go index 3f85b9862..4501ce380 100644 --- a/pkg/getter/ocigetter.go +++ b/pkg/getter/ocigetter.go @@ -49,6 +49,28 @@ func (g *OCIGetter) get(href string) (*bytes.Buffer, error) { return nil, err } + // In case any TLS setting is set, recreate the registry client + opts := []registry.ClientOption{} + + if g.opts.caFile != "" { + opts = append(opts, registry.ClientOptCAFile(g.opts.caFile)) + } + + if g.opts.certFile != "" && g.opts.keyFile != "" { + opts = append(opts, registry.ClientOptCertKeyFiles(g.opts.certFile, g.opts.keyFile)) + } + + if g.opts.insecureSkipVerifyTLS { + opts = append(opts, registry.ClientOptInsecureSkipVerifyTLS(g.opts.insecureSkipVerifyTLS)) + } + + if len(opts) > 0 { + client, err = registry.NewClient(opts...) + if err != nil { + return nil, err + } + } + buf, err := client.PullChart(r) if err != nil { return nil, err