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 21KB

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