From 04e772d801ce18eae6327dd649c5e390c24bacdf Mon Sep 17 00:00:00 2001 From: subinthomas1234 Date: Mon, 25 Jul 2022 16:18:40 +0530 Subject: [PATCH] What this PR does / why we need it: Fix for (#10597) Added 2-way TLS Support for oci pull for artifact repository which causes TLS handshake failure error. Special notes for your reviewer: Added flag for two-way authentication (--mtls-enabled) . eg: helm pull oci://nginx.testharbor.com:9443/testrepo/sslcharttest --version 0.1.0 --ca-file /etc/docker/certs.d/nginx.testharbor.com/ca.crt --cert-file /etc/docker/certs.d/nginx.testharbor.com/root_client.crt --key-file /etc/docker/certs.d/nginx.testharbor.com/root_client.key --mtls-enabled --- cmd/helm/flags.go | 1 + cmd/helm/pull_mtls_test.go | 136 ++++++++ pkg/action/install.go | 1 + pkg/action/pull.go | 22 +- pkg/getter/getter.go | 7 + pkg/registry/client.go | 68 +++- pkg/repo/repotest/mtlsserver.go | 445 +++++++++++++++++++++++++++ pkg/repo/repotest/mtlsserver_test.go | 118 +++++++ 8 files changed, 789 insertions(+), 9 deletions(-) create mode 100644 cmd/helm/pull_mtls_test.go create mode 100644 pkg/repo/repotest/mtlsserver.go create mode 100644 pkg/repo/repotest/mtlsserver_test.go diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go index 0cc0564e2..99c114d84 100644 --- a/cmd/helm/flags.go +++ b/cmd/helm/flags.go @@ -61,6 +61,7 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { 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") + f.BoolVar(&c.TlsEnabled, "mtls-enabled", false, "if two-way tls authentication enabled then trying to send client certificate") } // bindOutputFlag will add the output flag to the given command and bind the diff --git a/cmd/helm/pull_mtls_test.go b/cmd/helm/pull_mtls_test.go new file mode 100644 index 000000000..dc88bda20 --- /dev/null +++ b/cmd/helm/pull_mtls_test.go @@ -0,0 +1,136 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "helm.sh/helm/v3/pkg/repo/repotest" +) + +func TestMutualtlsPull(t *testing.T) { + srv, err := repotest.NewTempmtlsServerWithCleanup(t, "testdata/testcharts/*.tgz*") + if err != nil { + t.Fatal(err) + } + defer srv.Stopmtls() + + ociSrv, err := repotest.NewOCImtlsServer(t, srv.Rootmtls()) + if err != nil { + t.Fatal(err) + } + ociSrv.Run(t) + + if err := srv.LinkIndicesmtls(); err != nil { + t.Fatal(err) + } + + helmTestKeyOut := "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) \n" + + "Using Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\n" + + "Chart Hash Verified: " + + // all flags will get "-d outdir" appended. + tests := []struct { + name string + args string + existFile string + existDir string + wantError bool + wantErrorMsg string + failExpect string + expectFile string + expectDir bool + expectVerify bool + expectSha string + }{ + { + name: "Fetch OCI Chart", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --ca-file ../../testdata/rootca.crt --cert-file ../../testdata/rootca.crt --key-file ../../testdata/rootca.key --tls-enabled", ociSrv.RegistryURL), + expectFile: "./oci-dependent-chart-0.1.0.tgz", + }, + { + name: "Fail fetching non-existent OCI chart with mutual tls enabled", + args: fmt.Sprintf("oci://%s/u/ocitestuser/nosuchthing --version 0.1.0 --tls-enabled", ociSrv.RegistryURL), + failExpect: "Failed to fetch", + wantError: true, + }, + { + name: "Fail fetching OCI chart without version specified with mutual tls enabled", + args: fmt.Sprintf("oci://%s/u/ocitestuser/nosuchthing --tls-enabled", ociSrv.RegistryURL), + wantErrorMsg: "Error: --version flag is explicitly required for OCI registries", + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + outdir := srv.Rootmtls() + cmd := fmt.Sprintf("fetch %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) + } + } + _, out, 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) + } + + if tt.expectVerify { + outString := helmTestKeyOut + tt.expectSha + "\n" + if out != outString { + t.Errorf("%q: expected verification output %q, got %q", tt.name, outString, out) + } + + } + + 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) + } + }) + } +} diff --git a/pkg/action/install.go b/pkg/action/install.go index cd202ccab..97a833ea0 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -117,6 +117,7 @@ type ChartPathOptions struct { Username string // --username Verify bool // --verify Version string // --version + TlsEnabled bool // --mtls-enabled // registryClient provides a registry client but is not added with // options from a flag diff --git a/pkg/action/pull.go b/pkg/action/pull.go index b4018869e..07f30053a 100644 --- a/pkg/action/pull.go +++ b/pkg/action/pull.go @@ -25,6 +25,7 @@ import ( "github.com/pkg/errors" + "helm.sh/helm/v3/internal/tlsutil" "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/downloader" @@ -86,6 +87,7 @@ func (p *Pull) Run(chartRef string) (string, error) { getter.WithPassCredentialsAll(p.PassCredentialsAll), getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile), getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSverify), + getter.WithTwoWayTLSEnable(p.TlsEnabled), }, RegistryClient: p.cfg.RegistryClient, RepositoryConfig: p.Settings.RepositoryConfig, @@ -93,8 +95,24 @@ func (p *Pull) Run(chartRef string) (string, error) { } if registry.IsOCI(chartRef) { - c.Options = append(c.Options, - getter.WithRegistryClient(p.cfg.RegistryClient)) + if !p.TlsEnabled { + c.Options = append(c.Options, + getter.WithRegistryClient(p.cfg.RegistryClient), + ) + } else { + registryClient, err := registry.NewClient( + registry.ClientOptDebug(p.Settings.Debug), + registry.ClientOptCredentialsFile(p.Settings.RegistryConfig), + registry.ClientOptWriter(&out), + registry.ClientOptTwoWayTLSEnable(p.TlsEnabled), + registry.ClientOptChartRef(chartRef), + registry.ClientOptWithTLSOpts(tlsutil.Options{CaCertFile: p.CaFile, KeyFile: p.KeyFile, CertFile: p.CertFile, InsecureSkipVerify: p.InsecureSkipTLSverify}), + ) + if err != nil { + return out.String(), err + } + c.Options = append(c.Options, getter.WithRegistryClient(registryClient)) + } } if p.Verify { diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go index 653b032fe..b8b1252cb 100644 --- a/pkg/getter/getter.go +++ b/pkg/getter/getter.go @@ -42,6 +42,7 @@ type options struct { passCredentialsAll bool userAgent string version string + tlsEnabled bool registryClient *registry.Client timeout time.Duration transport *http.Transport @@ -87,6 +88,12 @@ func WithInsecureSkipVerifyTLS(insecureSkipVerifyTLS bool) Option { } } +func WithTwoWayTLSEnable(tlsEnabled bool) Option { + return func(opts *options) { + opts.tlsEnabled = tlsEnabled + } +} + // WithTLSClientConfig sets the client auth with the provided credentials. func WithTLSClientConfig(certFile, keyFile, caFile string) Option { return func(opts *options) { diff --git a/pkg/registry/client.go b/pkg/registry/client.go index c1004f956..12778a28a 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -22,9 +22,11 @@ import ( "fmt" "io" "io/ioutil" + "net" "net/http" "sort" "strings" + "time" "github.com/Masterminds/semver/v3" "github.com/containerd/containerd/remotes" @@ -38,6 +40,7 @@ import ( registryremote "oras.land/oras-go/pkg/registry/remote" registryauth "oras.land/oras-go/pkg/registry/remote/auth" + "helm.sh/helm/v3/internal/tlsutil" "helm.sh/helm/v3/internal/version" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/helmpath" @@ -61,6 +64,9 @@ type ( authorizer auth.Client registryAuthorizer *registryauth.Client resolver remotes.Resolver + tlsEnabled bool + chartRef string + utilOpts tlsutil.Options } // ClientOption allows specifying various settings configurable by the user for overriding the defaults @@ -87,14 +93,42 @@ func NewClient(options ...ClientOption) (*Client, error) { client.authorizer = authClient } if client.resolver == nil { - headers := http.Header{} - headers.Set("User-Agent", version.GetUserAgent()) - opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)} - resolver, err := client.authorizer.ResolverWithOpts(opts...) - if err != nil { - return nil, err + if client.tlsEnabled { + cfgtls, err := tlsutil.ClientConfig(client.utilOpts) + if err != nil { + fmt.Printf("error :%v\n", err) + + } + var rt http.RoundTripper = &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 30 * time.Second, + TLSClientConfig: cfgtls, + ResponseHeaderTimeout: time.Duration(30 * time.Second), + DisableKeepAlives: true, + } + sClient := http.Client{Transport: rt, Timeout: 30 * time.Second} + headers := http.Header{} + headers.Set("User-Agent", version.GetUserAgent()) + headers.Set("Authorization", "") + opts := []auth.ResolverOption{auth.WithResolverHeaders(headers), auth.WithResolverClient(&sClient)} + resolver, err := client.authorizer.ResolverWithOpts(opts...) + if err != nil { + return nil, err + } + client.resolver = resolver + } else { + headers := http.Header{} + headers.Set("User-Agent", version.GetUserAgent()) + opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)} + resolver, err := client.authorizer.ResolverWithOpts(opts...) + if err != nil { + return nil, err + } + client.resolver = resolver } - client.resolver = resolver } // allocate a cache if option is set @@ -159,6 +193,12 @@ func ClientOptWriter(out io.Writer) ClientOption { } } +func ClientOptChartRef(chartRef string) ClientOption { + return func(client *Client) { + client.chartRef = chartRef + } +} + // ClientOptCredentialsFile returns a function that sets the credentialsFile setting on a client options set func ClientOptCredentialsFile(credentialsFile string) ClientOption { return func(client *Client) { @@ -166,6 +206,20 @@ func ClientOptCredentialsFile(credentialsFile string) ClientOption { } } +//ClientOptTwoWayTLSEnable returns a function that sets the client certificate when two-way tls authentication enable +func ClientOptTwoWayTLSEnable(tlsEnabled bool) ClientOption { + return func(client *Client) { + client.tlsEnabled = tlsEnabled + } +} + +//ClientOptTwoWayTLSEnable returns a function that sets the client certificate when two-way tls authentication enable +func ClientOptWithTLSOpts(tlsOpts tlsutil.Options) ClientOption { + return func(client *Client) { + client.utilOpts = tlsOpts + } +} + type ( // LoginOption allows specifying various settings on login LoginOption func(*loginOperation) diff --git a/pkg/repo/repotest/mtlsserver.go b/pkg/repo/repotest/mtlsserver.go new file mode 100644 index 000000000..24b2a82e7 --- /dev/null +++ b/pkg/repo/repotest/mtlsserver.go @@ -0,0 +1,445 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repotest + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/distribution/distribution/v3/configuration" + "github.com/distribution/distribution/v3/registry" + _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" // used for docker test registry + _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" // used for docker test registry + "github.com/phayes/freeport" + "golang.org/x/crypto/bcrypt" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/internal/tlsutil" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + ociRegistry "helm.sh/helm/v3/pkg/registry" + "helm.sh/helm/v3/pkg/repo" +) + +// NewTempServerWithCleanup creates a server inside of a temp dir. +// +// If the passed in string is not "", it will be treated as a shell glob, and files +// will be copied from that path to the server's docroot. +// +// The caller is responsible for stopping the server. +// The temp dir will be removed by testing package automatically when test finished. +const ( + testCaCertFile = "crt.pem" + testCertFile = "rootca.crt" + testKeyFile = "rootca.key" +) + +const tlsTestDir = "../../testdata" + +func NewTempmtlsServerWithCleanup(t *testing.T, glob string) (*mtlsServer, error) { + srv, err := NewTempmtlsServer(glob) + t.Cleanup(func() { os.RemoveAll(srv.docroot) }) + return srv, err +} + +// Set up a fake repo with basic auth enabled +func NewTempmtlsServerWithCleanupAndBasicAuth(t *testing.T, glob string) *mtlsServer { + srv, err := NewTempmtlsServerWithCleanup(t, glob) + srv.Stopmtls() + if err != nil { + t.Fatal(err) + } + srv.WithMiddlewaremtls(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if !ok || username != "username" || password != "password" { + t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password) + } + })) + srv.mtlsStart() + return srv +} + +type mtlsOCIServer struct { + *registry.Registry + RegistryURL string + Dir string + TestUsername string + TestPassword string + Client *ociRegistry.Client +} + +type mtlsOCIServerRunConfig struct { + DependingChart *chart.Chart +} + +type mtlsOCIServerOpt func(config *mtlsOCIServerRunConfig) + +func NewOCImtlsServer(t *testing.T, dir string) (*mtlsOCIServer, error) { + testHtpasswdFileBasename := "authtest.htpasswd" + testUsername, testPassword := "username", "password" + + pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) + if err != nil { + t.Fatal("error generating bcrypt password for test htpasswd file") + } + htpasswdPath := filepath.Join(dir, testHtpasswdFileBasename) + err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644) + if err != nil { + t.Fatalf("error creating test htpasswd file") + } + + // Registry config + config := &configuration.Configuration{} + port, err := freeport.GetFreePort() + if err != nil { + t.Fatalf("error finding free port for test registry") + } + + config.HTTP.Addr = fmt.Sprintf(":%d", port) + config.HTTP.DrainTimeout = time.Duration(10) * time.Second + config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} + config.Auth = configuration.Auth{ + "htpasswd": configuration.Parameters{ + "realm": "localhost", + "path": htpasswdPath, + }, + } + + registryURL := fmt.Sprintf("localhost:%d", port) + + r, err := registry.NewRegistry(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + return &mtlsOCIServer{ + Registry: r, + RegistryURL: registryURL, + TestUsername: testUsername, + TestPassword: testPassword, + Dir: dir, + }, nil +} + +func (srv *mtlsOCIServer) Run(t *testing.T, opts ...mtlsOCIServerOpt) { + cfg := &mtlsOCIServerRunConfig{} + for _, fn := range opts { + fn(cfg) + } + + go srv.ListenAndServe() + + credentialsFile := filepath.Join(srv.Dir, "config.json") + + optns := tlsutil.Options{ + CaCertFile: testfile(t, testCaCertFile), + CertFile: testfile(t, testCertFile), + KeyFile: testfile(t, testKeyFile), + InsecureSkipVerify: false, + } + + // init test client + registryClient, err := ociRegistry.NewClient( + ociRegistry.ClientOptDebug(true), + ociRegistry.ClientOptEnableCache(true), + ociRegistry.ClientOptWriter(os.Stdout), + ociRegistry.ClientOptTwoWayTLSEnable(true), + ociRegistry.ClientOptCredentialsFile(credentialsFile), + ociRegistry.ClientOptWithTLSOpts(optns), + ) + if err != nil { + t.Fatalf("error creating registry client") + } + + err = registryClient.Login( + srv.RegistryURL, + ociRegistry.LoginOptBasicAuth(srv.TestUsername, srv.TestPassword), + ociRegistry.LoginOptInsecure(false)) + if err != nil { + t.Fatalf("error logging into registry with good credentials") + } + + ref := fmt.Sprintf("%s/u/ocitestuser/oci-dependent-chart:0.1.0", srv.RegistryURL) + + err = chartutil.ExpandFile(srv.Dir, filepath.Join(srv.Dir, "oci-dependent-chart-0.1.0.tgz")) + if err != nil { + t.Fatal(err) + } + + // valid chart + ch, err := loader.LoadDir(filepath.Join(srv.Dir, "oci-dependent-chart")) + if err != nil { + t.Fatal("error loading chart") + } + + err = os.RemoveAll(filepath.Join(srv.Dir, "oci-dependent-chart")) + if err != nil { + t.Fatal("error removing chart before push") + } + + // save it back to disk.. + absPath, err := chartutil.Save(ch, srv.Dir) + if err != nil { + t.Fatal("could not create chart archive") + } + + // load it into memory... + contentBytes, err := ioutil.ReadFile(absPath) + if err != nil { + t.Fatal("could not load chart into memory") + } + + result, err := registryClient.Push(contentBytes, ref) + if err != nil { + t.Fatalf("error pushing dependent chart: %s", err) + } + t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+ + "Config.Digest: %s, Config.Size: %d, "+ + "Chart.Digest: %s, Chart.Size: %d", + result.Manifest.Digest, result.Manifest.Size, + result.Config.Digest, result.Config.Size, + result.Chart.Digest, result.Chart.Size) + + srv.Client = registryClient + c := cfg.DependingChart + if c == nil { + return + } + + dependingRef := fmt.Sprintf("%s/u/ocitestuser/%s:%s", + srv.RegistryURL, c.Metadata.Name, c.Metadata.Version) + + // load it into memory... + absPath = filepath.Join(srv.Dir, + fmt.Sprintf("%s-%s.tgz", c.Metadata.Name, c.Metadata.Version)) + contentBytes, err = ioutil.ReadFile(absPath) + if err != nil { + t.Fatal("could not load chart into memory") + } + + result, err = registryClient.Push(contentBytes, dependingRef) + if err != nil { + t.Fatalf("error pushing depending chart: %s", err) + } + t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+ + "Config.Digest: %s, Config.Size: %d, "+ + "Chart.Digest: %s, Chart.Size: %d", + result.Manifest.Digest, result.Manifest.Size, + result.Config.Digest, result.Config.Size, + result.Chart.Digest, result.Chart.Size) +} + +func testfile(t *testing.T, file string) (path string) { + var err error + if path, err = filepath.Abs(filepath.Join(tlsTestDir, file)); err != nil { + t.Fatalf("error getting absolute path to test file %q: %v", file, err) + } + return path +} + +// NewTempServer creates a server inside of a temp dir. +// +// If the passed in string is not "", it will be treated as a shell glob, and files +// will be copied from that path to the server's docroot. +// +// The caller is responsible for destroying the temp directory as well as stopping +// the server. +// +// Deprecated: use NewTempServerWithCleanup +func NewTempmtlsServer(glob string) (*mtlsServer, error) { + tdir, err := ioutil.TempDir("", "helm-repotest-") + if err != nil { + return nil, err + } + srv := NewmtlsServer(tdir) + + if glob != "" { + if _, err := srv.mtlsCopyCharts(glob); err != nil { + srv.Stopmtls() + return srv, err + } + } + + return srv, nil +} + +// NewServer creates a repository server for testing. +// +// docroot should be a temp dir managed by the caller. +// +// This will start the server, serving files off of the docroot. +// +// Use CopyCharts to move charts into the repository and then index them +// for service. +func NewmtlsServer(docroot string) *mtlsServer { + root, err := filepath.Abs(docroot) + if err != nil { + panic(err) + } + srv := &mtlsServer{ + docroot: root, + } + srv.mtlsStart() + // Add the testing repository as the only repo. + if err := setmtlsTestingRepository(srv.mtlsURL(), filepath.Join(root, "repositories.yaml")); err != nil { + panic(err) + } + return srv +} + +// Server is an implementation of a repository server for testing. +type mtlsServer struct { + docroot string + srv *httptest.Server + middleware http.HandlerFunc +} + +// WithMiddleware injects middleware in front of the server. This can be used to inject +// additional functionality like layering in an authentication frontend. +func (s *mtlsServer) WithMiddlewaremtls(middleware http.HandlerFunc) { + s.middleware = middleware +} + +// Root gets the docroot for the server. +func (s *mtlsServer) Rootmtls() string { + return s.docroot +} + +// CopyCharts takes a glob expression and copies those charts to the server root. +func (s *mtlsServer) mtlsCopyCharts(origin string) ([]string, error) { + files, err := filepath.Glob(origin) + if err != nil { + return []string{}, err + } + copied := make([]string, len(files)) + for i, f := range files { + base := filepath.Base(f) + newname := filepath.Join(s.docroot, base) + data, err := ioutil.ReadFile(f) + if err != nil { + return []string{}, err + } + if err := ioutil.WriteFile(newname, data, 0644); err != nil { + return []string{}, err + } + copied[i] = newname + } + + err = s.mtlsCreateIndex() + return copied, err +} + +// CreateIndex will read docroot and generate an index.yaml file. +func (s *mtlsServer) mtlsCreateIndex() error { + // generate the index + index, err := repo.IndexDirectory(s.docroot, s.mtlsURL()) + if err != nil { + return err + } + + d, err := yaml.Marshal(index) + if err != nil { + return err + } + + ifile := filepath.Join(s.docroot, "index.yaml") + return ioutil.WriteFile(ifile, d, 0644) +} + +func (s *mtlsServer) mtlsStart() { + s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if s.middleware != nil { + s.middleware.ServeHTTP(w, r) + } + http.FileServer(http.Dir(s.docroot)).ServeHTTP(w, r) + })) +} + +func (s *mtlsServer) StartmTLS() { + cd := "../../testdata" + ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "crt.pem"), filepath.Join(cd, "key.pem") + + s.srv = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if s.middleware != nil { + s.middleware.ServeHTTP(w, r) + } + http.FileServer(http.Dir(s.Rootmtls())).ServeHTTP(w, r) + })) + tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca) + if err != nil { + panic(err) + } + tlsConf.BuildNameToCertificate() + tlsConf.ServerName = "helm.sh" + s.srv.TLS = tlsConf + s.srv.StartTLS() + + // Set up repositories config with ca file + repoConfig := filepath.Join(s.Rootmtls(), "repositories.yaml") + + r := repo.NewFile() + r.Add(&repo.Entry{ + Name: "test", + URL: s.mtlsURL(), + CAFile: filepath.Join("../../testdata", "rootca.crt"), + }) + + if err := r.WriteFile(repoConfig, 0644); err != nil { + panic(err) + } +} + +// Stop stops the server and closes all connections. +// +// It should be called explicitly. +func (s *mtlsServer) Stopmtls() { + s.srv.Close() +} + +// URL returns the URL of the server. +// +// Example: +// http://localhost:1776 +func (s *mtlsServer) mtlsURL() string { + return s.srv.URL +} + +// LinkIndices links the index created with CreateIndex and makes a symbolic link to the cache index. +// +// This makes it possible to simulate a local cache of a repository. +func (s *mtlsServer) LinkIndicesmtls() error { + lstart := filepath.Join(s.docroot, "index.yaml") + ldest := filepath.Join(s.docroot, "test-index.yaml") + return os.Symlink(lstart, ldest) +} + +// setTestingRepository sets up a testing repository.yaml with only the given URL. +func setmtlsTestingRepository(url, fname string) error { + r := repo.NewFile() + r.Add(&repo.Entry{ + Name: "test", + URL: url, + }) + return r.WriteFile(fname, 0644) +} diff --git a/pkg/repo/repotest/mtlsserver_test.go b/pkg/repo/repotest/mtlsserver_test.go new file mode 100644 index 000000000..8510f75d7 --- /dev/null +++ b/pkg/repo/repotest/mtlsserver_test.go @@ -0,0 +1,118 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repotest + +import ( + "io/ioutil" + "net/http" + "os" + "path/filepath" + "testing" + + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/repo" +) + +// Young'n, in these here parts, we test our tests. + +func TestMtlsServer(t *testing.T) { + defer ensure.HelmHome(t)() + + rootDir := ensure.TempDir(t) + defer os.RemoveAll(rootDir) + + srv := NewServer(rootDir) + defer srv.Stop() + + c, err := srv.CopyCharts("testdata/*.tgz") + if err != nil { + // Some versions of Go don't correctly fire defer on Fatal. + t.Fatal(err) + } + + if len(c) != 1 { + t.Errorf("Unexpected chart count: %d", len(c)) + } + + if filepath.Base(c[0]) != "examplechart-0.1.0.tgz" { + t.Errorf("Unexpected chart: %s", c[0]) + } + + res, err := http.Get(srv.URL() + "/examplechart-0.1.0.tgz") + res.Body.Close() + if err != nil { + t.Fatal(err) + } + + if res.ContentLength < 500 { + t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength) + } + + res, err = http.Get(srv.URL() + "/index.yaml") + if err != nil { + t.Fatal(err) + } + + data, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + + m := repo.NewIndexFile() + if err := yaml.Unmarshal(data, m); err != nil { + t.Fatal(err) + } + + if l := len(m.Entries); l != 1 { + t.Fatalf("Expected 1 entry, got %d", l) + } + + expect := "examplechart" + if !m.Has(expect, "0.1.0") { + t.Errorf("missing %q", expect) + } + + res, err = http.Get(srv.URL() + "/index.yaml-nosuchthing") + res.Body.Close() + if err != nil { + t.Fatal(err) + } + if res.StatusCode != 404 { + t.Fatalf("Expected 404, got %d", res.StatusCode) + } +} + +func TestNewmtlsTempServer(t *testing.T) { + defer ensure.HelmHome(t)() + + srv, err := NewTempServerWithCleanup(t, "testdata/examplechart-0.1.0.tgz") + if err != nil { + t.Fatal(err) + } + defer srv.Stop() + + res, err := http.Head(srv.URL() + "/examplechart-0.1.0.tgz") + res.Body.Close() + if err != nil { + t.Error(err) + } + if res.StatusCode != 200 { + t.Errorf("Expected 200, got %d", res.StatusCode) + } +}