manifest.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. package distribution
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "strings"
  8. "github.com/containerd/containerd/content"
  9. cerrdefs "github.com/containerd/containerd/errdefs"
  10. "github.com/containerd/containerd/remotes"
  11. "github.com/containerd/log"
  12. "github.com/distribution/reference"
  13. "github.com/docker/distribution"
  14. "github.com/docker/distribution/manifest/manifestlist"
  15. "github.com/docker/distribution/manifest/schema1"
  16. "github.com/docker/distribution/manifest/schema2"
  17. "github.com/docker/docker/registry"
  18. "github.com/opencontainers/go-digest"
  19. ocispec "github.com/opencontainers/image-spec/specs-go/v1"
  20. "github.com/pkg/errors"
  21. )
  22. // labelDistributionSource describes the source blob comes from.
  23. const labelDistributionSource = "containerd.io/distribution.source"
  24. // This is used by manifestStore to pare down the requirements to implement a
  25. // full distribution.ManifestService, since `Get` is all we use here.
  26. type manifestGetter interface {
  27. Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error)
  28. Exists(ctx context.Context, dgst digest.Digest) (bool, error)
  29. }
  30. type manifestStore struct {
  31. local ContentStore
  32. remote manifestGetter
  33. }
  34. // ContentStore is the interface used to persist registry blobs
  35. //
  36. // Currently this is only used to persist manifests and manifest lists.
  37. // It is exported because `distribution.Pull` takes one as an argument.
  38. type ContentStore interface {
  39. content.Ingester
  40. content.Provider
  41. Info(ctx context.Context, dgst digest.Digest) (content.Info, error)
  42. Abort(ctx context.Context, ref string) error
  43. Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error)
  44. }
  45. func makeDistributionSourceLabel(ref reference.Named) (string, string) {
  46. domain := reference.Domain(ref)
  47. if domain == "" {
  48. domain = registry.DefaultNamespace
  49. }
  50. repo := reference.Path(ref)
  51. return fmt.Sprintf("%s.%s", labelDistributionSource, domain), repo
  52. }
  53. // Taken from https://github.com/containerd/containerd/blob/e079e4a155c86f07bbd602fe6753ecacc78198c2/remotes/docker/handler.go#L84-L108
  54. func appendDistributionSourceLabel(originLabel, repo string) string {
  55. repos := []string{}
  56. if originLabel != "" {
  57. repos = strings.Split(originLabel, ",")
  58. }
  59. repos = append(repos, repo)
  60. // use empty string to present duplicate items
  61. for i := 1; i < len(repos); i++ {
  62. tmp, j := repos[i], i-1
  63. for ; j >= 0 && repos[j] >= tmp; j-- {
  64. if repos[j] == tmp {
  65. tmp = ""
  66. }
  67. repos[j+1] = repos[j]
  68. }
  69. repos[j+1] = tmp
  70. }
  71. i := 0
  72. for ; i < len(repos) && repos[i] == ""; i++ {
  73. }
  74. return strings.Join(repos[i:], ",")
  75. }
  76. func hasDistributionSource(label, repo string) bool {
  77. sources := strings.Split(label, ",")
  78. for _, s := range sources {
  79. if s == repo {
  80. return true
  81. }
  82. }
  83. return false
  84. }
  85. func (m *manifestStore) getLocal(ctx context.Context, desc ocispec.Descriptor, ref reference.Named) (distribution.Manifest, error) {
  86. ra, err := m.local.ReaderAt(ctx, desc)
  87. if err != nil {
  88. return nil, errors.Wrap(err, "error getting content store reader")
  89. }
  90. defer ra.Close()
  91. distKey, distRepo := makeDistributionSourceLabel(ref)
  92. info, err := m.local.Info(ctx, desc.Digest)
  93. if err != nil {
  94. return nil, errors.Wrap(err, "error getting content info")
  95. }
  96. if _, ok := ref.(reference.Canonical); ok {
  97. // Since this is specified by digest...
  98. // We know we have the content locally, we need to check if we've seen this content at the specified repository before.
  99. // If we have, we can just return the manifest from the local content store.
  100. // If we haven't, we need to check the remote repository to see if it has the content, otherwise we can end up returning
  101. // a manifest that has never even existed in the remote before.
  102. if !hasDistributionSource(info.Labels[distKey], distRepo) {
  103. log.G(ctx).WithField("ref", ref).Debug("found manifest but no mataching source repo is listed, checking with remote")
  104. exists, err := m.remote.Exists(ctx, desc.Digest)
  105. if err != nil {
  106. return nil, errors.Wrap(err, "error checking if remote exists")
  107. }
  108. if !exists {
  109. return nil, errors.Wrapf(cerrdefs.ErrNotFound, "manifest %v not found", desc.Digest)
  110. }
  111. }
  112. }
  113. // Update the distribution sources since we now know the content exists in the remote.
  114. if info.Labels == nil {
  115. info.Labels = map[string]string{}
  116. }
  117. info.Labels[distKey] = appendDistributionSourceLabel(info.Labels[distKey], distRepo)
  118. if _, err := m.local.Update(ctx, info, "labels."+distKey); err != nil {
  119. log.G(ctx).WithError(err).WithField("ref", ref).Warn("Could not update content distribution source")
  120. }
  121. r := io.NewSectionReader(ra, 0, ra.Size())
  122. data, err := io.ReadAll(r)
  123. if err != nil {
  124. return nil, errors.Wrap(err, "error reading manifest from content store")
  125. }
  126. manifest, _, err := distribution.UnmarshalManifest(desc.MediaType, data)
  127. if err != nil {
  128. return nil, errors.Wrap(err, "error unmarshaling manifest from content store")
  129. }
  130. return manifest, nil
  131. }
  132. func (m *manifestStore) getMediaType(ctx context.Context, desc ocispec.Descriptor) (string, error) {
  133. ra, err := m.local.ReaderAt(ctx, desc)
  134. if err != nil {
  135. return "", errors.Wrap(err, "error getting reader to detect media type")
  136. }
  137. defer ra.Close()
  138. mt, err := detectManifestMediaType(ra)
  139. if err != nil {
  140. return "", errors.Wrap(err, "error detecting media type")
  141. }
  142. return mt, nil
  143. }
  144. func (m *manifestStore) Get(ctx context.Context, desc ocispec.Descriptor, ref reference.Named) (distribution.Manifest, error) {
  145. l := log.G(ctx)
  146. if desc.MediaType == "" {
  147. // When pulling by digest we will not have the media type on the
  148. // descriptor since we have not made a request to the registry yet
  149. //
  150. // We already have the digest, so we only lookup locally... by digest.
  151. //
  152. // Let's try to detect the media type so we can have a good ref key
  153. // here. We may not even have the content locally, and this is fine, but
  154. // if we do we should determine that.
  155. mt, err := m.getMediaType(ctx, desc)
  156. if err != nil && !cerrdefs.IsNotFound(err) {
  157. l.WithError(err).Warn("Error looking up media type of content")
  158. }
  159. desc.MediaType = mt
  160. }
  161. key := remotes.MakeRefKey(ctx, desc)
  162. // Here we open a writer to the requested content. This both gives us a
  163. // reference to write to if indeed we need to persist it and increments the
  164. // ref count on the content.
  165. w, err := m.local.Writer(ctx, content.WithDescriptor(desc), content.WithRef(key))
  166. if err != nil {
  167. if cerrdefs.IsAlreadyExists(err) {
  168. var manifest distribution.Manifest
  169. if manifest, err = m.getLocal(ctx, desc, ref); err == nil {
  170. return manifest, nil
  171. }
  172. }
  173. // always fallback to the remote if there is an error with the local store
  174. }
  175. if w != nil {
  176. defer w.Close()
  177. }
  178. l.WithError(err).Debug("Fetching manifest from remote")
  179. manifest, err := m.remote.Get(ctx, desc.Digest)
  180. if err != nil {
  181. if err := m.local.Abort(ctx, key); err != nil {
  182. l.WithError(err).Warn("Error while attempting to abort content ingest")
  183. }
  184. return nil, err
  185. }
  186. if w != nil {
  187. // if `w` is nil here, something happened with the content store, so don't bother trying to persist.
  188. if err := m.Put(ctx, manifest, desc, w, ref); err != nil {
  189. if err := m.local.Abort(ctx, key); err != nil {
  190. l.WithError(err).Warn("error aborting content ingest")
  191. }
  192. l.WithError(err).Warn("Error persisting manifest")
  193. }
  194. }
  195. return manifest, nil
  196. }
  197. func (m *manifestStore) Put(ctx context.Context, manifest distribution.Manifest, desc ocispec.Descriptor, w content.Writer, ref reference.Named) error {
  198. mt, payload, err := manifest.Payload()
  199. if err != nil {
  200. return err
  201. }
  202. desc.Size = int64(len(payload))
  203. desc.MediaType = mt
  204. if _, err = w.Write(payload); err != nil {
  205. return errors.Wrap(err, "error writing manifest to content store")
  206. }
  207. distKey, distSource := makeDistributionSourceLabel(ref)
  208. if err := w.Commit(ctx, desc.Size, desc.Digest, content.WithLabels(map[string]string{
  209. distKey: distSource,
  210. })); err != nil {
  211. return errors.Wrap(err, "error committing manifest to content store")
  212. }
  213. return nil
  214. }
  215. func detectManifestMediaType(ra content.ReaderAt) (string, error) {
  216. dt := make([]byte, ra.Size())
  217. if _, err := ra.ReadAt(dt, 0); err != nil {
  218. return "", err
  219. }
  220. return detectManifestBlobMediaType(dt)
  221. }
  222. // This is used when the manifest store does not know the media type of a sha it
  223. // was told to get. This would currently only happen when pulling by digest.
  224. // The media type is needed so the blob can be unmarshalled properly.
  225. func detectManifestBlobMediaType(dt []byte) (string, error) {
  226. var mfst struct {
  227. MediaType string `json:"mediaType"`
  228. Manifests json.RawMessage `json:"manifests"` // oci index, manifest list
  229. Config json.RawMessage `json:"config"` // schema2 Manifest
  230. Layers json.RawMessage `json:"layers"` // schema2 Manifest
  231. FSLayers json.RawMessage `json:"fsLayers"` // schema1 Manifest
  232. }
  233. if err := json.Unmarshal(dt, &mfst); err != nil {
  234. return "", err
  235. }
  236. // We may have a media type specified in the json, in which case that should be used.
  237. // Docker types should generally have a media type set.
  238. // OCI (golang) types do not have a `mediaType` defined, and it is optional in the spec.
  239. //
  240. // `distribution.UnmarshalManifest`, which is used to unmarshal this for real, checks these media type values.
  241. // If the specified media type does not match it will error, and in some cases (docker media types) it is required.
  242. // So pretty much if we don't have a media type we can fall back to OCI.
  243. // This does have a special fallback for schema1 manifests just because it is easy to detect.
  244. switch mfst.MediaType {
  245. case schema2.MediaTypeManifest, ocispec.MediaTypeImageManifest:
  246. if mfst.Manifests != nil || mfst.FSLayers != nil {
  247. return "", fmt.Errorf(`media-type: %q should not have "manifests" or "fsLayers"`, mfst.MediaType)
  248. }
  249. return mfst.MediaType, nil
  250. case manifestlist.MediaTypeManifestList, ocispec.MediaTypeImageIndex:
  251. if mfst.Config != nil || mfst.Layers != nil || mfst.FSLayers != nil {
  252. return "", fmt.Errorf(`media-type: %q should not have "config", "layers", or "fsLayers"`, mfst.MediaType)
  253. }
  254. return mfst.MediaType, nil
  255. case schema1.MediaTypeManifest:
  256. if mfst.Manifests != nil || mfst.Layers != nil {
  257. return "", fmt.Errorf(`media-type: %q should not have "manifests" or "layers"`, mfst.MediaType)
  258. }
  259. return mfst.MediaType, nil
  260. default:
  261. if mfst.MediaType != "" {
  262. return mfst.MediaType, nil
  263. }
  264. }
  265. switch {
  266. case mfst.FSLayers != nil && mfst.Manifests == nil && mfst.Layers == nil && mfst.Config == nil:
  267. return schema1.MediaTypeManifest, nil
  268. case mfst.Config != nil && mfst.Manifests == nil && mfst.FSLayers == nil,
  269. mfst.Layers != nil && mfst.Manifests == nil && mfst.FSLayers == nil:
  270. return ocispec.MediaTypeImageManifest, nil
  271. case mfst.Config == nil && mfst.Layers == nil && mfst.FSLayers == nil:
  272. // fallback to index
  273. return ocispec.MediaTypeImageIndex, nil
  274. }
  275. return "", errors.New("media-type: cannot determine")
  276. }