github.com/containerd/Containerd@v1.4.13/images/image.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package images 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "sort" 24 "time" 25 26 "github.com/containerd/containerd/content" 27 "github.com/containerd/containerd/errdefs" 28 "github.com/containerd/containerd/log" 29 "github.com/containerd/containerd/platforms" 30 digest "github.com/opencontainers/go-digest" 31 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 32 "github.com/pkg/errors" 33 ) 34 35 // Image provides the model for how containerd views container images. 36 type Image struct { 37 // Name of the image. 38 // 39 // To be pulled, it must be a reference compatible with resolvers. 40 // 41 // This field is required. 42 Name string 43 44 // Labels provide runtime decoration for the image record. 45 // 46 // There is no default behavior for how these labels are propagated. They 47 // only decorate the static metadata object. 48 // 49 // This field is optional. 50 Labels map[string]string 51 52 // Target describes the root content for this image. Typically, this is 53 // a manifest, index or manifest list. 54 Target ocispec.Descriptor 55 56 CreatedAt, UpdatedAt time.Time 57 } 58 59 // DeleteOptions provide options on image delete 60 type DeleteOptions struct { 61 Synchronous bool 62 } 63 64 // DeleteOpt allows configuring a delete operation 65 type DeleteOpt func(context.Context, *DeleteOptions) error 66 67 // SynchronousDelete is used to indicate that an image deletion and removal of 68 // the image resources should occur synchronously before returning a result. 69 func SynchronousDelete() DeleteOpt { 70 return func(ctx context.Context, o *DeleteOptions) error { 71 o.Synchronous = true 72 return nil 73 } 74 } 75 76 // Store and interact with images 77 type Store interface { 78 Get(ctx context.Context, name string) (Image, error) 79 List(ctx context.Context, filters ...string) ([]Image, error) 80 Create(ctx context.Context, image Image) (Image, error) 81 82 // Update will replace the data in the store with the provided image. If 83 // one or more fieldpaths are provided, only those fields will be updated. 84 Update(ctx context.Context, image Image, fieldpaths ...string) (Image, error) 85 86 Delete(ctx context.Context, name string, opts ...DeleteOpt) error 87 } 88 89 // TODO(stevvooe): Many of these functions make strong platform assumptions, 90 // which are untrue in a lot of cases. More refactoring must be done here to 91 // make this work in all cases. 92 93 // Config resolves the image configuration descriptor. 94 // 95 // The caller can then use the descriptor to resolve and process the 96 // configuration of the image. 97 func (image *Image) Config(ctx context.Context, provider content.Provider, platform platforms.MatchComparer) (ocispec.Descriptor, error) { 98 return Config(ctx, provider, image.Target, platform) 99 } 100 101 // RootFS returns the unpacked diffids that make up and images rootfs. 102 // 103 // These are used to verify that a set of layers unpacked to the expected 104 // values. 105 func (image *Image) RootFS(ctx context.Context, provider content.Provider, platform platforms.MatchComparer) ([]digest.Digest, error) { 106 desc, err := image.Config(ctx, provider, platform) 107 if err != nil { 108 return nil, err 109 } 110 return RootFS(ctx, provider, desc) 111 } 112 113 // Size returns the total size of an image's packed resources. 114 func (image *Image) Size(ctx context.Context, provider content.Provider, platform platforms.MatchComparer) (int64, error) { 115 var size int64 116 return size, Walk(ctx, Handlers(HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 117 if desc.Size < 0 { 118 return nil, errors.Errorf("invalid size %v in %v (%v)", desc.Size, desc.Digest, desc.MediaType) 119 } 120 size += desc.Size 121 return nil, nil 122 }), LimitManifests(FilterPlatforms(ChildrenHandler(provider), platform), platform, 1)), image.Target) 123 } 124 125 type platformManifest struct { 126 p *ocispec.Platform 127 m *ocispec.Manifest 128 } 129 130 // Manifest resolves a manifest from the image for the given platform. 131 // 132 // When a manifest descriptor inside of a manifest index does not have 133 // a platform defined, the platform from the image config is considered. 134 // 135 // If the descriptor points to a non-index manifest, then the manifest is 136 // unmarshalled and returned without considering the platform inside of the 137 // config. 138 // 139 // TODO(stevvooe): This violates the current platform agnostic approach to this 140 // package by returning a specific manifest type. We'll need to refactor this 141 // to return a manifest descriptor or decide that we want to bring the API in 142 // this direction because this abstraction is not needed.` 143 func Manifest(ctx context.Context, provider content.Provider, image ocispec.Descriptor, platform platforms.MatchComparer) (ocispec.Manifest, error) { 144 var ( 145 limit = 1 146 m []platformManifest 147 wasIndex bool 148 ) 149 150 if err := Walk(ctx, HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 151 switch desc.MediaType { 152 case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest: 153 p, err := content.ReadBlob(ctx, provider, desc) 154 if err != nil { 155 return nil, err 156 } 157 158 if err := validateMediaType(p, desc.MediaType); err != nil { 159 return nil, errors.Wrapf(err, "manifest: invalid desc %s", desc.Digest) 160 } 161 162 var manifest ocispec.Manifest 163 if err := json.Unmarshal(p, &manifest); err != nil { 164 return nil, err 165 } 166 167 if desc.Digest != image.Digest && platform != nil { 168 if desc.Platform != nil && !platform.Match(*desc.Platform) { 169 return nil, nil 170 } 171 172 if desc.Platform == nil { 173 p, err := content.ReadBlob(ctx, provider, manifest.Config) 174 if err != nil { 175 return nil, err 176 } 177 178 var image ocispec.Image 179 if err := json.Unmarshal(p, &image); err != nil { 180 return nil, err 181 } 182 183 if !platform.Match(platforms.Normalize(ocispec.Platform{OS: image.OS, Architecture: image.Architecture})) { 184 return nil, nil 185 } 186 187 } 188 } 189 190 m = append(m, platformManifest{ 191 p: desc.Platform, 192 m: &manifest, 193 }) 194 195 return nil, nil 196 case MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex: 197 p, err := content.ReadBlob(ctx, provider, desc) 198 if err != nil { 199 return nil, err 200 } 201 202 if err := validateMediaType(p, desc.MediaType); err != nil { 203 return nil, errors.Wrapf(err, "manifest: invalid desc %s", desc.Digest) 204 } 205 206 var idx ocispec.Index 207 if err := json.Unmarshal(p, &idx); err != nil { 208 return nil, err 209 } 210 211 if platform == nil { 212 return idx.Manifests, nil 213 } 214 215 var descs []ocispec.Descriptor 216 for _, d := range idx.Manifests { 217 if d.Platform == nil || platform.Match(*d.Platform) { 218 descs = append(descs, d) 219 } 220 } 221 222 sort.SliceStable(descs, func(i, j int) bool { 223 if descs[i].Platform == nil { 224 return false 225 } 226 if descs[j].Platform == nil { 227 return true 228 } 229 return platform.Less(*descs[i].Platform, *descs[j].Platform) 230 }) 231 232 wasIndex = true 233 234 if len(descs) > limit { 235 return descs[:limit], nil 236 } 237 return descs, nil 238 } 239 return nil, errors.Wrapf(errdefs.ErrNotFound, "unexpected media type %v for %v", desc.MediaType, desc.Digest) 240 }), image); err != nil { 241 return ocispec.Manifest{}, err 242 } 243 244 if len(m) == 0 { 245 err := errors.Wrapf(errdefs.ErrNotFound, "manifest %v", image.Digest) 246 if wasIndex { 247 err = errors.Wrapf(errdefs.ErrNotFound, "no match for platform in manifest %v", image.Digest) 248 } 249 return ocispec.Manifest{}, err 250 } 251 return *m[0].m, nil 252 } 253 254 // Config resolves the image configuration descriptor using a content provided 255 // to resolve child resources on the image. 256 // 257 // The caller can then use the descriptor to resolve and process the 258 // configuration of the image. 259 func Config(ctx context.Context, provider content.Provider, image ocispec.Descriptor, platform platforms.MatchComparer) (ocispec.Descriptor, error) { 260 manifest, err := Manifest(ctx, provider, image, platform) 261 if err != nil { 262 return ocispec.Descriptor{}, err 263 } 264 return manifest.Config, err 265 } 266 267 // Platforms returns one or more platforms supported by the image. 268 func Platforms(ctx context.Context, provider content.Provider, image ocispec.Descriptor) ([]ocispec.Platform, error) { 269 var platformSpecs []ocispec.Platform 270 return platformSpecs, Walk(ctx, Handlers(HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 271 if desc.Platform != nil { 272 platformSpecs = append(platformSpecs, *desc.Platform) 273 return nil, ErrSkipDesc 274 } 275 276 switch desc.MediaType { 277 case MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig: 278 p, err := content.ReadBlob(ctx, provider, desc) 279 if err != nil { 280 return nil, err 281 } 282 283 var image ocispec.Image 284 if err := json.Unmarshal(p, &image); err != nil { 285 return nil, err 286 } 287 288 platformSpecs = append(platformSpecs, 289 platforms.Normalize(ocispec.Platform{OS: image.OS, Architecture: image.Architecture})) 290 } 291 return nil, nil 292 }), ChildrenHandler(provider)), image) 293 } 294 295 // Check returns nil if the all components of an image are available in the 296 // provider for the specified platform. 297 // 298 // If available is true, the caller can assume that required represents the 299 // complete set of content required for the image. 300 // 301 // missing will have the components that are part of required but not avaiiable 302 // in the provider. 303 // 304 // If there is a problem resolving content, an error will be returned. 305 func Check(ctx context.Context, provider content.Provider, image ocispec.Descriptor, platform platforms.MatchComparer) (available bool, required, present, missing []ocispec.Descriptor, err error) { 306 mfst, err := Manifest(ctx, provider, image, platform) 307 if err != nil { 308 if errdefs.IsNotFound(err) { 309 return false, []ocispec.Descriptor{image}, nil, []ocispec.Descriptor{image}, nil 310 } 311 312 return false, nil, nil, nil, errors.Wrapf(err, "failed to check image %v", image.Digest) 313 } 314 315 // TODO(stevvooe): It is possible that referenced conponents could have 316 // children, but this is rare. For now, we ignore this and only verify 317 // that manifest components are present. 318 required = append([]ocispec.Descriptor{mfst.Config}, mfst.Layers...) 319 320 for _, desc := range required { 321 ra, err := provider.ReaderAt(ctx, desc) 322 if err != nil { 323 if errdefs.IsNotFound(err) { 324 missing = append(missing, desc) 325 continue 326 } else { 327 return false, nil, nil, nil, errors.Wrapf(err, "failed to check image %v", desc.Digest) 328 } 329 } 330 ra.Close() 331 present = append(present, desc) 332 333 } 334 335 return true, required, present, missing, nil 336 } 337 338 // Children returns the immediate children of content described by the descriptor. 339 func Children(ctx context.Context, provider content.Provider, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 340 var descs []ocispec.Descriptor 341 switch desc.MediaType { 342 case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest: 343 p, err := content.ReadBlob(ctx, provider, desc) 344 if err != nil { 345 return nil, err 346 } 347 348 if err := validateMediaType(p, desc.MediaType); err != nil { 349 return nil, errors.Wrapf(err, "children: invalid desc %s", desc.Digest) 350 } 351 352 // TODO(stevvooe): We just assume oci manifest, for now. There may be 353 // subtle differences from the docker version. 354 var manifest ocispec.Manifest 355 if err := json.Unmarshal(p, &manifest); err != nil { 356 return nil, err 357 } 358 359 descs = append(descs, manifest.Config) 360 descs = append(descs, manifest.Layers...) 361 case MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex: 362 p, err := content.ReadBlob(ctx, provider, desc) 363 if err != nil { 364 return nil, err 365 } 366 367 if err := validateMediaType(p, desc.MediaType); err != nil { 368 return nil, errors.Wrapf(err, "children: invalid desc %s", desc.Digest) 369 } 370 371 var index ocispec.Index 372 if err := json.Unmarshal(p, &index); err != nil { 373 return nil, err 374 } 375 376 descs = append(descs, index.Manifests...) 377 default: 378 if IsLayerType(desc.MediaType) || IsKnownConfig(desc.MediaType) { 379 // childless data types. 380 return nil, nil 381 } 382 log.G(ctx).Debugf("encountered unknown type %v; children may not be fetched", desc.MediaType) 383 } 384 385 return descs, nil 386 } 387 388 // unknownDocument represents a manifest, manifest list, or index that has not 389 // yet been validated. 390 type unknownDocument struct { 391 MediaType string `json:"mediaType,omitempty"` 392 Config json.RawMessage `json:"config,omitempty"` 393 Layers json.RawMessage `json:"layers,omitempty"` 394 Manifests json.RawMessage `json:"manifests,omitempty"` 395 FSLayers json.RawMessage `json:"fsLayers,omitempty"` // schema 1 396 } 397 398 // validateMediaType returns an error if the byte slice is invalid JSON or if 399 // the media type identifies the blob as one format but it contains elements of 400 // another format. 401 func validateMediaType(b []byte, mt string) error { 402 var doc unknownDocument 403 if err := json.Unmarshal(b, &doc); err != nil { 404 return err 405 } 406 if len(doc.FSLayers) != 0 { 407 return fmt.Errorf("media-type: schema 1 not supported") 408 } 409 switch mt { 410 case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest: 411 if len(doc.Manifests) != 0 || 412 doc.MediaType == MediaTypeDockerSchema2ManifestList || 413 doc.MediaType == ocispec.MediaTypeImageIndex { 414 return fmt.Errorf("media-type: expected manifest but found index (%s)", mt) 415 } 416 case MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex: 417 if len(doc.Config) != 0 || len(doc.Layers) != 0 || 418 doc.MediaType == MediaTypeDockerSchema2Manifest || 419 doc.MediaType == ocispec.MediaTypeImageManifest { 420 return fmt.Errorf("media-type: expected index but found manifest (%s)", mt) 421 } 422 } 423 return nil 424 } 425 426 // RootFS returns the unpacked diffids that make up and images rootfs. 427 // 428 // These are used to verify that a set of layers unpacked to the expected 429 // values. 430 func RootFS(ctx context.Context, provider content.Provider, configDesc ocispec.Descriptor) ([]digest.Digest, error) { 431 p, err := content.ReadBlob(ctx, provider, configDesc) 432 if err != nil { 433 return nil, err 434 } 435 436 var config ocispec.Image 437 if err := json.Unmarshal(p, &config); err != nil { 438 return nil, err 439 } 440 return config.RootFS.DiffIDs, nil 441 }