...

Source file src/helm.sh/helm/v3/pkg/registry/utils_test.go

Documentation: helm.sh/helm/v3/pkg/registry

     1  /*
     2  Copyright The Helm Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package registry
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/tls"
    23  	"fmt"
    24  	"io"
    25  	"net"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"net/url"
    29  	"os"
    30  	"path/filepath"
    31  	"strings"
    32  	"time"
    33  
    34  	"github.com/distribution/distribution/v3/configuration"
    35  	"github.com/distribution/distribution/v3/registry"
    36  	_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
    37  	_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
    38  	"github.com/foxcpp/go-mockdns"
    39  	"github.com/phayes/freeport"
    40  	"github.com/stretchr/testify/suite"
    41  	"golang.org/x/crypto/bcrypt"
    42  
    43  	"helm.sh/helm/v3/internal/tlsutil"
    44  )
    45  
    46  const (
    47  	tlsServerKey  = "./testdata/tls/server.key"
    48  	tlsServerCert = "./testdata/tls/server.crt"
    49  	tlsCA         = "./testdata/tls/ca.crt"
    50  	tlsKey        = "./testdata/tls/client.key"
    51  	tlsCert       = "./testdata/tls/client.crt"
    52  )
    53  
    54  var (
    55  	testWorkspaceDir         = "helm-registry-test"
    56  	testHtpasswdFileBasename = "authtest.htpasswd"
    57  	testUsername             = "myuser"
    58  	testPassword             = "mypass"
    59  )
    60  
    61  type TestSuite struct {
    62  	suite.Suite
    63  	Out                     io.Writer
    64  	DockerRegistryHost      string
    65  	CompromisedRegistryHost string
    66  	WorkspaceDir            string
    67  	RegistryClient          *Client
    68  
    69  	// A mock DNS server needed for TLS connection testing.
    70  	srv *mockdns.Server
    71  }
    72  
    73  func setup(suite *TestSuite, tlsEnabled, insecure bool) *registry.Registry {
    74  	suite.WorkspaceDir = testWorkspaceDir
    75  	os.RemoveAll(suite.WorkspaceDir)
    76  	os.Mkdir(suite.WorkspaceDir, 0700)
    77  
    78  	var (
    79  		out bytes.Buffer
    80  		err error
    81  	)
    82  	suite.Out = &out
    83  	credentialsFile := filepath.Join(suite.WorkspaceDir, CredentialsFileBasename)
    84  
    85  	// init test client
    86  	opts := []ClientOption{
    87  		ClientOptDebug(true),
    88  		ClientOptEnableCache(true),
    89  		ClientOptWriter(suite.Out),
    90  		ClientOptCredentialsFile(credentialsFile),
    91  		ClientOptResolver(nil),
    92  	}
    93  
    94  	if tlsEnabled {
    95  		var tlsConf *tls.Config
    96  		if insecure {
    97  			tlsConf, err = tlsutil.NewClientTLS("", "", "", true)
    98  		} else {
    99  			tlsConf, err = tlsutil.NewClientTLS(tlsCert, tlsKey, tlsCA, false)
   100  		}
   101  		httpClient := &http.Client{
   102  			Transport: &http.Transport{
   103  				TLSClientConfig: tlsConf,
   104  			},
   105  		}
   106  		suite.Nil(err, "no error loading tls config")
   107  		opts = append(opts, ClientOptHTTPClient(httpClient))
   108  	} else {
   109  		opts = append(opts, ClientOptPlainHTTP())
   110  	}
   111  
   112  	suite.RegistryClient, err = NewClient(opts...)
   113  	suite.Nil(err, "no error creating registry client")
   114  
   115  	// create htpasswd file (w BCrypt, which is required)
   116  	pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost)
   117  	suite.Nil(err, "no error generating bcrypt password for test htpasswd file")
   118  	htpasswdPath := filepath.Join(suite.WorkspaceDir, testHtpasswdFileBasename)
   119  	err = os.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644)
   120  	suite.Nil(err, "no error creating test htpasswd file")
   121  
   122  	// Registry config
   123  	config := &configuration.Configuration{}
   124  	port, err := freeport.GetFreePort()
   125  	suite.Nil(err, "no error finding free port for test registry")
   126  
   127  	// Change the registry host to another host which is not localhost.
   128  	// This is required because Docker enforces HTTP if the registry
   129  	// host is localhost/127.0.0.1.
   130  	suite.DockerRegistryHost = fmt.Sprintf("helm-test-registry:%d", port)
   131  	suite.srv, _ = mockdns.NewServer(map[string]mockdns.Zone{
   132  		"helm-test-registry.": {
   133  			A: []string{"127.0.0.1"},
   134  		},
   135  	}, false)
   136  	suite.srv.PatchNet(net.DefaultResolver)
   137  
   138  	config.HTTP.Addr = fmt.Sprintf(":%d", port)
   139  	config.HTTP.DrainTimeout = time.Duration(10) * time.Second
   140  	config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
   141  
   142  	// Basic auth is not possible if we are serving HTTP.
   143  	if tlsEnabled {
   144  		config.Auth = configuration.Auth{
   145  			"htpasswd": configuration.Parameters{
   146  				"realm": "localhost",
   147  				"path":  htpasswdPath,
   148  			},
   149  		}
   150  	}
   151  
   152  	// config tls
   153  	if tlsEnabled {
   154  		// TLS config
   155  		// this set tlsConf.ClientAuth = tls.RequireAndVerifyClientCert in the
   156  		// server tls config
   157  		config.HTTP.TLS.Certificate = tlsServerCert
   158  		config.HTTP.TLS.Key = tlsServerKey
   159  		// Skip client authentication if the registry is insecure.
   160  		if !insecure {
   161  			config.HTTP.TLS.ClientCAs = []string{tlsCA}
   162  		}
   163  	}
   164  	dockerRegistry, err := registry.NewRegistry(context.Background(), config)
   165  	suite.Nil(err, "no error creating test registry")
   166  
   167  	suite.CompromisedRegistryHost = initCompromisedRegistryTestServer()
   168  	return dockerRegistry
   169  }
   170  
   171  func teardown(suite *TestSuite) {
   172  	if suite.srv != nil {
   173  		mockdns.UnpatchNet(net.DefaultResolver)
   174  		suite.srv.Close()
   175  	}
   176  }
   177  
   178  func initCompromisedRegistryTestServer() string {
   179  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   180  		if strings.Contains(r.URL.Path, "manifests") {
   181  			w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
   182  			w.WriteHeader(200)
   183  
   184  			// layers[0] is the blob []byte("a")
   185  			w.Write([]byte(
   186  				fmt.Sprintf(`{ "schemaVersion": 2, "config": {
   187      "mediaType": "%s",
   188      "digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133",
   189      "size": 181
   190    },
   191    "layers": [
   192      {
   193        "mediaType": "%s",
   194        "digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
   195        "size": 1
   196      }
   197    ]
   198  }`, ConfigMediaType, ChartLayerMediaType)))
   199  		} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" {
   200  			w.Header().Set("Content-Type", "application/json")
   201  			w.WriteHeader(200)
   202  			w.Write([]byte("{\"name\":\"mychart\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\\n" +
   203  				"an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" +
   204  				"\"application\"}"))
   205  		} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" {
   206  			w.Header().Set("Content-Type", ChartLayerMediaType)
   207  			w.WriteHeader(200)
   208  			w.Write([]byte("b"))
   209  		} else {
   210  			w.WriteHeader(500)
   211  		}
   212  	}))
   213  
   214  	u, _ := url.Parse(s.URL)
   215  	return fmt.Sprintf("localhost:%s", u.Port())
   216  }
   217  
   218  func testPush(suite *TestSuite) {
   219  
   220  	testingChartCreationTime := "1977-09-02T22:04:05Z"
   221  
   222  	// Bad bytes
   223  	ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)
   224  	_, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptCreationTime(testingChartCreationTime))
   225  	suite.NotNil(err, "error pushing non-chart bytes")
   226  
   227  	// Load a test chart
   228  	chartData, err := os.ReadFile("../repo/repotest/testdata/examplechart-0.1.0.tgz")
   229  	suite.Nil(err, "no error loading test chart")
   230  	meta, err := extractChartMeta(chartData)
   231  	suite.Nil(err, "no error extracting chart meta")
   232  
   233  	// non-strict ref (chart name)
   234  	ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version)
   235  	_, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime))
   236  	suite.NotNil(err, "error pushing non-strict ref (bad basename)")
   237  
   238  	// non-strict ref (chart name), with strict mode disabled
   239  	_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptCreationTime(testingChartCreationTime))
   240  	suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled")
   241  
   242  	// non-strict ref (chart version)
   243  	ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name)
   244  	_, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime))
   245  	suite.NotNil(err, "error pushing non-strict ref (bad tag)")
   246  
   247  	// non-strict ref (chart version), with strict mode disabled
   248  	_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptCreationTime(testingChartCreationTime))
   249  	suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled")
   250  
   251  	// basic push, good ref
   252  	chartData, err = os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz")
   253  	suite.Nil(err, "no error loading test chart")
   254  	meta, err = extractChartMeta(chartData)
   255  	suite.Nil(err, "no error extracting chart meta")
   256  	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
   257  	_, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime))
   258  	suite.Nil(err, "no error pushing good ref")
   259  
   260  	_, err = suite.RegistryClient.Pull(ref)
   261  	suite.Nil(err, "no error pulling a simple chart")
   262  
   263  	// Load another test chart
   264  	chartData, err = os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz")
   265  	suite.Nil(err, "no error loading test chart")
   266  	meta, err = extractChartMeta(chartData)
   267  	suite.Nil(err, "no error extracting chart meta")
   268  
   269  	// Load prov file
   270  	provData, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov")
   271  	suite.Nil(err, "no error loading test prov")
   272  
   273  	// push with prov
   274  	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
   275  	result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData), PushOptCreationTime(testingChartCreationTime))
   276  	suite.Nil(err, "no error pushing good ref with prov")
   277  
   278  	_, err = suite.RegistryClient.Pull(ref)
   279  	suite.Nil(err, "no error pulling a simple chart")
   280  
   281  	// Validate the output
   282  	// Note: these digests/sizes etc may change if the test chart/prov files are modified,
   283  	// or if the format of the OCI manifest changes
   284  	suite.Equal(ref, result.Ref)
   285  	suite.Equal(meta.Name, result.Chart.Meta.Name)
   286  	suite.Equal(meta.Version, result.Chart.Meta.Version)
   287  	suite.Equal(int64(742), result.Manifest.Size)
   288  	suite.Equal(int64(99), result.Config.Size)
   289  	suite.Equal(int64(973), result.Chart.Size)
   290  	suite.Equal(int64(695), result.Prov.Size)
   291  	suite.Equal(
   292  		"sha256:fbbade96da6050f68f94f122881e3b80051a18f13ab5f4081868dd494538f5c2",
   293  		result.Manifest.Digest)
   294  	suite.Equal(
   295  		"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
   296  		result.Config.Digest)
   297  	suite.Equal(
   298  		"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
   299  		result.Chart.Digest)
   300  	suite.Equal(
   301  		"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
   302  		result.Prov.Digest)
   303  }
   304  
   305  func testPull(suite *TestSuite) {
   306  	// bad/missing ref
   307  	ref := fmt.Sprintf("%s/testrepo/no-existy:1.2.3", suite.DockerRegistryHost)
   308  	_, err := suite.RegistryClient.Pull(ref)
   309  	suite.NotNil(err, "error on bad/missing ref")
   310  
   311  	// Load test chart (to build ref pushed in previous test)
   312  	chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz")
   313  	suite.Nil(err, "no error loading test chart")
   314  	meta, err := extractChartMeta(chartData)
   315  	suite.Nil(err, "no error extracting chart meta")
   316  	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
   317  
   318  	// Simple pull, chart only
   319  	_, err = suite.RegistryClient.Pull(ref)
   320  	suite.Nil(err, "no error pulling a simple chart")
   321  
   322  	// Simple pull with prov (no prov uploaded)
   323  	_, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true))
   324  	suite.NotNil(err, "error pulling a chart with prov when no prov exists")
   325  
   326  	// Simple pull with prov, ignoring missing prov
   327  	_, err = suite.RegistryClient.Pull(ref,
   328  		PullOptWithProv(true),
   329  		PullOptIgnoreMissingProv(true))
   330  	suite.Nil(err,
   331  		"no error pulling a chart with prov when no prov exists, ignoring missing")
   332  
   333  	// Load test chart (to build ref pushed in previous test)
   334  	chartData, err = os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz")
   335  	suite.Nil(err, "no error loading test chart")
   336  	meta, err = extractChartMeta(chartData)
   337  	suite.Nil(err, "no error extracting chart meta")
   338  	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
   339  
   340  	// Load prov file
   341  	provData, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov")
   342  	suite.Nil(err, "no error loading test prov")
   343  
   344  	// no chart and no prov causes error
   345  	_, err = suite.RegistryClient.Pull(ref,
   346  		PullOptWithChart(false),
   347  		PullOptWithProv(false))
   348  	suite.NotNil(err, "error on both no chart and no prov")
   349  
   350  	// full pull with chart and prov
   351  	result, err := suite.RegistryClient.Pull(ref, PullOptWithProv(true))
   352  	suite.Nil(err, "no error pulling a chart with prov")
   353  
   354  	// Validate the output
   355  	// Note: these digests/sizes etc may change if the test chart/prov files are modified,
   356  	// or if the format of the OCI manifest changes
   357  	suite.Equal(ref, result.Ref)
   358  	suite.Equal(meta.Name, result.Chart.Meta.Name)
   359  	suite.Equal(meta.Version, result.Chart.Meta.Version)
   360  	suite.Equal(int64(742), result.Manifest.Size)
   361  	suite.Equal(int64(99), result.Config.Size)
   362  	suite.Equal(int64(973), result.Chart.Size)
   363  	suite.Equal(int64(695), result.Prov.Size)
   364  	suite.Equal(
   365  		"sha256:fbbade96da6050f68f94f122881e3b80051a18f13ab5f4081868dd494538f5c2",
   366  		result.Manifest.Digest)
   367  	suite.Equal(
   368  		"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
   369  		result.Config.Digest)
   370  	suite.Equal(
   371  		"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
   372  		result.Chart.Digest)
   373  	suite.Equal(
   374  		"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
   375  		result.Prov.Digest)
   376  	suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}],\"annotations\":{\"org.opencontainers.image.created\":\"1977-09-02T22:04:05Z\",\"org.opencontainers.image.description\":\"A Helm chart for Kubernetes\",\"org.opencontainers.image.title\":\"signtest\",\"org.opencontainers.image.version\":\"0.1.0\"}}",
   377  		string(result.Manifest.Data))
   378  	suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}",
   379  		string(result.Config.Data))
   380  	suite.Equal(chartData, result.Chart.Data)
   381  	suite.Equal(provData, result.Prov.Data)
   382  }
   383  
   384  func testTags(suite *TestSuite) {
   385  	// Load test chart (to build ref pushed in previous test)
   386  	chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz")
   387  	suite.Nil(err, "no error loading test chart")
   388  	meta, err := extractChartMeta(chartData)
   389  	suite.Nil(err, "no error extracting chart meta")
   390  	ref := fmt.Sprintf("%s/testrepo/%s", suite.DockerRegistryHost, meta.Name)
   391  
   392  	// Query for tags and validate length
   393  	tags, err := suite.RegistryClient.Tags(ref)
   394  	suite.Nil(err, "no error retrieving tags")
   395  	suite.Equal(1, len(tags))
   396  }
   397  

View as plain text