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