You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

container.go 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package container
  4. import (
  5. "errors"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "net/url"
  10. "os"
  11. "regexp"
  12. "strconv"
  13. "strings"
  14. packages_model "code.gitea.io/gitea/models/packages"
  15. container_model "code.gitea.io/gitea/models/packages/container"
  16. user_model "code.gitea.io/gitea/models/user"
  17. "code.gitea.io/gitea/modules/json"
  18. "code.gitea.io/gitea/modules/log"
  19. packages_module "code.gitea.io/gitea/modules/packages"
  20. container_module "code.gitea.io/gitea/modules/packages/container"
  21. "code.gitea.io/gitea/modules/setting"
  22. "code.gitea.io/gitea/modules/util"
  23. "code.gitea.io/gitea/routers/api/packages/helper"
  24. "code.gitea.io/gitea/services/context"
  25. packages_service "code.gitea.io/gitea/services/packages"
  26. container_service "code.gitea.io/gitea/services/packages/container"
  27. digest "github.com/opencontainers/go-digest"
  28. )
  29. // maximum size of a container manifest
  30. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
  31. const maxManifestSize = 10 * 1024 * 1024
  32. var (
  33. imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`)
  34. referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`)
  35. )
  36. type containerHeaders struct {
  37. Status int
  38. ContentDigest string
  39. UploadUUID string
  40. Range string
  41. Location string
  42. ContentType string
  43. ContentLength int64
  44. }
  45. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers
  46. func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
  47. if h.Location != "" {
  48. resp.Header().Set("Location", h.Location)
  49. }
  50. if h.Range != "" {
  51. resp.Header().Set("Range", h.Range)
  52. }
  53. if h.ContentType != "" {
  54. resp.Header().Set("Content-Type", h.ContentType)
  55. }
  56. if h.ContentLength != 0 {
  57. resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
  58. }
  59. if h.UploadUUID != "" {
  60. resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
  61. }
  62. if h.ContentDigest != "" {
  63. resp.Header().Set("Docker-Content-Digest", h.ContentDigest)
  64. resp.Header().Set("ETag", fmt.Sprintf(`"%s"`, h.ContentDigest))
  65. }
  66. resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
  67. resp.WriteHeader(h.Status)
  68. }
  69. func jsonResponse(ctx *context.Context, status int, obj any) {
  70. setResponseHeaders(ctx.Resp, &containerHeaders{
  71. Status: status,
  72. ContentType: "application/json",
  73. })
  74. if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
  75. log.Error("JSON encode: %v", err)
  76. }
  77. }
  78. func apiError(ctx *context.Context, status int, err error) {
  79. helper.LogAndProcessError(ctx, status, err, func(message string) {
  80. setResponseHeaders(ctx.Resp, &containerHeaders{
  81. Status: status,
  82. })
  83. })
  84. }
  85. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes
  86. func apiErrorDefined(ctx *context.Context, err *namedError) {
  87. type ContainerError struct {
  88. Code string `json:"code"`
  89. Message string `json:"message"`
  90. }
  91. type ContainerErrors struct {
  92. Errors []ContainerError `json:"errors"`
  93. }
  94. jsonResponse(ctx, err.StatusCode, ContainerErrors{
  95. Errors: []ContainerError{
  96. {
  97. Code: err.Code,
  98. Message: err.Message,
  99. },
  100. },
  101. })
  102. }
  103. func apiUnauthorizedError(ctx *context.Context) {
  104. ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`)
  105. apiErrorDefined(ctx, errUnauthorized)
  106. }
  107. // ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
  108. func ReqContainerAccess(ctx *context.Context) {
  109. if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
  110. apiUnauthorizedError(ctx)
  111. }
  112. }
  113. // VerifyImageName is a middleware which checks if the image name is allowed
  114. func VerifyImageName(ctx *context.Context) {
  115. if !imageNamePattern.MatchString(ctx.Params("image")) {
  116. apiErrorDefined(ctx, errNameInvalid)
  117. }
  118. }
  119. // DetermineSupport is used to test if the registry supports OCI
  120. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support
  121. func DetermineSupport(ctx *context.Context) {
  122. setResponseHeaders(ctx.Resp, &containerHeaders{
  123. Status: http.StatusOK,
  124. })
  125. }
  126. // Authenticate creates a token for the current user
  127. // If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled.
  128. func Authenticate(ctx *context.Context) {
  129. u := ctx.Doer
  130. if u == nil {
  131. if setting.Service.RequireSignInView {
  132. apiUnauthorizedError(ctx)
  133. return
  134. }
  135. u = user_model.NewGhostUser()
  136. }
  137. token, err := packages_service.CreateAuthorizationToken(u)
  138. if err != nil {
  139. apiError(ctx, http.StatusInternalServerError, err)
  140. return
  141. }
  142. ctx.JSON(http.StatusOK, map[string]string{
  143. "token": token,
  144. })
  145. }
  146. // https://distribution.github.io/distribution/spec/auth/oauth/
  147. func AuthenticateNotImplemented(ctx *context.Context) {
  148. // This optional endpoint can be used to authenticate a client.
  149. // It must implement the specification described in:
  150. // https://datatracker.ietf.org/doc/html/rfc6749
  151. // https://distribution.github.io/distribution/spec/auth/oauth/
  152. // Purpose of this stub is to respond with 404 Not Found instead of 405 Method Not Allowed.
  153. ctx.Status(http.StatusNotFound)
  154. }
  155. // https://docs.docker.com/registry/spec/api/#listing-repositories
  156. func GetRepositoryList(ctx *context.Context) {
  157. n := ctx.FormInt("n")
  158. if n <= 0 || n > 100 {
  159. n = 100
  160. }
  161. last := ctx.FormTrim("last")
  162. repositories, err := container_model.GetRepositories(ctx, ctx.Doer, n, last)
  163. if err != nil {
  164. apiError(ctx, http.StatusInternalServerError, err)
  165. return
  166. }
  167. type RepositoryList struct {
  168. Repositories []string `json:"repositories"`
  169. }
  170. if len(repositories) == n {
  171. v := url.Values{}
  172. if n > 0 {
  173. v.Add("n", strconv.Itoa(n))
  174. }
  175. v.Add("last", repositories[len(repositories)-1])
  176. ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/_catalog?%s>; rel="next"`, v.Encode()))
  177. }
  178. jsonResponse(ctx, http.StatusOK, RepositoryList{
  179. Repositories: repositories,
  180. })
  181. }
  182. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository
  183. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
  184. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
  185. func InitiateUploadBlob(ctx *context.Context) {
  186. image := ctx.Params("image")
  187. mount := ctx.FormTrim("mount")
  188. from := ctx.FormTrim("from")
  189. if mount != "" {
  190. blob, _ := workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
  191. Repository: from,
  192. Digest: mount,
  193. })
  194. if blob != nil {
  195. accessible, err := packages_model.IsBlobAccessibleForUser(ctx, blob.Blob.ID, ctx.Doer)
  196. if err != nil {
  197. apiError(ctx, http.StatusInternalServerError, err)
  198. return
  199. }
  200. if accessible {
  201. if err := mountBlob(ctx, &packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}, blob.Blob); err != nil {
  202. apiError(ctx, http.StatusInternalServerError, err)
  203. return
  204. }
  205. setResponseHeaders(ctx.Resp, &containerHeaders{
  206. Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount),
  207. ContentDigest: mount,
  208. Status: http.StatusCreated,
  209. })
  210. return
  211. }
  212. }
  213. }
  214. digest := ctx.FormTrim("digest")
  215. if digest != "" {
  216. buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
  217. if err != nil {
  218. apiError(ctx, http.StatusInternalServerError, err)
  219. return
  220. }
  221. defer buf.Close()
  222. if digest != digestFromHashSummer(buf) {
  223. apiErrorDefined(ctx, errDigestInvalid)
  224. return
  225. }
  226. if _, err := saveAsPackageBlob(ctx,
  227. buf,
  228. &packages_service.PackageCreationInfo{
  229. PackageInfo: packages_service.PackageInfo{
  230. Owner: ctx.Package.Owner,
  231. Name: image,
  232. },
  233. Creator: ctx.Doer,
  234. },
  235. ); err != nil {
  236. switch err {
  237. case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
  238. apiError(ctx, http.StatusForbidden, err)
  239. default:
  240. apiError(ctx, http.StatusInternalServerError, err)
  241. }
  242. return
  243. }
  244. setResponseHeaders(ctx.Resp, &containerHeaders{
  245. Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
  246. ContentDigest: digest,
  247. Status: http.StatusCreated,
  248. })
  249. return
  250. }
  251. upload, err := packages_model.CreateBlobUpload(ctx)
  252. if err != nil {
  253. apiError(ctx, http.StatusInternalServerError, err)
  254. return
  255. }
  256. setResponseHeaders(ctx.Resp, &containerHeaders{
  257. Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
  258. Range: "0-0",
  259. UploadUUID: upload.ID,
  260. Status: http.StatusAccepted,
  261. })
  262. }
  263. // https://docs.docker.com/registry/spec/api/#get-blob-upload
  264. func GetUploadBlob(ctx *context.Context) {
  265. uuid := ctx.Params("uuid")
  266. upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
  267. if err != nil {
  268. if err == packages_model.ErrPackageBlobUploadNotExist {
  269. apiErrorDefined(ctx, errBlobUploadUnknown)
  270. } else {
  271. apiError(ctx, http.StatusInternalServerError, err)
  272. }
  273. return
  274. }
  275. setResponseHeaders(ctx.Resp, &containerHeaders{
  276. Range: fmt.Sprintf("0-%d", upload.BytesReceived),
  277. UploadUUID: upload.ID,
  278. Status: http.StatusNoContent,
  279. })
  280. }
  281. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
  282. func UploadBlob(ctx *context.Context) {
  283. image := ctx.Params("image")
  284. uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid"))
  285. if err != nil {
  286. if err == packages_model.ErrPackageBlobUploadNotExist {
  287. apiErrorDefined(ctx, errBlobUploadUnknown)
  288. } else {
  289. apiError(ctx, http.StatusInternalServerError, err)
  290. }
  291. return
  292. }
  293. defer uploader.Close()
  294. contentRange := ctx.Req.Header.Get("Content-Range")
  295. if contentRange != "" {
  296. start, end := 0, 0
  297. if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil {
  298. apiErrorDefined(ctx, errBlobUploadInvalid)
  299. return
  300. }
  301. if int64(start) != uploader.Size() {
  302. apiErrorDefined(ctx, errBlobUploadInvalid.WithStatusCode(http.StatusRequestedRangeNotSatisfiable))
  303. return
  304. }
  305. } else if uploader.Size() != 0 {
  306. apiErrorDefined(ctx, errBlobUploadInvalid.WithMessage("Stream uploads after first write are not allowed"))
  307. return
  308. }
  309. if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
  310. apiError(ctx, http.StatusInternalServerError, err)
  311. return
  312. }
  313. setResponseHeaders(ctx.Resp, &containerHeaders{
  314. Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID),
  315. Range: fmt.Sprintf("0-%d", uploader.Size()-1),
  316. UploadUUID: uploader.ID,
  317. Status: http.StatusAccepted,
  318. })
  319. }
  320. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
  321. func EndUploadBlob(ctx *context.Context) {
  322. image := ctx.Params("image")
  323. digest := ctx.FormTrim("digest")
  324. if digest == "" {
  325. apiErrorDefined(ctx, errDigestInvalid)
  326. return
  327. }
  328. uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid"))
  329. if err != nil {
  330. if err == packages_model.ErrPackageBlobUploadNotExist {
  331. apiErrorDefined(ctx, errBlobUploadUnknown)
  332. } else {
  333. apiError(ctx, http.StatusInternalServerError, err)
  334. }
  335. return
  336. }
  337. doClose := true
  338. defer func() {
  339. if doClose {
  340. uploader.Close()
  341. }
  342. }()
  343. if ctx.Req.Body != nil {
  344. if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
  345. apiError(ctx, http.StatusInternalServerError, err)
  346. return
  347. }
  348. }
  349. if digest != digestFromHashSummer(uploader) {
  350. apiErrorDefined(ctx, errDigestInvalid)
  351. return
  352. }
  353. if _, err := saveAsPackageBlob(ctx,
  354. uploader,
  355. &packages_service.PackageCreationInfo{
  356. PackageInfo: packages_service.PackageInfo{
  357. Owner: ctx.Package.Owner,
  358. Name: image,
  359. },
  360. Creator: ctx.Doer,
  361. },
  362. ); err != nil {
  363. switch err {
  364. case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
  365. apiError(ctx, http.StatusForbidden, err)
  366. default:
  367. apiError(ctx, http.StatusInternalServerError, err)
  368. }
  369. return
  370. }
  371. if err := uploader.Close(); err != nil {
  372. apiError(ctx, http.StatusInternalServerError, err)
  373. return
  374. }
  375. doClose = false
  376. if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil {
  377. apiError(ctx, http.StatusInternalServerError, err)
  378. return
  379. }
  380. setResponseHeaders(ctx.Resp, &containerHeaders{
  381. Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
  382. ContentDigest: digest,
  383. Status: http.StatusCreated,
  384. })
  385. }
  386. // https://docs.docker.com/registry/spec/api/#delete-blob-upload
  387. func CancelUploadBlob(ctx *context.Context) {
  388. uuid := ctx.Params("uuid")
  389. _, err := packages_model.GetBlobUploadByID(ctx, uuid)
  390. if err != nil {
  391. if err == packages_model.ErrPackageBlobUploadNotExist {
  392. apiErrorDefined(ctx, errBlobUploadUnknown)
  393. } else {
  394. apiError(ctx, http.StatusInternalServerError, err)
  395. }
  396. return
  397. }
  398. if err := container_service.RemoveBlobUploadByID(ctx, uuid); err != nil {
  399. apiError(ctx, http.StatusInternalServerError, err)
  400. return
  401. }
  402. setResponseHeaders(ctx.Resp, &containerHeaders{
  403. Status: http.StatusNoContent,
  404. })
  405. }
  406. func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
  407. d := ctx.Params("digest")
  408. if digest.Digest(d).Validate() != nil {
  409. return nil, container_model.ErrContainerBlobNotExist
  410. }
  411. return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
  412. OwnerID: ctx.Package.Owner.ID,
  413. Image: ctx.Params("image"),
  414. Digest: d,
  415. })
  416. }
  417. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
  418. func HeadBlob(ctx *context.Context) {
  419. blob, err := getBlobFromContext(ctx)
  420. if err != nil {
  421. if err == container_model.ErrContainerBlobNotExist {
  422. apiErrorDefined(ctx, errBlobUnknown)
  423. } else {
  424. apiError(ctx, http.StatusInternalServerError, err)
  425. }
  426. return
  427. }
  428. setResponseHeaders(ctx.Resp, &containerHeaders{
  429. ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
  430. ContentLength: blob.Blob.Size,
  431. Status: http.StatusOK,
  432. })
  433. }
  434. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-blobs
  435. func GetBlob(ctx *context.Context) {
  436. blob, err := getBlobFromContext(ctx)
  437. if err != nil {
  438. if err == container_model.ErrContainerBlobNotExist {
  439. apiErrorDefined(ctx, errBlobUnknown)
  440. } else {
  441. apiError(ctx, http.StatusInternalServerError, err)
  442. }
  443. return
  444. }
  445. serveBlob(ctx, blob)
  446. }
  447. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs
  448. func DeleteBlob(ctx *context.Context) {
  449. d := ctx.Params("digest")
  450. if digest.Digest(d).Validate() != nil {
  451. apiErrorDefined(ctx, errBlobUnknown)
  452. return
  453. }
  454. if err := deleteBlob(ctx, ctx.Package.Owner.ID, ctx.Params("image"), d); err != nil {
  455. apiError(ctx, http.StatusInternalServerError, err)
  456. return
  457. }
  458. setResponseHeaders(ctx.Resp, &containerHeaders{
  459. Status: http.StatusAccepted,
  460. })
  461. }
  462. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
  463. func UploadManifest(ctx *context.Context) {
  464. reference := ctx.Params("reference")
  465. mci := &manifestCreationInfo{
  466. MediaType: ctx.Req.Header.Get("Content-Type"),
  467. Owner: ctx.Package.Owner,
  468. Creator: ctx.Doer,
  469. Image: ctx.Params("image"),
  470. Reference: reference,
  471. IsTagged: digest.Digest(reference).Validate() != nil,
  472. }
  473. if mci.IsTagged && !referencePattern.MatchString(reference) {
  474. apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid"))
  475. return
  476. }
  477. maxSize := maxManifestSize + 1
  478. buf, err := packages_module.CreateHashedBufferFromReaderWithSize(&io.LimitedReader{R: ctx.Req.Body, N: int64(maxSize)}, maxSize)
  479. if err != nil {
  480. apiError(ctx, http.StatusInternalServerError, err)
  481. return
  482. }
  483. defer buf.Close()
  484. if buf.Size() > maxManifestSize {
  485. apiErrorDefined(ctx, errManifestInvalid.WithMessage("Manifest exceeds maximum size").WithStatusCode(http.StatusRequestEntityTooLarge))
  486. return
  487. }
  488. digest, err := processManifest(ctx, mci, buf)
  489. if err != nil {
  490. var namedError *namedError
  491. if errors.As(err, &namedError) {
  492. apiErrorDefined(ctx, namedError)
  493. } else if errors.Is(err, container_model.ErrContainerBlobNotExist) {
  494. apiErrorDefined(ctx, errBlobUnknown)
  495. } else {
  496. switch err {
  497. case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
  498. apiError(ctx, http.StatusForbidden, err)
  499. default:
  500. apiError(ctx, http.StatusInternalServerError, err)
  501. }
  502. }
  503. return
  504. }
  505. setResponseHeaders(ctx.Resp, &containerHeaders{
  506. Location: fmt.Sprintf("/v2/%s/%s/manifests/%s", ctx.Package.Owner.LowerName, mci.Image, reference),
  507. ContentDigest: digest,
  508. Status: http.StatusCreated,
  509. })
  510. }
  511. func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) {
  512. reference := ctx.Params("reference")
  513. opts := &container_model.BlobSearchOptions{
  514. OwnerID: ctx.Package.Owner.ID,
  515. Image: ctx.Params("image"),
  516. IsManifest: true,
  517. }
  518. if digest.Digest(reference).Validate() == nil {
  519. opts.Digest = reference
  520. } else if referencePattern.MatchString(reference) {
  521. opts.Tag = reference
  522. } else {
  523. return nil, container_model.ErrContainerBlobNotExist
  524. }
  525. return opts, nil
  526. }
  527. func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
  528. opts, err := getBlobSearchOptionsFromContext(ctx)
  529. if err != nil {
  530. return nil, err
  531. }
  532. return workaroundGetContainerBlob(ctx, opts)
  533. }
  534. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
  535. func HeadManifest(ctx *context.Context) {
  536. manifest, err := getManifestFromContext(ctx)
  537. if err != nil {
  538. if err == container_model.ErrContainerBlobNotExist {
  539. apiErrorDefined(ctx, errManifestUnknown)
  540. } else {
  541. apiError(ctx, http.StatusInternalServerError, err)
  542. }
  543. return
  544. }
  545. setResponseHeaders(ctx.Resp, &containerHeaders{
  546. ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest),
  547. ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType),
  548. ContentLength: manifest.Blob.Size,
  549. Status: http.StatusOK,
  550. })
  551. }
  552. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
  553. func GetManifest(ctx *context.Context) {
  554. manifest, err := getManifestFromContext(ctx)
  555. if err != nil {
  556. if err == container_model.ErrContainerBlobNotExist {
  557. apiErrorDefined(ctx, errManifestUnknown)
  558. } else {
  559. apiError(ctx, http.StatusInternalServerError, err)
  560. }
  561. return
  562. }
  563. serveBlob(ctx, manifest)
  564. }
  565. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags
  566. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-manifests
  567. func DeleteManifest(ctx *context.Context) {
  568. opts, err := getBlobSearchOptionsFromContext(ctx)
  569. if err != nil {
  570. apiErrorDefined(ctx, errManifestUnknown)
  571. return
  572. }
  573. pvs, err := container_model.GetManifestVersions(ctx, opts)
  574. if err != nil {
  575. apiError(ctx, http.StatusInternalServerError, err)
  576. return
  577. }
  578. if len(pvs) == 0 {
  579. apiErrorDefined(ctx, errManifestUnknown)
  580. return
  581. }
  582. for _, pv := range pvs {
  583. if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
  584. apiError(ctx, http.StatusInternalServerError, err)
  585. return
  586. }
  587. }
  588. setResponseHeaders(ctx.Resp, &containerHeaders{
  589. Status: http.StatusAccepted,
  590. })
  591. }
  592. func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
  593. s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob)
  594. if err != nil {
  595. apiError(ctx, http.StatusInternalServerError, err)
  596. return
  597. }
  598. headers := &containerHeaders{
  599. ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest),
  600. ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType),
  601. ContentLength: pfd.Blob.Size,
  602. Status: http.StatusOK,
  603. }
  604. if u != nil {
  605. headers.Status = http.StatusTemporaryRedirect
  606. headers.Location = u.String()
  607. setResponseHeaders(ctx.Resp, headers)
  608. return
  609. }
  610. defer s.Close()
  611. setResponseHeaders(ctx.Resp, headers)
  612. if _, err := io.Copy(ctx.Resp, s); err != nil {
  613. log.Error("Error whilst copying content to response: %v", err)
  614. }
  615. }
  616. // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery
  617. func GetTagList(ctx *context.Context) {
  618. image := ctx.Params("image")
  619. if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil {
  620. if err == packages_model.ErrPackageNotExist {
  621. apiErrorDefined(ctx, errNameUnknown)
  622. } else {
  623. apiError(ctx, http.StatusInternalServerError, err)
  624. }
  625. return
  626. }
  627. n := -1
  628. if ctx.FormTrim("n") != "" {
  629. n = ctx.FormInt("n")
  630. }
  631. last := ctx.FormTrim("last")
  632. tags, err := container_model.GetImageTags(ctx, ctx.Package.Owner.ID, image, n, last)
  633. if err != nil {
  634. apiError(ctx, http.StatusInternalServerError, err)
  635. return
  636. }
  637. type TagList struct {
  638. Name string `json:"name"`
  639. Tags []string `json:"tags"`
  640. }
  641. if len(tags) > 0 {
  642. v := url.Values{}
  643. if n > 0 {
  644. v.Add("n", strconv.Itoa(n))
  645. }
  646. v.Add("last", tags[len(tags)-1])
  647. ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/%s/%s/tags/list?%s>; rel="next"`, ctx.Package.Owner.LowerName, image, v.Encode()))
  648. }
  649. jsonResponse(ctx, http.StatusOK, TagList{
  650. Name: strings.ToLower(ctx.Package.Owner.LowerName + "/" + image),
  651. Tags: tags,
  652. })
  653. }
  654. // FIXME: Workaround to be removed in v1.20
  655. // https://github.com/go-gitea/gitea/issues/19586
  656. func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) {
  657. blob, err := container_model.GetContainerBlob(ctx, opts)
  658. if err != nil {
  659. return nil, err
  660. }
  661. err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(blob.Blob.HashSHA256))
  662. if err != nil {
  663. if errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist) {
  664. log.Debug("Package registry inconsistent: blob %s does not exist on file system", blob.Blob.HashSHA256)
  665. return nil, container_model.ErrContainerBlobNotExist
  666. }
  667. return nil, err
  668. }
  669. return blob, nil
  670. }