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 }