github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/registry/client_test.go (about)

     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  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"net/http/httptest"
    27  	"net/url"
    28  	"os"
    29  	"path/filepath"
    30  	"strings"
    31  	"testing"
    32  	"time"
    33  
    34  	"github.com/containerd/containerd/errdefs"
    35  	"github.com/distribution/distribution/v3/configuration"
    36  	"github.com/distribution/distribution/v3/registry"
    37  	_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
    38  	_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
    39  	"github.com/phayes/freeport"
    40  	"github.com/stretchr/testify/suite"
    41  	"golang.org/x/crypto/bcrypt"
    42  )
    43  
    44  var (
    45  	testWorkspaceDir         = "helm-registry-test"
    46  	testHtpasswdFileBasename = "authtest.htpasswd"
    47  	testUsername             = "myuser"
    48  	testPassword             = "mypass"
    49  )
    50  
    51  type RegistryClientTestSuite struct {
    52  	suite.Suite
    53  	Out                     io.Writer
    54  	DockerRegistryHost      string
    55  	CompromisedRegistryHost string
    56  	WorkspaceDir            string
    57  	RegistryClient          *Client
    58  }
    59  
    60  func (suite *RegistryClientTestSuite) SetupSuite() {
    61  	suite.WorkspaceDir = testWorkspaceDir
    62  	os.RemoveAll(suite.WorkspaceDir)
    63  	os.Mkdir(suite.WorkspaceDir, 0700)
    64  
    65  	var out bytes.Buffer
    66  	suite.Out = &out
    67  	credentialsFile := filepath.Join(suite.WorkspaceDir, CredentialsFileBasename)
    68  
    69  	// init test client
    70  	var err error
    71  	suite.RegistryClient, err = NewClient(
    72  		ClientOptDebug(true),
    73  		ClientOptEnableCache(true),
    74  		ClientOptWriter(suite.Out),
    75  		ClientOptCredentialsFile(credentialsFile),
    76  	)
    77  	suite.Nil(err, "no error creating registry client")
    78  
    79  	// create htpasswd file (w BCrypt, which is required)
    80  	pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost)
    81  	suite.Nil(err, "no error generating bcrypt password for test htpasswd file")
    82  	htpasswdPath := filepath.Join(suite.WorkspaceDir, testHtpasswdFileBasename)
    83  	err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644)
    84  	suite.Nil(err, "no error creating test htpasswd file")
    85  
    86  	// Registry config
    87  	config := &configuration.Configuration{}
    88  	port, err := freeport.GetFreePort()
    89  	suite.Nil(err, "no error finding free port for test registry")
    90  	suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port)
    91  	config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port)
    92  	config.HTTP.DrainTimeout = time.Duration(10) * time.Second
    93  	config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
    94  	config.Auth = configuration.Auth{
    95  		"htpasswd": configuration.Parameters{
    96  			"realm": "localhost",
    97  			"path":  htpasswdPath,
    98  		},
    99  	}
   100  	dockerRegistry, err := registry.NewRegistry(context.Background(), config)
   101  	suite.Nil(err, "no error creating test registry")
   102  
   103  	suite.CompromisedRegistryHost = initCompromisedRegistryTestServer()
   104  
   105  	// Start Docker registry
   106  	go dockerRegistry.ListenAndServe()
   107  }
   108  
   109  func (suite *RegistryClientTestSuite) TearDownSuite() {
   110  	os.RemoveAll(suite.WorkspaceDir)
   111  }
   112  
   113  func (suite *RegistryClientTestSuite) Test_0_Login() {
   114  	err := suite.RegistryClient.Login(suite.DockerRegistryHost,
   115  		LoginOptBasicAuth("badverybad", "ohsobad"),
   116  		LoginOptInsecure(false))
   117  	suite.NotNil(err, "error logging into registry with bad credentials")
   118  
   119  	err = suite.RegistryClient.Login(suite.DockerRegistryHost,
   120  		LoginOptBasicAuth("badverybad", "ohsobad"),
   121  		LoginOptInsecure(true))
   122  	suite.NotNil(err, "error logging into registry with bad credentials, insecure mode")
   123  
   124  	err = suite.RegistryClient.Login(suite.DockerRegistryHost,
   125  		LoginOptBasicAuth(testUsername, testPassword),
   126  		LoginOptInsecure(false))
   127  	suite.Nil(err, "no error logging into registry with good credentials")
   128  
   129  	err = suite.RegistryClient.Login(suite.DockerRegistryHost,
   130  		LoginOptBasicAuth(testUsername, testPassword),
   131  		LoginOptInsecure(true))
   132  	suite.Nil(err, "no error logging into registry with good credentials, insecure mode")
   133  }
   134  
   135  func (suite *RegistryClientTestSuite) Test_1_Push() {
   136  	// Bad bytes
   137  	ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)
   138  	_, err := suite.RegistryClient.Push([]byte("hello"), ref)
   139  	suite.NotNil(err, "error pushing non-chart bytes")
   140  
   141  	// Load a test chart
   142  	chartData, err := ioutil.ReadFile("../repo/repotest/testdata/examplechart-0.1.0.tgz")
   143  	suite.Nil(err, "no error loading test chart")
   144  	meta, err := extractChartMeta(chartData)
   145  	suite.Nil(err, "no error extracting chart meta")
   146  
   147  	// non-strict ref (chart name)
   148  	ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version)
   149  	_, err = suite.RegistryClient.Push(chartData, ref)
   150  	suite.NotNil(err, "error pushing non-strict ref (bad basename)")
   151  
   152  	// non-strict ref (chart name), with strict mode disabled
   153  	_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false))
   154  	suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled")
   155  
   156  	// non-strict ref (chart version)
   157  	ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name)
   158  	_, err = suite.RegistryClient.Push(chartData, ref)
   159  	suite.NotNil(err, "error pushing non-strict ref (bad tag)")
   160  
   161  	// non-strict ref (chart version), with strict mode disabled
   162  	_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false))
   163  	suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled")
   164  
   165  	// basic push, good ref
   166  	chartData, err = ioutil.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz")
   167  	suite.Nil(err, "no error loading test chart")
   168  	meta, err = extractChartMeta(chartData)
   169  	suite.Nil(err, "no error extracting chart meta")
   170  	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
   171  	_, err = suite.RegistryClient.Push(chartData, ref)
   172  	suite.Nil(err, "no error pushing good ref")
   173  
   174  	_, err = suite.RegistryClient.Pull(ref)
   175  	suite.Nil(err, "no error pulling a simple chart")
   176  
   177  	// Load another test chart
   178  	chartData, err = ioutil.ReadFile("../downloader/testdata/signtest-0.1.0.tgz")
   179  	suite.Nil(err, "no error loading test chart")
   180  	meta, err = extractChartMeta(chartData)
   181  	suite.Nil(err, "no error extracting chart meta")
   182  
   183  	// Load prov file
   184  	provData, err := ioutil.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov")
   185  	suite.Nil(err, "no error loading test prov")
   186  
   187  	// push with prov
   188  	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
   189  	result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData))
   190  	suite.Nil(err, "no error pushing good ref with prov")
   191  
   192  	_, err = suite.RegistryClient.Pull(ref)
   193  	suite.Nil(err, "no error pulling a simple chart")
   194  
   195  	// Validate the output
   196  	// Note: these digests/sizes etc may change if the test chart/prov files are modified,
   197  	// or if the format of the OCI manifest changes
   198  	suite.Equal(ref, result.Ref)
   199  	suite.Equal(meta.Name, result.Chart.Meta.Name)
   200  	suite.Equal(meta.Version, result.Chart.Meta.Version)
   201  	suite.Equal(int64(512), result.Manifest.Size)
   202  	suite.Equal(int64(99), result.Config.Size)
   203  	suite.Equal(int64(973), result.Chart.Size)
   204  	suite.Equal(int64(695), result.Prov.Size)
   205  	suite.Equal(
   206  		"sha256:af4c20a1df1431495e673c14ecfa3a2ba24839a7784349d6787cd67957392e83",
   207  		result.Manifest.Digest)
   208  	suite.Equal(
   209  		"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
   210  		result.Config.Digest)
   211  	suite.Equal(
   212  		"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
   213  		result.Chart.Digest)
   214  	suite.Equal(
   215  		"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
   216  		result.Prov.Digest)
   217  }
   218  
   219  func (suite *RegistryClientTestSuite) Test_2_Pull() {
   220  	// bad/missing ref
   221  	ref := fmt.Sprintf("%s/testrepo/no-existy:1.2.3", suite.DockerRegistryHost)
   222  	_, err := suite.RegistryClient.Pull(ref)
   223  	suite.NotNil(err, "error on bad/missing ref")
   224  
   225  	// Load test chart (to build ref pushed in previous test)
   226  	chartData, err := ioutil.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz")
   227  	suite.Nil(err, "no error loading test chart")
   228  	meta, err := extractChartMeta(chartData)
   229  	suite.Nil(err, "no error extracting chart meta")
   230  	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
   231  
   232  	// Simple pull, chart only
   233  	_, err = suite.RegistryClient.Pull(ref)
   234  	suite.Nil(err, "no error pulling a simple chart")
   235  
   236  	// Simple pull with prov (no prov uploaded)
   237  	_, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true))
   238  	suite.NotNil(err, "error pulling a chart with prov when no prov exists")
   239  
   240  	// Simple pull with prov, ignoring missing prov
   241  	_, err = suite.RegistryClient.Pull(ref,
   242  		PullOptWithProv(true),
   243  		PullOptIgnoreMissingProv(true))
   244  	suite.Nil(err,
   245  		"no error pulling a chart with prov when no prov exists, ignoring missing")
   246  
   247  	// Load test chart (to build ref pushed in previous test)
   248  	chartData, err = ioutil.ReadFile("../downloader/testdata/signtest-0.1.0.tgz")
   249  	suite.Nil(err, "no error loading test chart")
   250  	meta, err = extractChartMeta(chartData)
   251  	suite.Nil(err, "no error extracting chart meta")
   252  	ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
   253  
   254  	// Load prov file
   255  	provData, err := ioutil.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov")
   256  	suite.Nil(err, "no error loading test prov")
   257  
   258  	// no chart and no prov causes error
   259  	_, err = suite.RegistryClient.Pull(ref,
   260  		PullOptWithChart(false),
   261  		PullOptWithProv(false))
   262  	suite.NotNil(err, "error on both no chart and no prov")
   263  
   264  	// full pull with chart and prov
   265  	result, err := suite.RegistryClient.Pull(ref, PullOptWithProv(true))
   266  	suite.Nil(err, "no error pulling a chart with prov")
   267  
   268  	// Validate the output
   269  	// Note: these digests/sizes etc may change if the test chart/prov files are modified,
   270  	// or if the format of the OCI manifest changes
   271  	suite.Equal(ref, result.Ref)
   272  	suite.Equal(meta.Name, result.Chart.Meta.Name)
   273  	suite.Equal(meta.Version, result.Chart.Meta.Version)
   274  	suite.Equal(int64(512), result.Manifest.Size)
   275  	suite.Equal(int64(99), result.Config.Size)
   276  	suite.Equal(int64(973), result.Chart.Size)
   277  	suite.Equal(int64(695), result.Prov.Size)
   278  	suite.Equal(
   279  		"sha256:af4c20a1df1431495e673c14ecfa3a2ba24839a7784349d6787cd67957392e83",
   280  		result.Manifest.Digest)
   281  	suite.Equal(
   282  		"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
   283  		result.Config.Digest)
   284  	suite.Equal(
   285  		"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
   286  		result.Chart.Digest)
   287  	suite.Equal(
   288  		"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
   289  		result.Prov.Digest)
   290  	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}]}",
   291  		string(result.Manifest.Data))
   292  	suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}",
   293  		string(result.Config.Data))
   294  	suite.Equal(chartData, result.Chart.Data)
   295  	suite.Equal(provData, result.Prov.Data)
   296  }
   297  
   298  func (suite *RegistryClientTestSuite) Test_3_Tags() {
   299  
   300  	// Load test chart (to build ref pushed in previous test)
   301  	chartData, err := ioutil.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz")
   302  	suite.Nil(err, "no error loading test chart")
   303  	meta, err := extractChartMeta(chartData)
   304  	suite.Nil(err, "no error extracting chart meta")
   305  	ref := fmt.Sprintf("%s/testrepo/%s", suite.DockerRegistryHost, meta.Name)
   306  
   307  	// Query for tags and validate length
   308  	tags, err := suite.RegistryClient.Tags(ref)
   309  	suite.Nil(err, "no error retrieving tags")
   310  	suite.Equal(1, len(tags))
   311  
   312  }
   313  
   314  func (suite *RegistryClientTestSuite) Test_4_Logout() {
   315  	err := suite.RegistryClient.Logout("this-host-aint-real:5000")
   316  	suite.NotNil(err, "error logging out of registry that has no entry")
   317  
   318  	err = suite.RegistryClient.Logout(suite.DockerRegistryHost)
   319  	suite.Nil(err, "no error logging out of registry")
   320  }
   321  
   322  func (suite *RegistryClientTestSuite) Test_5_ManInTheMiddle() {
   323  	ref := fmt.Sprintf("%s/testrepo/supposedlysafechart:9.9.9", suite.CompromisedRegistryHost)
   324  
   325  	// returns content that does not match the expected digest
   326  	_, err := suite.RegistryClient.Pull(ref)
   327  	suite.NotNil(err)
   328  	suite.True(errdefs.IsFailedPrecondition(err))
   329  }
   330  
   331  func TestRegistryClientTestSuite(t *testing.T) {
   332  	suite.Run(t, new(RegistryClientTestSuite))
   333  }
   334  
   335  func initCompromisedRegistryTestServer() string {
   336  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   337  		if strings.Contains(r.URL.Path, "manifests") {
   338  			w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
   339  			w.WriteHeader(200)
   340  
   341  			// layers[0] is the blob []byte("a")
   342  			w.Write([]byte(
   343  				fmt.Sprintf(`{ "schemaVersion": 2, "config": {
   344      "mediaType": "%s",
   345      "digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133",
   346      "size": 181
   347    },
   348    "layers": [
   349      {
   350        "mediaType": "%s",
   351        "digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
   352        "size": 1
   353      }
   354    ]
   355  }`, ConfigMediaType, ChartLayerMediaType)))
   356  		} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" {
   357  			w.Header().Set("Content-Type", "application/json")
   358  			w.WriteHeader(200)
   359  			w.Write([]byte("{\"name\":\"mychart\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\\n" +
   360  				"an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" +
   361  				"\"application\"}"))
   362  		} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" {
   363  			w.Header().Set("Content-Type", ChartLayerMediaType)
   364  			w.WriteHeader(200)
   365  			w.Write([]byte("b"))
   366  		} else {
   367  			w.WriteHeader(500)
   368  		}
   369  	}))
   370  
   371  	u, _ := url.Parse(s.URL)
   372  	return fmt.Sprintf("localhost:%s", u.Port())
   373  }