github.com/containerd/Containerd@v1.4.13/images/archive/exporter.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 archive 18 19 import ( 20 "archive/tar" 21 "context" 22 "encoding/json" 23 "io" 24 "path" 25 "sort" 26 27 "github.com/containerd/containerd/content" 28 "github.com/containerd/containerd/errdefs" 29 "github.com/containerd/containerd/images" 30 "github.com/containerd/containerd/platforms" 31 digest "github.com/opencontainers/go-digest" 32 ocispecs "github.com/opencontainers/image-spec/specs-go" 33 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 34 "github.com/pkg/errors" 35 ) 36 37 type exportOptions struct { 38 manifests []ocispec.Descriptor 39 platform platforms.MatchComparer 40 allPlatforms bool 41 skipDockerManifest bool 42 } 43 44 // ExportOpt defines options for configuring exported descriptors 45 type ExportOpt func(context.Context, *exportOptions) error 46 47 // WithPlatform defines the platform to require manifest lists have 48 // not exporting all platforms. 49 // Additionally, platform is used to resolve image configs for 50 // Docker v1.1, v1.2 format compatibility. 51 func WithPlatform(p platforms.MatchComparer) ExportOpt { 52 return func(ctx context.Context, o *exportOptions) error { 53 o.platform = p 54 return nil 55 } 56 } 57 58 // WithAllPlatforms exports all manifests from a manifest list. 59 // Missing content will fail the export. 60 func WithAllPlatforms() ExportOpt { 61 return func(ctx context.Context, o *exportOptions) error { 62 o.allPlatforms = true 63 return nil 64 } 65 } 66 67 // WithSkipDockerManifest skips creation of the Docker compatible 68 // manifest.json file. 69 func WithSkipDockerManifest() ExportOpt { 70 return func(ctx context.Context, o *exportOptions) error { 71 o.skipDockerManifest = true 72 return nil 73 } 74 } 75 76 // WithImage adds the provided images to the exported archive. 77 func WithImage(is images.Store, name string) ExportOpt { 78 return func(ctx context.Context, o *exportOptions) error { 79 img, err := is.Get(ctx, name) 80 if err != nil { 81 return err 82 } 83 84 img.Target.Annotations = addNameAnnotation(name, img.Target.Annotations) 85 o.manifests = append(o.manifests, img.Target) 86 87 return nil 88 } 89 } 90 91 // WithManifest adds a manifest to the exported archive. 92 // When names are given they will be set on the manifest in the 93 // exported archive, creating an index record for each name. 94 // When no names are provided, it is up to caller to put name annotation to 95 // on the manifest descriptor if needed. 96 func WithManifest(manifest ocispec.Descriptor, names ...string) ExportOpt { 97 return func(ctx context.Context, o *exportOptions) error { 98 if len(names) == 0 { 99 o.manifests = append(o.manifests, manifest) 100 } 101 for _, name := range names { 102 mc := manifest 103 mc.Annotations = addNameAnnotation(name, manifest.Annotations) 104 o.manifests = append(o.manifests, mc) 105 } 106 107 return nil 108 } 109 } 110 111 func addNameAnnotation(name string, base map[string]string) map[string]string { 112 annotations := map[string]string{} 113 for k, v := range base { 114 annotations[k] = v 115 } 116 117 annotations[images.AnnotationImageName] = name 118 annotations[ocispec.AnnotationRefName] = ociReferenceName(name) 119 120 return annotations 121 } 122 123 // Export implements Exporter. 124 func Export(ctx context.Context, store content.Provider, writer io.Writer, opts ...ExportOpt) error { 125 var eo exportOptions 126 for _, opt := range opts { 127 if err := opt(ctx, &eo); err != nil { 128 return err 129 } 130 } 131 132 records := []tarRecord{ 133 ociLayoutFile(""), 134 ociIndexRecord(eo.manifests), 135 } 136 137 algorithms := map[string]struct{}{} 138 dManifests := map[digest.Digest]*exportManifest{} 139 resolvedIndex := map[digest.Digest]digest.Digest{} 140 for _, desc := range eo.manifests { 141 switch desc.MediaType { 142 case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest: 143 mt, ok := dManifests[desc.Digest] 144 if !ok { 145 // TODO(containerd): Skip if already added 146 r, err := getRecords(ctx, store, desc, algorithms) 147 if err != nil { 148 return err 149 } 150 records = append(records, r...) 151 152 mt = &exportManifest{ 153 manifest: desc, 154 } 155 dManifests[desc.Digest] = mt 156 } 157 158 name := desc.Annotations[images.AnnotationImageName] 159 if name != "" && !eo.skipDockerManifest { 160 mt.names = append(mt.names, name) 161 } 162 case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex: 163 d, ok := resolvedIndex[desc.Digest] 164 if !ok { 165 records = append(records, blobRecord(store, desc)) 166 167 p, err := content.ReadBlob(ctx, store, desc) 168 if err != nil { 169 return err 170 } 171 172 var index ocispec.Index 173 if err := json.Unmarshal(p, &index); err != nil { 174 return err 175 } 176 177 var manifests []ocispec.Descriptor 178 for _, m := range index.Manifests { 179 if eo.platform != nil { 180 if m.Platform == nil || eo.platform.Match(*m.Platform) { 181 manifests = append(manifests, m) 182 } else if !eo.allPlatforms { 183 continue 184 } 185 } 186 187 r, err := getRecords(ctx, store, m, algorithms) 188 if err != nil { 189 return err 190 } 191 192 records = append(records, r...) 193 } 194 195 if !eo.skipDockerManifest { 196 if len(manifests) >= 1 { 197 if len(manifests) > 1 { 198 sort.SliceStable(manifests, func(i, j int) bool { 199 if manifests[i].Platform == nil { 200 return false 201 } 202 if manifests[j].Platform == nil { 203 return true 204 } 205 return eo.platform.Less(*manifests[i].Platform, *manifests[j].Platform) 206 }) 207 } 208 d = manifests[0].Digest 209 dManifests[d] = &exportManifest{ 210 manifest: manifests[0], 211 } 212 } else if eo.platform != nil { 213 return errors.Wrap(errdefs.ErrNotFound, "no manifest found for platform") 214 } 215 } 216 resolvedIndex[desc.Digest] = d 217 } 218 if d != "" { 219 if name := desc.Annotations[images.AnnotationImageName]; name != "" { 220 mt := dManifests[d] 221 mt.names = append(mt.names, name) 222 } 223 224 } 225 default: 226 return errors.Wrap(errdefs.ErrInvalidArgument, "only manifests may be exported") 227 } 228 } 229 230 if len(dManifests) > 0 { 231 tr, err := manifestsRecord(ctx, store, dManifests) 232 if err != nil { 233 return errors.Wrap(err, "unable to create manifests file") 234 } 235 236 records = append(records, tr) 237 } 238 239 if len(algorithms) > 0 { 240 records = append(records, directoryRecord("blobs/", 0755)) 241 for alg := range algorithms { 242 records = append(records, directoryRecord("blobs/"+alg+"/", 0755)) 243 } 244 } 245 246 tw := tar.NewWriter(writer) 247 defer tw.Close() 248 return writeTar(ctx, tw, records) 249 } 250 251 func getRecords(ctx context.Context, store content.Provider, desc ocispec.Descriptor, algorithms map[string]struct{}) ([]tarRecord, error) { 252 var records []tarRecord 253 exportHandler := func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 254 records = append(records, blobRecord(store, desc)) 255 algorithms[desc.Digest.Algorithm().String()] = struct{}{} 256 return nil, nil 257 } 258 259 childrenHandler := images.ChildrenHandler(store) 260 261 handlers := images.Handlers( 262 childrenHandler, 263 images.HandlerFunc(exportHandler), 264 ) 265 266 // Walk sequentially since the number of fetches is likely one and doing in 267 // parallel requires locking the export handler 268 if err := images.Walk(ctx, handlers, desc); err != nil { 269 return nil, err 270 } 271 272 return records, nil 273 } 274 275 type tarRecord struct { 276 Header *tar.Header 277 CopyTo func(context.Context, io.Writer) (int64, error) 278 } 279 280 func blobRecord(cs content.Provider, desc ocispec.Descriptor) tarRecord { 281 path := path.Join("blobs", desc.Digest.Algorithm().String(), desc.Digest.Encoded()) 282 return tarRecord{ 283 Header: &tar.Header{ 284 Name: path, 285 Mode: 0444, 286 Size: desc.Size, 287 Typeflag: tar.TypeReg, 288 }, 289 CopyTo: func(ctx context.Context, w io.Writer) (int64, error) { 290 r, err := cs.ReaderAt(ctx, desc) 291 if err != nil { 292 return 0, errors.Wrap(err, "failed to get reader") 293 } 294 defer r.Close() 295 296 // Verify digest 297 dgstr := desc.Digest.Algorithm().Digester() 298 299 n, err := io.Copy(io.MultiWriter(w, dgstr.Hash()), content.NewReader(r)) 300 if err != nil { 301 return 0, errors.Wrap(err, "failed to copy to tar") 302 } 303 if dgstr.Digest() != desc.Digest { 304 return 0, errors.Errorf("unexpected digest %s copied", dgstr.Digest()) 305 } 306 return n, nil 307 }, 308 } 309 } 310 311 func directoryRecord(name string, mode int64) tarRecord { 312 return tarRecord{ 313 Header: &tar.Header{ 314 Name: name, 315 Mode: mode, 316 Typeflag: tar.TypeDir, 317 }, 318 } 319 } 320 321 func ociLayoutFile(version string) tarRecord { 322 if version == "" { 323 version = ocispec.ImageLayoutVersion 324 } 325 layout := ocispec.ImageLayout{ 326 Version: version, 327 } 328 329 b, err := json.Marshal(layout) 330 if err != nil { 331 panic(err) 332 } 333 334 return tarRecord{ 335 Header: &tar.Header{ 336 Name: ocispec.ImageLayoutFile, 337 Mode: 0444, 338 Size: int64(len(b)), 339 Typeflag: tar.TypeReg, 340 }, 341 CopyTo: func(ctx context.Context, w io.Writer) (int64, error) { 342 n, err := w.Write(b) 343 return int64(n), err 344 }, 345 } 346 347 } 348 349 func ociIndexRecord(manifests []ocispec.Descriptor) tarRecord { 350 index := ocispec.Index{ 351 Versioned: ocispecs.Versioned{ 352 SchemaVersion: 2, 353 }, 354 Manifests: manifests, 355 } 356 357 b, err := json.Marshal(index) 358 if err != nil { 359 panic(err) 360 } 361 362 return tarRecord{ 363 Header: &tar.Header{ 364 Name: "index.json", 365 Mode: 0644, 366 Size: int64(len(b)), 367 Typeflag: tar.TypeReg, 368 }, 369 CopyTo: func(ctx context.Context, w io.Writer) (int64, error) { 370 n, err := w.Write(b) 371 return int64(n), err 372 }, 373 } 374 } 375 376 type exportManifest struct { 377 manifest ocispec.Descriptor 378 names []string 379 } 380 381 func manifestsRecord(ctx context.Context, store content.Provider, manifests map[digest.Digest]*exportManifest) (tarRecord, error) { 382 mfsts := make([]struct { 383 Config string 384 RepoTags []string 385 Layers []string 386 }, len(manifests)) 387 388 var i int 389 for _, m := range manifests { 390 p, err := content.ReadBlob(ctx, store, m.manifest) 391 if err != nil { 392 return tarRecord{}, err 393 } 394 395 var manifest ocispec.Manifest 396 if err := json.Unmarshal(p, &manifest); err != nil { 397 return tarRecord{}, err 398 } 399 if err := manifest.Config.Digest.Validate(); err != nil { 400 return tarRecord{}, errors.Wrapf(err, "invalid manifest %q", m.manifest.Digest) 401 } 402 403 dgst := manifest.Config.Digest 404 mfsts[i].Config = path.Join("blobs", dgst.Algorithm().String(), dgst.Encoded()) 405 for _, l := range manifest.Layers { 406 path := path.Join("blobs", l.Digest.Algorithm().String(), l.Digest.Encoded()) 407 mfsts[i].Layers = append(mfsts[i].Layers, path) 408 } 409 410 for _, name := range m.names { 411 nname, err := familiarizeReference(name) 412 if err != nil { 413 return tarRecord{}, err 414 } 415 416 mfsts[i].RepoTags = append(mfsts[i].RepoTags, nname) 417 } 418 419 i++ 420 } 421 422 b, err := json.Marshal(mfsts) 423 if err != nil { 424 return tarRecord{}, err 425 } 426 427 return tarRecord{ 428 Header: &tar.Header{ 429 Name: "manifest.json", 430 Mode: 0644, 431 Size: int64(len(b)), 432 Typeflag: tar.TypeReg, 433 }, 434 CopyTo: func(ctx context.Context, w io.Writer) (int64, error) { 435 n, err := w.Write(b) 436 return int64(n), err 437 }, 438 }, nil 439 } 440 441 func writeTar(ctx context.Context, tw *tar.Writer, records []tarRecord) error { 442 sort.Slice(records, func(i, j int) bool { 443 return records[i].Header.Name < records[j].Header.Name 444 }) 445 446 var last string 447 for _, record := range records { 448 if record.Header.Name == last { 449 continue 450 } 451 last = record.Header.Name 452 if err := tw.WriteHeader(record.Header); err != nil { 453 return err 454 } 455 if record.CopyTo != nil { 456 n, err := record.CopyTo(ctx, tw) 457 if err != nil { 458 return err 459 } 460 if n != record.Header.Size { 461 return errors.Errorf("unexpected copy size for %s", record.Header.Name) 462 } 463 } else if record.Header.Size > 0 { 464 return errors.Errorf("no content to write to record with non-zero size for %s", record.Header.Name) 465 } 466 } 467 return nil 468 }