code.gitea.io/gitea@v1.22.3/tests/integration/api_packages_container_test.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package integration 5 6 import ( 7 "bytes" 8 "crypto/sha256" 9 "encoding/base64" 10 "fmt" 11 "net/http" 12 "strings" 13 "sync" 14 "testing" 15 16 auth_model "code.gitea.io/gitea/models/auth" 17 "code.gitea.io/gitea/models/db" 18 packages_model "code.gitea.io/gitea/models/packages" 19 container_model "code.gitea.io/gitea/models/packages/container" 20 "code.gitea.io/gitea/models/unittest" 21 user_model "code.gitea.io/gitea/models/user" 22 container_module "code.gitea.io/gitea/modules/packages/container" 23 "code.gitea.io/gitea/modules/setting" 24 api "code.gitea.io/gitea/modules/structs" 25 "code.gitea.io/gitea/modules/test" 26 package_service "code.gitea.io/gitea/services/packages" 27 "code.gitea.io/gitea/tests" 28 29 oci "github.com/opencontainers/image-spec/specs-go/v1" 30 "github.com/stretchr/testify/assert" 31 ) 32 33 func TestPackageContainer(t *testing.T) { 34 defer tests.PrepareTestEnv(t)() 35 36 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 37 session := loginUser(t, user.Name) 38 token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadPackage) 39 privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31}) 40 41 has := func(l packages_model.PackagePropertyList, name string) bool { 42 for _, pp := range l { 43 if pp.Name == name { 44 return true 45 } 46 } 47 return false 48 } 49 getAllByName := func(l packages_model.PackagePropertyList, name string) []string { 50 values := make([]string, 0, len(l)) 51 for _, pp := range l { 52 if pp.Name == name { 53 values = append(values, pp.Value) 54 } 55 } 56 return values 57 } 58 59 images := []string{"test", "te/st"} 60 tags := []string{"latest", "main"} 61 multiTag := "multi" 62 63 unknownDigest := "sha256:0000000000000000000000000000000000000000000000000000000000000000" 64 65 blobDigest := "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 66 blobContent, _ := base64.StdEncoding.DecodeString(`H4sIAAAJbogA/2IYBaNgFIxYAAgAAP//Lq+17wAEAAA=`) 67 68 configDigest := "sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d" 69 configContent := `{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/true"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"container":"b89fe92a887d55c0961f02bdfbfd8ac3ddf66167db374770d2d9e9fab3311510","container_config":{"Hostname":"b89fe92a887d","Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/true\"]"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"created":"2022-01-01T00:00:00.000000000Z","docker_version":"20.10.12","history":[{"created":"2022-01-01T00:00:00.000000000Z","created_by":"/bin/sh -c #(nop) COPY file:0e7589b0c800daaf6fa460d2677101e4676dd9491980210cb345480e513f3602 in /true "},{"created":"2022-01-01T00:00:00.000000001Z","created_by":"/bin/sh -c #(nop) CMD [\"/true\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:0ff3b91bdf21ecdf2f2f3d4372c2098a14dbe06cd678e8f0a85fd4902d00e2e2"]}}` 70 71 manifestDigest := "sha256:4f10484d1c1bb13e3956b4de1cd42db8e0f14a75be1617b60f2de3cd59c803c6" 72 manifestContent := `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}` 73 74 untaggedManifestDigest := "sha256:4305f5f5572b9a426b88909b036e52ee3cf3d7b9c1b01fac840e90747f56623d" 75 untaggedManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageManifest + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}` 76 77 indexManifestDigest := "sha256:bab112d6efb9e7f221995caaaa880352feb5bd8b1faf52fae8d12c113aa123ec" 78 indexManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageIndex + `","manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"` + manifestDigest + `","platform":{"os":"linux","architecture":"arm","variant":"v7"}},{"mediaType":"` + oci.MediaTypeImageManifest + `","digest":"` + untaggedManifestDigest + `","platform":{"os":"linux","architecture":"arm64","variant":"v8"}}]}` 79 80 anonymousToken := "" 81 userToken := "" 82 readToken := "" 83 badToken := "" 84 85 t.Run("Authenticate", func(t *testing.T) { 86 type TokenResponse struct { 87 Token string `json:"token"` 88 } 89 90 defaultAuthenticateValues := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`} 91 92 t.Run("Anonymous", func(t *testing.T) { 93 defer tests.PrintCurrentTest(t)() 94 95 req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)) 96 resp := MakeRequest(t, req, http.StatusUnauthorized) 97 98 assert.ElementsMatch(t, defaultAuthenticateValues, resp.Header().Values("WWW-Authenticate")) 99 100 req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL)) 101 resp = MakeRequest(t, req, http.StatusOK) 102 103 tokenResponse := &TokenResponse{} 104 DecodeJSON(t, resp, &tokenResponse) 105 106 assert.NotEmpty(t, tokenResponse.Token) 107 108 anonymousToken = fmt.Sprintf("Bearer %s", tokenResponse.Token) 109 110 req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)). 111 AddTokenAuth(anonymousToken) 112 MakeRequest(t, req, http.StatusOK) 113 114 defer test.MockVariableValue(&setting.Service.RequireSignInView, true)() 115 116 req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)) 117 MakeRequest(t, req, http.StatusUnauthorized) 118 119 req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL)) 120 MakeRequest(t, req, http.StatusUnauthorized) 121 122 defer test.MockVariableValue(&setting.AppURL, "https://domain:8443/sub-path/")() 123 defer test.MockVariableValue(&setting.AppSubURL, "/sub-path")() 124 req = NewRequest(t, "GET", "/v2") 125 resp = MakeRequest(t, req, http.StatusUnauthorized) 126 assert.Equal(t, `Bearer realm="https://domain:8443/v2/token",service="container_registry",scope="*"`, resp.Header().Get("WWW-Authenticate")) 127 }) 128 129 t.Run("UserName/Password", func(t *testing.T) { 130 defer tests.PrintCurrentTest(t)() 131 132 req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)) 133 resp := MakeRequest(t, req, http.StatusUnauthorized) 134 135 assert.ElementsMatch(t, defaultAuthenticateValues, resp.Header().Values("WWW-Authenticate")) 136 137 req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL)). 138 AddBasicAuth(user.Name) 139 resp = MakeRequest(t, req, http.StatusOK) 140 141 tokenResponse := &TokenResponse{} 142 DecodeJSON(t, resp, &tokenResponse) 143 144 assert.NotEmpty(t, tokenResponse.Token) 145 pkgMeta, err := package_service.ParseAuthorizationToken(tokenResponse.Token) 146 assert.NoError(t, err) 147 assert.Equal(t, user.ID, pkgMeta.UserID) 148 assert.Equal(t, auth_model.AccessTokenScopeAll, pkgMeta.Scope) 149 150 userToken = fmt.Sprintf("Bearer %s", tokenResponse.Token) 151 152 req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)). 153 AddTokenAuth(userToken) 154 MakeRequest(t, req, http.StatusOK) 155 }) 156 157 // Token that should enforce the read scope. 158 t.Run("AccessToken", func(t *testing.T) { 159 defer tests.PrintCurrentTest(t)() 160 161 session := loginUser(t, user.Name) 162 163 readToken = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadPackage) 164 req := NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL)) 165 req.Request.SetBasicAuth(user.Name, readToken) 166 resp := MakeRequest(t, req, http.StatusOK) 167 tokenResponse := &TokenResponse{} 168 DecodeJSON(t, resp, &tokenResponse) 169 170 readToken = fmt.Sprintf("Bearer %s", tokenResponse.Token) 171 172 badToken = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadNotification) 173 req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL)) 174 req.Request.SetBasicAuth(user.Name, badToken) 175 MakeRequest(t, req, http.StatusUnauthorized) 176 177 testCase := func(scope auth_model.AccessTokenScope, expectedAuthStatus, expectedStatus int) { 178 token := getTokenForLoggedInUser(t, session, scope) 179 180 req := NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL)) 181 req.SetBasicAuth(user.Name, token) 182 183 resp := MakeRequest(t, req, expectedAuthStatus) 184 if expectedAuthStatus != http.StatusOK { 185 return 186 } 187 188 tokenResponse := &TokenResponse{} 189 DecodeJSON(t, resp, &tokenResponse) 190 191 assert.NotEmpty(t, tokenResponse.Token) 192 193 req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)). 194 AddTokenAuth(fmt.Sprintf("Bearer %s", tokenResponse.Token)) 195 MakeRequest(t, req, expectedStatus) 196 } 197 testCase(auth_model.AccessTokenScopeReadPackage, http.StatusOK, http.StatusOK) 198 testCase(auth_model.AccessTokenScopeAll, http.StatusOK, http.StatusOK) 199 testCase(auth_model.AccessTokenScopeReadNotification, http.StatusUnauthorized, http.StatusUnauthorized) 200 testCase(auth_model.AccessTokenScopeWritePackage, http.StatusOK, http.StatusOK) 201 }) 202 }) 203 204 t.Run("DetermineSupport", func(t *testing.T) { 205 defer tests.PrintCurrentTest(t)() 206 207 req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)). 208 AddTokenAuth(userToken) 209 resp := MakeRequest(t, req, http.StatusOK) 210 assert.Equal(t, "registry/2.0", resp.Header().Get("Docker-Distribution-Api-Version")) 211 212 req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)). 213 AddTokenAuth(readToken) 214 resp = MakeRequest(t, req, http.StatusOK) 215 assert.Equal(t, "registry/2.0", resp.Header().Get("Docker-Distribution-Api-Version")) 216 217 req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)). 218 AddTokenAuth(badToken) 219 MakeRequest(t, req, http.StatusUnauthorized) 220 }) 221 222 for _, image := range images { 223 t.Run(fmt.Sprintf("[Image:%s]", image), func(t *testing.T) { 224 url := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, image) 225 226 t.Run("UploadBlob/Monolithic", func(t *testing.T) { 227 defer tests.PrintCurrentTest(t)() 228 229 req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)). 230 AddTokenAuth(anonymousToken) 231 MakeRequest(t, req, http.StatusUnauthorized) 232 233 req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)). 234 AddTokenAuth(readToken) 235 MakeRequest(t, req, http.StatusUnauthorized) 236 237 req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)). 238 AddTokenAuth(badToken) 239 MakeRequest(t, req, http.StatusUnauthorized) 240 241 req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, unknownDigest), bytes.NewReader(blobContent)). 242 AddTokenAuth(userToken) 243 MakeRequest(t, req, http.StatusBadRequest) 244 245 req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, blobDigest), bytes.NewReader(blobContent)). 246 AddTokenAuth(userToken) 247 resp := MakeRequest(t, req, http.StatusCreated) 248 249 assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location")) 250 assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest")) 251 252 pv, err := packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, container_model.UploadVersion) 253 assert.NoError(t, err) 254 255 pfs, err := packages_model.GetFilesByVersionID(db.DefaultContext, pv.ID) 256 assert.NoError(t, err) 257 assert.Len(t, pfs, 1) 258 259 pb, err := packages_model.GetBlobByID(db.DefaultContext, pfs[0].BlobID) 260 assert.NoError(t, err) 261 assert.EqualValues(t, len(blobContent), pb.Size) 262 }) 263 264 t.Run("UploadBlob/Chunked", func(t *testing.T) { 265 defer tests.PrintCurrentTest(t)() 266 267 req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)). 268 AddTokenAuth(readToken) 269 MakeRequest(t, req, http.StatusUnauthorized) 270 271 req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)). 272 AddTokenAuth(badToken) 273 MakeRequest(t, req, http.StatusUnauthorized) 274 275 req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)). 276 AddTokenAuth(userToken) 277 resp := MakeRequest(t, req, http.StatusAccepted) 278 279 uuid := resp.Header().Get("Docker-Upload-Uuid") 280 assert.NotEmpty(t, uuid) 281 282 pbu, err := packages_model.GetBlobUploadByID(db.DefaultContext, uuid) 283 assert.NoError(t, err) 284 assert.EqualValues(t, 0, pbu.BytesReceived) 285 286 uploadURL := resp.Header().Get("Location") 287 assert.NotEmpty(t, uploadURL) 288 289 req = NewRequestWithBody(t, "PATCH", setting.AppURL+uploadURL[1:]+"000", bytes.NewReader(blobContent)). 290 AddTokenAuth(userToken) 291 MakeRequest(t, req, http.StatusNotFound) 292 293 req = NewRequestWithBody(t, "PATCH", setting.AppURL+uploadURL[1:], bytes.NewReader(blobContent)). 294 AddTokenAuth(userToken). 295 SetHeader("Content-Range", "1-10") 296 MakeRequest(t, req, http.StatusRequestedRangeNotSatisfiable) 297 298 contentRange := fmt.Sprintf("0-%d", len(blobContent)-1) 299 req.SetHeader("Content-Range", contentRange) 300 resp = MakeRequest(t, req, http.StatusAccepted) 301 302 assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid")) 303 assert.Equal(t, contentRange, resp.Header().Get("Range")) 304 305 uploadURL = resp.Header().Get("Location") 306 307 req = NewRequest(t, "GET", setting.AppURL+uploadURL[1:]). 308 AddTokenAuth(userToken) 309 resp = MakeRequest(t, req, http.StatusNoContent) 310 311 assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid")) 312 assert.Equal(t, fmt.Sprintf("0-%d", len(blobContent)), resp.Header().Get("Range")) 313 314 pbu, err = packages_model.GetBlobUploadByID(db.DefaultContext, uuid) 315 assert.NoError(t, err) 316 assert.EqualValues(t, len(blobContent), pbu.BytesReceived) 317 318 req = NewRequest(t, "PUT", fmt.Sprintf("%s?digest=%s", setting.AppURL+uploadURL[1:], blobDigest)). 319 AddTokenAuth(userToken) 320 resp = MakeRequest(t, req, http.StatusCreated) 321 322 assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location")) 323 assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest")) 324 325 t.Run("Cancel", func(t *testing.T) { 326 defer tests.PrintCurrentTest(t)() 327 328 req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)). 329 AddTokenAuth(userToken) 330 resp := MakeRequest(t, req, http.StatusAccepted) 331 332 uuid := resp.Header().Get("Docker-Upload-Uuid") 333 assert.NotEmpty(t, uuid) 334 335 uploadURL := resp.Header().Get("Location") 336 assert.NotEmpty(t, uploadURL) 337 338 req = NewRequest(t, "GET", setting.AppURL+uploadURL[1:]). 339 AddTokenAuth(userToken) 340 resp = MakeRequest(t, req, http.StatusNoContent) 341 342 assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid")) 343 assert.Equal(t, "0-0", resp.Header().Get("Range")) 344 345 req = NewRequest(t, "DELETE", setting.AppURL+uploadURL[1:]). 346 AddTokenAuth(userToken) 347 MakeRequest(t, req, http.StatusNoContent) 348 349 req = NewRequest(t, "GET", setting.AppURL+uploadURL[1:]). 350 AddTokenAuth(userToken) 351 MakeRequest(t, req, http.StatusNotFound) 352 }) 353 }) 354 355 t.Run("UploadBlob/Mount", func(t *testing.T) { 356 defer tests.PrintCurrentTest(t)() 357 358 privateBlobDigest := "sha256:6ccce4863b70f258d691f59609d31b4502e1ba5199942d3bc5d35d17a4ce771d" 359 req := NewRequestWithBody(t, "POST", fmt.Sprintf("%sv2/%s/%s/blobs/uploads?digest=%s", setting.AppURL, privateUser.Name, image, privateBlobDigest), strings.NewReader("gitea")). 360 AddBasicAuth(privateUser.Name) 361 MakeRequest(t, req, http.StatusCreated) 362 363 req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads?mount=%s", url, unknownDigest)). 364 AddTokenAuth(userToken) 365 MakeRequest(t, req, http.StatusAccepted) 366 367 req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads?mount=%s", url, privateBlobDigest)). 368 AddTokenAuth(userToken) 369 MakeRequest(t, req, http.StatusAccepted) 370 371 req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads?mount=%s", url, blobDigest)). 372 AddTokenAuth(userToken) 373 resp := MakeRequest(t, req, http.StatusCreated) 374 375 assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location")) 376 assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest")) 377 378 req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads?mount=%s&from=%s", url, unknownDigest, "unknown/image")). 379 AddTokenAuth(userToken) 380 MakeRequest(t, req, http.StatusAccepted) 381 382 req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads?mount=%s&from=%s/%s", url, blobDigest, user.Name, image)). 383 AddTokenAuth(userToken) 384 resp = MakeRequest(t, req, http.StatusCreated) 385 386 assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location")) 387 assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest")) 388 }) 389 390 for _, tag := range tags { 391 t.Run(fmt.Sprintf("[Tag:%s]", tag), func(t *testing.T) { 392 t.Run("UploadManifest", func(t *testing.T) { 393 defer tests.PrintCurrentTest(t)() 394 395 req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, configDigest), strings.NewReader(configContent)). 396 AddTokenAuth(userToken) 397 MakeRequest(t, req, http.StatusCreated) 398 399 req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)). 400 AddTokenAuth(anonymousToken). 401 SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") 402 MakeRequest(t, req, http.StatusUnauthorized) 403 404 req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)). 405 AddTokenAuth(userToken). 406 SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") 407 resp := MakeRequest(t, req, http.StatusCreated) 408 409 assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest")) 410 411 pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, tag) 412 assert.NoError(t, err) 413 414 pd, err := packages_model.GetPackageDescriptor(db.DefaultContext, pv) 415 assert.NoError(t, err) 416 assert.Nil(t, pd.SemVer) 417 assert.Equal(t, image, pd.Package.Name) 418 assert.Equal(t, tag, pd.Version.Version) 419 assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository)) 420 assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged)) 421 422 assert.IsType(t, &container_module.Metadata{}, pd.Metadata) 423 metadata := pd.Metadata.(*container_module.Metadata) 424 assert.Equal(t, container_module.TypeOCI, metadata.Type) 425 assert.Len(t, metadata.ImageLayers, 2) 426 assert.Empty(t, metadata.Manifests) 427 428 assert.Len(t, pd.Files, 3) 429 for _, pfd := range pd.Files { 430 switch pfd.File.Name { 431 case container_model.ManifestFilename: 432 assert.True(t, pfd.File.IsLead) 433 assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", pfd.Properties.GetByName(container_module.PropertyMediaType)) 434 assert.Equal(t, manifestDigest, pfd.Properties.GetByName(container_module.PropertyDigest)) 435 case strings.Replace(configDigest, ":", "_", 1): 436 assert.False(t, pfd.File.IsLead) 437 assert.Equal(t, "application/vnd.docker.container.image.v1+json", pfd.Properties.GetByName(container_module.PropertyMediaType)) 438 assert.Equal(t, configDigest, pfd.Properties.GetByName(container_module.PropertyDigest)) 439 case strings.Replace(blobDigest, ":", "_", 1): 440 assert.False(t, pfd.File.IsLead) 441 assert.Equal(t, "application/vnd.docker.image.rootfs.diff.tar.gzip", pfd.Properties.GetByName(container_module.PropertyMediaType)) 442 assert.Equal(t, blobDigest, pfd.Properties.GetByName(container_module.PropertyDigest)) 443 default: 444 assert.FailNow(t, "unknown file: %s", pfd.File.Name) 445 } 446 } 447 448 req = NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag)). 449 AddTokenAuth(userToken) 450 MakeRequest(t, req, http.StatusOK) 451 452 pv, err = packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, tag) 453 assert.NoError(t, err) 454 assert.EqualValues(t, 1, pv.DownloadCount) 455 456 // Overwrite existing tag should keep the download count 457 req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)). 458 AddTokenAuth(userToken). 459 SetHeader("Content-Type", oci.MediaTypeImageManifest) 460 MakeRequest(t, req, http.StatusCreated) 461 462 pv, err = packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, tag) 463 assert.NoError(t, err) 464 assert.EqualValues(t, 1, pv.DownloadCount) 465 }) 466 467 t.Run("HeadManifest", func(t *testing.T) { 468 defer tests.PrintCurrentTest(t)() 469 470 req := NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/unknown-tag", url)). 471 AddTokenAuth(userToken) 472 MakeRequest(t, req, http.StatusNotFound) 473 474 req = NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/%s", url, tag)). 475 AddTokenAuth(userToken) 476 resp := MakeRequest(t, req, http.StatusOK) 477 478 assert.Equal(t, fmt.Sprintf("%d", len(manifestContent)), resp.Header().Get("Content-Length")) 479 assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest")) 480 }) 481 482 t.Run("GetManifest", func(t *testing.T) { 483 defer tests.PrintCurrentTest(t)() 484 485 req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/unknown-tag", url)). 486 AddTokenAuth(userToken) 487 MakeRequest(t, req, http.StatusNotFound) 488 489 req = NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag)). 490 AddTokenAuth(userToken) 491 resp := MakeRequest(t, req, http.StatusOK) 492 493 assert.Equal(t, fmt.Sprintf("%d", len(manifestContent)), resp.Header().Get("Content-Length")) 494 assert.Equal(t, oci.MediaTypeImageManifest, resp.Header().Get("Content-Type")) 495 assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest")) 496 assert.Equal(t, manifestContent, resp.Body.String()) 497 }) 498 }) 499 } 500 501 t.Run("UploadUntaggedManifest", func(t *testing.T) { 502 defer tests.PrintCurrentTest(t)() 503 504 req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, untaggedManifestDigest), strings.NewReader(untaggedManifestContent)). 505 AddTokenAuth(userToken). 506 SetHeader("Content-Type", oci.MediaTypeImageManifest) 507 resp := MakeRequest(t, req, http.StatusCreated) 508 509 assert.Equal(t, untaggedManifestDigest, resp.Header().Get("Docker-Content-Digest")) 510 511 req = NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/%s", url, untaggedManifestDigest)). 512 AddTokenAuth(userToken) 513 resp = MakeRequest(t, req, http.StatusOK) 514 515 assert.Equal(t, fmt.Sprintf("%d", len(untaggedManifestContent)), resp.Header().Get("Content-Length")) 516 assert.Equal(t, untaggedManifestDigest, resp.Header().Get("Docker-Content-Digest")) 517 518 pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, untaggedManifestDigest) 519 assert.NoError(t, err) 520 521 pd, err := packages_model.GetPackageDescriptor(db.DefaultContext, pv) 522 assert.NoError(t, err) 523 assert.Nil(t, pd.SemVer) 524 assert.Equal(t, image, pd.Package.Name) 525 assert.Equal(t, untaggedManifestDigest, pd.Version.Version) 526 assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository)) 527 assert.False(t, has(pd.VersionProperties, container_module.PropertyManifestTagged)) 528 529 assert.IsType(t, &container_module.Metadata{}, pd.Metadata) 530 531 assert.Len(t, pd.Files, 3) 532 for _, pfd := range pd.Files { 533 if pfd.File.Name == container_model.ManifestFilename { 534 assert.True(t, pfd.File.IsLead) 535 assert.Equal(t, oci.MediaTypeImageManifest, pfd.Properties.GetByName(container_module.PropertyMediaType)) 536 assert.Equal(t, untaggedManifestDigest, pfd.Properties.GetByName(container_module.PropertyDigest)) 537 } 538 } 539 }) 540 541 t.Run("UploadIndexManifest", func(t *testing.T) { 542 defer tests.PrintCurrentTest(t)() 543 544 req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, multiTag), strings.NewReader(indexManifestContent)). 545 AddTokenAuth(userToken). 546 SetHeader("Content-Type", oci.MediaTypeImageIndex) 547 resp := MakeRequest(t, req, http.StatusCreated) 548 549 assert.Equal(t, indexManifestDigest, resp.Header().Get("Docker-Content-Digest")) 550 551 pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, multiTag) 552 assert.NoError(t, err) 553 554 pd, err := packages_model.GetPackageDescriptor(db.DefaultContext, pv) 555 assert.NoError(t, err) 556 assert.Nil(t, pd.SemVer) 557 assert.Equal(t, image, pd.Package.Name) 558 assert.Equal(t, multiTag, pd.Version.Version) 559 assert.ElementsMatch(t, []string{strings.ToLower(user.LowerName + "/" + image)}, getAllByName(pd.PackageProperties, container_module.PropertyRepository)) 560 assert.True(t, has(pd.VersionProperties, container_module.PropertyManifestTagged)) 561 562 assert.ElementsMatch(t, []string{manifestDigest, untaggedManifestDigest}, getAllByName(pd.VersionProperties, container_module.PropertyManifestReference)) 563 564 assert.IsType(t, &container_module.Metadata{}, pd.Metadata) 565 metadata := pd.Metadata.(*container_module.Metadata) 566 assert.Equal(t, container_module.TypeOCI, metadata.Type) 567 assert.Len(t, metadata.Manifests, 2) 568 assert.Condition(t, func() bool { 569 for _, m := range metadata.Manifests { 570 switch m.Platform { 571 case "linux/arm/v7": 572 assert.Equal(t, manifestDigest, m.Digest) 573 assert.EqualValues(t, 1524, m.Size) 574 case "linux/arm64/v8": 575 assert.Equal(t, untaggedManifestDigest, m.Digest) 576 assert.EqualValues(t, 1514, m.Size) 577 default: 578 return false 579 } 580 } 581 return true 582 }) 583 584 assert.Len(t, pd.Files, 1) 585 assert.True(t, pd.Files[0].File.IsLead) 586 assert.Equal(t, oci.MediaTypeImageIndex, pd.Files[0].Properties.GetByName(container_module.PropertyMediaType)) 587 assert.Equal(t, indexManifestDigest, pd.Files[0].Properties.GetByName(container_module.PropertyDigest)) 588 }) 589 590 t.Run("HeadBlob", func(t *testing.T) { 591 defer tests.PrintCurrentTest(t)() 592 593 req := NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, unknownDigest)). 594 AddTokenAuth(userToken) 595 MakeRequest(t, req, http.StatusNotFound) 596 597 req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). 598 AddTokenAuth(userToken) 599 resp := MakeRequest(t, req, http.StatusOK) 600 601 assert.Equal(t, fmt.Sprintf("%d", len(blobContent)), resp.Header().Get("Content-Length")) 602 assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest")) 603 604 req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). 605 AddTokenAuth(anonymousToken) 606 MakeRequest(t, req, http.StatusOK) 607 }) 608 609 t.Run("GetBlob", func(t *testing.T) { 610 defer tests.PrintCurrentTest(t)() 611 612 req := NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, unknownDigest)). 613 AddTokenAuth(userToken) 614 MakeRequest(t, req, http.StatusNotFound) 615 616 req = NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). 617 AddTokenAuth(userToken) 618 resp := MakeRequest(t, req, http.StatusOK) 619 620 assert.Equal(t, fmt.Sprintf("%d", len(blobContent)), resp.Header().Get("Content-Length")) 621 assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest")) 622 assert.Equal(t, blobContent, resp.Body.Bytes()) 623 }) 624 625 t.Run("GetTagList", func(t *testing.T) { 626 defer tests.PrintCurrentTest(t)() 627 628 cases := []struct { 629 URL string 630 ExpectedTags []string 631 ExpectedLink string 632 }{ 633 { 634 URL: fmt.Sprintf("%s/tags/list", url), 635 ExpectedTags: []string{"latest", "main", "multi"}, 636 ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image), 637 }, 638 { 639 URL: fmt.Sprintf("%s/tags/list?n=0", url), 640 ExpectedTags: []string{}, 641 ExpectedLink: "", 642 }, 643 { 644 URL: fmt.Sprintf("%s/tags/list?n=2", url), 645 ExpectedTags: []string{"latest", "main"}, 646 ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=2>; rel="next"`, user.Name, image), 647 }, 648 { 649 URL: fmt.Sprintf("%s/tags/list?last=main", url), 650 ExpectedTags: []string{"multi"}, 651 ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image), 652 }, 653 { 654 URL: fmt.Sprintf("%s/tags/list?n=1&last=latest", url), 655 ExpectedTags: []string{"main"}, 656 ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=1>; rel="next"`, user.Name, image), 657 }, 658 } 659 660 for _, c := range cases { 661 req := NewRequest(t, "GET", c.URL). 662 AddTokenAuth(userToken) 663 resp := MakeRequest(t, req, http.StatusOK) 664 665 type TagList struct { 666 Name string `json:"name"` 667 Tags []string `json:"tags"` 668 } 669 670 tagList := &TagList{} 671 DecodeJSON(t, resp, &tagList) 672 673 assert.Equal(t, user.Name+"/"+image, tagList.Name) 674 assert.Equal(t, c.ExpectedTags, tagList.Tags) 675 assert.Equal(t, c.ExpectedLink, resp.Header().Get("Link")) 676 } 677 678 req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s?type=container&q=%s", user.Name, image)). 679 AddTokenAuth(token) 680 resp := MakeRequest(t, req, http.StatusOK) 681 682 var apiPackages []*api.Package 683 DecodeJSON(t, resp, &apiPackages) 684 assert.Len(t, apiPackages, 4) // "latest", "main", "multi", "sha256:..." 685 }) 686 687 t.Run("Delete", func(t *testing.T) { 688 t.Run("Blob", func(t *testing.T) { 689 defer tests.PrintCurrentTest(t)() 690 691 req := NewRequest(t, "DELETE", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). 692 AddTokenAuth(userToken) 693 MakeRequest(t, req, http.StatusAccepted) 694 695 req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). 696 AddTokenAuth(userToken) 697 MakeRequest(t, req, http.StatusNotFound) 698 }) 699 700 t.Run("ManifestByDigest", func(t *testing.T) { 701 defer tests.PrintCurrentTest(t)() 702 703 req := NewRequest(t, "DELETE", fmt.Sprintf("%s/manifests/%s", url, untaggedManifestDigest)). 704 AddTokenAuth(userToken) 705 MakeRequest(t, req, http.StatusAccepted) 706 707 req = NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/%s", url, untaggedManifestDigest)). 708 AddTokenAuth(userToken) 709 MakeRequest(t, req, http.StatusNotFound) 710 }) 711 712 t.Run("ManifestByTag", func(t *testing.T) { 713 defer tests.PrintCurrentTest(t)() 714 715 req := NewRequest(t, "DELETE", fmt.Sprintf("%s/manifests/%s", url, multiTag)). 716 AddTokenAuth(userToken) 717 MakeRequest(t, req, http.StatusAccepted) 718 719 req = NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/%s", url, multiTag)). 720 AddTokenAuth(userToken) 721 MakeRequest(t, req, http.StatusNotFound) 722 }) 723 }) 724 }) 725 } 726 727 // https://github.com/go-gitea/gitea/issues/19586 728 t.Run("ParallelUpload", func(t *testing.T) { 729 defer tests.PrintCurrentTest(t)() 730 731 url := fmt.Sprintf("%sv2/%s/parallel", setting.AppURL, user.Name) 732 733 var wg sync.WaitGroup 734 for i := 0; i < 10; i++ { 735 wg.Add(1) 736 737 content := []byte{byte(i)} 738 digest := fmt.Sprintf("sha256:%x", sha256.Sum256(content)) 739 740 go func() { 741 defer wg.Done() 742 743 req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, digest), bytes.NewReader(content)). 744 AddTokenAuth(userToken) 745 resp := MakeRequest(t, req, http.StatusCreated) 746 747 assert.Equal(t, digest, resp.Header().Get("Docker-Content-Digest")) 748 }() 749 } 750 wg.Wait() 751 }) 752 753 t.Run("OwnerNameChange", func(t *testing.T) { 754 defer tests.PrintCurrentTest(t)() 755 756 checkCatalog := func(owner string) func(t *testing.T) { 757 return func(t *testing.T) { 758 defer tests.PrintCurrentTest(t)() 759 760 req := NewRequest(t, "GET", fmt.Sprintf("%sv2/_catalog", setting.AppURL)). 761 AddTokenAuth(userToken) 762 resp := MakeRequest(t, req, http.StatusOK) 763 764 type RepositoryList struct { 765 Repositories []string `json:"repositories"` 766 } 767 768 repoList := &RepositoryList{} 769 DecodeJSON(t, resp, &repoList) 770 771 assert.Len(t, repoList.Repositories, len(images)) 772 names := make([]string, 0, len(images)) 773 for _, image := range images { 774 names = append(names, strings.ToLower(owner+"/"+image)) 775 } 776 assert.ElementsMatch(t, names, repoList.Repositories) 777 } 778 } 779 780 t.Run(fmt.Sprintf("Catalog[%s]", user.LowerName), checkCatalog(user.LowerName)) 781 782 session := loginUser(t, user.Name) 783 784 newOwnerName := "newUsername" 785 786 req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ 787 "_csrf": GetCSRF(t, session, "/user/settings"), 788 "name": newOwnerName, 789 "email": "user2@example.com", 790 "language": "en-US", 791 }) 792 session.MakeRequest(t, req, http.StatusSeeOther) 793 794 t.Run(fmt.Sprintf("Catalog[%s]", newOwnerName), checkCatalog(newOwnerName)) 795 796 req = NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ 797 "_csrf": GetCSRF(t, session, "/user/settings"), 798 "name": user.Name, 799 "email": "user2@example.com", 800 "language": "en-US", 801 }) 802 session.MakeRequest(t, req, http.StatusSeeOther) 803 }) 804 }