gitea源码

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