github.com/containerd/Containerd@v1.4.13/remotes/docker/schema1/converter.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 schema1 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/base64" 23 "encoding/json" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "strconv" 28 "strings" 29 "sync" 30 "time" 31 32 "golang.org/x/sync/errgroup" 33 34 "github.com/containerd/containerd/archive/compression" 35 "github.com/containerd/containerd/content" 36 "github.com/containerd/containerd/errdefs" 37 "github.com/containerd/containerd/images" 38 "github.com/containerd/containerd/log" 39 "github.com/containerd/containerd/remotes" 40 digest "github.com/opencontainers/go-digest" 41 specs "github.com/opencontainers/image-spec/specs-go" 42 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 43 "github.com/pkg/errors" 44 ) 45 46 const ( 47 manifestSizeLimit = 8e6 // 8MB 48 labelDockerSchema1EmptyLayer = "containerd.io/docker.schema1.empty-layer" 49 ) 50 51 type blobState struct { 52 diffID digest.Digest 53 empty bool 54 } 55 56 // Converter converts schema1 manifests to schema2 on fetch 57 type Converter struct { 58 contentStore content.Store 59 fetcher remotes.Fetcher 60 61 pulledManifest *manifest 62 63 mu sync.Mutex 64 blobMap map[digest.Digest]blobState 65 layerBlobs map[digest.Digest]ocispec.Descriptor 66 } 67 68 // NewConverter returns a new converter 69 func NewConverter(contentStore content.Store, fetcher remotes.Fetcher) *Converter { 70 return &Converter{ 71 contentStore: contentStore, 72 fetcher: fetcher, 73 blobMap: map[digest.Digest]blobState{}, 74 layerBlobs: map[digest.Digest]ocispec.Descriptor{}, 75 } 76 } 77 78 // Handle fetching descriptors for a docker media type 79 func (c *Converter) Handle(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 80 switch desc.MediaType { 81 case images.MediaTypeDockerSchema1Manifest: 82 if err := c.fetchManifest(ctx, desc); err != nil { 83 return nil, err 84 } 85 86 m := c.pulledManifest 87 if len(m.FSLayers) != len(m.History) { 88 return nil, errors.New("invalid schema 1 manifest, history and layer mismatch") 89 } 90 descs := make([]ocispec.Descriptor, 0, len(c.pulledManifest.FSLayers)) 91 92 for i := range m.FSLayers { 93 if _, ok := c.blobMap[c.pulledManifest.FSLayers[i].BlobSum]; !ok { 94 empty, err := isEmptyLayer([]byte(m.History[i].V1Compatibility)) 95 if err != nil { 96 return nil, err 97 } 98 99 // Do no attempt to download a known empty blob 100 if !empty { 101 descs = append([]ocispec.Descriptor{ 102 { 103 MediaType: images.MediaTypeDockerSchema2LayerGzip, 104 Digest: c.pulledManifest.FSLayers[i].BlobSum, 105 Size: -1, 106 }, 107 }, descs...) 108 } 109 c.blobMap[c.pulledManifest.FSLayers[i].BlobSum] = blobState{ 110 empty: empty, 111 } 112 } 113 } 114 return descs, nil 115 case images.MediaTypeDockerSchema2LayerGzip: 116 if c.pulledManifest == nil { 117 return nil, errors.New("manifest required for schema 1 blob pull") 118 } 119 return nil, c.fetchBlob(ctx, desc) 120 default: 121 return nil, fmt.Errorf("%v not support for schema 1 manifests", desc.MediaType) 122 } 123 } 124 125 // ConvertOptions provides options on converting a docker schema1 manifest. 126 type ConvertOptions struct { 127 // ManifestMediaType specifies the media type of the manifest OCI descriptor. 128 ManifestMediaType string 129 130 // ConfigMediaType specifies the media type of the manifest config OCI 131 // descriptor. 132 ConfigMediaType string 133 } 134 135 // ConvertOpt allows configuring a convert operation. 136 type ConvertOpt func(context.Context, *ConvertOptions) error 137 138 // UseDockerSchema2 is used to indicate that a schema1 manifest should be 139 // converted into the media types for a docker schema2 manifest. 140 func UseDockerSchema2() ConvertOpt { 141 return func(ctx context.Context, o *ConvertOptions) error { 142 o.ManifestMediaType = images.MediaTypeDockerSchema2Manifest 143 o.ConfigMediaType = images.MediaTypeDockerSchema2Config 144 return nil 145 } 146 } 147 148 // Convert a docker manifest to an OCI descriptor 149 func (c *Converter) Convert(ctx context.Context, opts ...ConvertOpt) (ocispec.Descriptor, error) { 150 co := ConvertOptions{ 151 ManifestMediaType: ocispec.MediaTypeImageManifest, 152 ConfigMediaType: ocispec.MediaTypeImageConfig, 153 } 154 for _, opt := range opts { 155 if err := opt(ctx, &co); err != nil { 156 return ocispec.Descriptor{}, err 157 } 158 } 159 160 history, diffIDs, err := c.schema1ManifestHistory() 161 if err != nil { 162 return ocispec.Descriptor{}, errors.Wrap(err, "schema 1 conversion failed") 163 } 164 165 var img ocispec.Image 166 if err := json.Unmarshal([]byte(c.pulledManifest.History[0].V1Compatibility), &img); err != nil { 167 return ocispec.Descriptor{}, errors.Wrap(err, "failed to unmarshal image from schema 1 history") 168 } 169 170 img.History = history 171 img.RootFS = ocispec.RootFS{ 172 Type: "layers", 173 DiffIDs: diffIDs, 174 } 175 176 b, err := json.MarshalIndent(img, "", " ") 177 if err != nil { 178 return ocispec.Descriptor{}, errors.Wrap(err, "failed to marshal image") 179 } 180 181 config := ocispec.Descriptor{ 182 MediaType: co.ConfigMediaType, 183 Digest: digest.Canonical.FromBytes(b), 184 Size: int64(len(b)), 185 } 186 187 layers := make([]ocispec.Descriptor, len(diffIDs)) 188 for i, diffID := range diffIDs { 189 layers[i] = c.layerBlobs[diffID] 190 } 191 192 manifest := ocispec.Manifest{ 193 Versioned: specs.Versioned{ 194 SchemaVersion: 2, 195 }, 196 Config: config, 197 Layers: layers, 198 } 199 200 mb, err := json.MarshalIndent(manifest, "", " ") 201 if err != nil { 202 return ocispec.Descriptor{}, errors.Wrap(err, "failed to marshal image") 203 } 204 205 desc := ocispec.Descriptor{ 206 MediaType: co.ManifestMediaType, 207 Digest: digest.Canonical.FromBytes(mb), 208 Size: int64(len(mb)), 209 } 210 211 labels := map[string]string{} 212 labels["containerd.io/gc.ref.content.0"] = manifest.Config.Digest.String() 213 for i, ch := range manifest.Layers { 214 labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = ch.Digest.String() 215 } 216 217 ref := remotes.MakeRefKey(ctx, desc) 218 if err := content.WriteBlob(ctx, c.contentStore, ref, bytes.NewReader(mb), desc, content.WithLabels(labels)); err != nil { 219 return ocispec.Descriptor{}, errors.Wrap(err, "failed to write image manifest") 220 } 221 222 ref = remotes.MakeRefKey(ctx, config) 223 if err := content.WriteBlob(ctx, c.contentStore, ref, bytes.NewReader(b), config); err != nil { 224 return ocispec.Descriptor{}, errors.Wrap(err, "failed to write image config") 225 } 226 227 return desc, nil 228 } 229 230 // ReadStripSignature reads in a schema1 manifest and returns a byte array 231 // with the "signatures" field stripped 232 func ReadStripSignature(schema1Blob io.Reader) ([]byte, error) { 233 b, err := ioutil.ReadAll(io.LimitReader(schema1Blob, manifestSizeLimit)) // limit to 8MB 234 if err != nil { 235 return nil, err 236 } 237 238 return stripSignature(b) 239 } 240 241 func (c *Converter) fetchManifest(ctx context.Context, desc ocispec.Descriptor) error { 242 log.G(ctx).Debug("fetch schema 1") 243 244 rc, err := c.fetcher.Fetch(ctx, desc) 245 if err != nil { 246 return err 247 } 248 249 b, err := ReadStripSignature(rc) 250 rc.Close() 251 if err != nil { 252 return err 253 } 254 255 var m manifest 256 if err := json.Unmarshal(b, &m); err != nil { 257 return err 258 } 259 if len(m.Manifests) != 0 || len(m.Layers) != 0 { 260 return errors.New("converter: expected schema1 document but found extra keys") 261 } 262 c.pulledManifest = &m 263 264 return nil 265 } 266 267 func (c *Converter) fetchBlob(ctx context.Context, desc ocispec.Descriptor) error { 268 log.G(ctx).Debug("fetch blob") 269 270 var ( 271 ref = remotes.MakeRefKey(ctx, desc) 272 calc = newBlobStateCalculator() 273 compressMethod = compression.Gzip 274 ) 275 276 // size may be unknown, set to zero for content ingest 277 ingestDesc := desc 278 if ingestDesc.Size == -1 { 279 ingestDesc.Size = 0 280 } 281 282 cw, err := content.OpenWriter(ctx, c.contentStore, content.WithRef(ref), content.WithDescriptor(ingestDesc)) 283 if err != nil { 284 if !errdefs.IsAlreadyExists(err) { 285 return err 286 } 287 288 reuse, err := c.reuseLabelBlobState(ctx, desc) 289 if err != nil { 290 return err 291 } 292 293 if reuse { 294 return nil 295 } 296 297 ra, err := c.contentStore.ReaderAt(ctx, desc) 298 if err != nil { 299 return err 300 } 301 defer ra.Close() 302 303 r, err := compression.DecompressStream(content.NewReader(ra)) 304 if err != nil { 305 return err 306 } 307 308 compressMethod = r.GetCompression() 309 _, err = io.Copy(calc, r) 310 r.Close() 311 if err != nil { 312 return err 313 } 314 } else { 315 defer cw.Close() 316 317 rc, err := c.fetcher.Fetch(ctx, desc) 318 if err != nil { 319 return err 320 } 321 defer rc.Close() 322 323 eg, _ := errgroup.WithContext(ctx) 324 pr, pw := io.Pipe() 325 326 eg.Go(func() error { 327 r, err := compression.DecompressStream(pr) 328 if err != nil { 329 return err 330 } 331 332 compressMethod = r.GetCompression() 333 _, err = io.Copy(calc, r) 334 r.Close() 335 pr.CloseWithError(err) 336 return err 337 }) 338 339 eg.Go(func() error { 340 defer pw.Close() 341 342 return content.Copy(ctx, cw, io.TeeReader(rc, pw), ingestDesc.Size, ingestDesc.Digest) 343 }) 344 345 if err := eg.Wait(); err != nil { 346 return err 347 } 348 } 349 350 if desc.Size == -1 { 351 info, err := c.contentStore.Info(ctx, desc.Digest) 352 if err != nil { 353 return errors.Wrap(err, "failed to get blob info") 354 } 355 desc.Size = info.Size 356 } 357 358 if compressMethod == compression.Uncompressed { 359 log.G(ctx).WithField("id", desc.Digest).Debugf("changed media type for uncompressed schema1 layer blob") 360 desc.MediaType = images.MediaTypeDockerSchema2Layer 361 } 362 363 state := calc.State() 364 365 cinfo := content.Info{ 366 Digest: desc.Digest, 367 Labels: map[string]string{ 368 "containerd.io/uncompressed": state.diffID.String(), 369 labelDockerSchema1EmptyLayer: strconv.FormatBool(state.empty), 370 }, 371 } 372 373 if _, err := c.contentStore.Update(ctx, cinfo, "labels.containerd.io/uncompressed", fmt.Sprintf("labels.%s", labelDockerSchema1EmptyLayer)); err != nil { 374 return errors.Wrap(err, "failed to update uncompressed label") 375 } 376 377 c.mu.Lock() 378 c.blobMap[desc.Digest] = state 379 c.layerBlobs[state.diffID] = desc 380 c.mu.Unlock() 381 382 return nil 383 } 384 385 func (c *Converter) reuseLabelBlobState(ctx context.Context, desc ocispec.Descriptor) (bool, error) { 386 cinfo, err := c.contentStore.Info(ctx, desc.Digest) 387 if err != nil { 388 return false, errors.Wrap(err, "failed to get blob info") 389 } 390 desc.Size = cinfo.Size 391 392 diffID, ok := cinfo.Labels["containerd.io/uncompressed"] 393 if !ok { 394 return false, nil 395 } 396 397 emptyVal, ok := cinfo.Labels[labelDockerSchema1EmptyLayer] 398 if !ok { 399 return false, nil 400 } 401 402 isEmpty, err := strconv.ParseBool(emptyVal) 403 if err != nil { 404 log.G(ctx).WithField("id", desc.Digest).Warnf("failed to parse bool from label %s: %v", labelDockerSchema1EmptyLayer, isEmpty) 405 return false, nil 406 } 407 408 bState := blobState{empty: isEmpty} 409 410 if bState.diffID, err = digest.Parse(diffID); err != nil { 411 log.G(ctx).WithField("id", desc.Digest).Warnf("failed to parse digest from label containerd.io/uncompressed: %v", diffID) 412 return false, nil 413 } 414 415 // NOTE: there is no need to read header to get compression method 416 // because there are only two kinds of methods. 417 if bState.diffID == desc.Digest { 418 desc.MediaType = images.MediaTypeDockerSchema2Layer 419 } else { 420 desc.MediaType = images.MediaTypeDockerSchema2LayerGzip 421 } 422 423 c.mu.Lock() 424 c.blobMap[desc.Digest] = bState 425 c.layerBlobs[bState.diffID] = desc 426 c.mu.Unlock() 427 return true, nil 428 } 429 430 func (c *Converter) schema1ManifestHistory() ([]ocispec.History, []digest.Digest, error) { 431 if c.pulledManifest == nil { 432 return nil, nil, errors.New("missing schema 1 manifest for conversion") 433 } 434 m := *c.pulledManifest 435 436 if len(m.History) == 0 { 437 return nil, nil, errors.New("no history") 438 } 439 440 history := make([]ocispec.History, len(m.History)) 441 diffIDs := []digest.Digest{} 442 for i := range m.History { 443 var h v1History 444 if err := json.Unmarshal([]byte(m.History[i].V1Compatibility), &h); err != nil { 445 return nil, nil, errors.Wrap(err, "failed to unmarshal history") 446 } 447 448 blobSum := m.FSLayers[i].BlobSum 449 450 state := c.blobMap[blobSum] 451 452 history[len(history)-i-1] = ocispec.History{ 453 Author: h.Author, 454 Comment: h.Comment, 455 Created: &h.Created, 456 CreatedBy: strings.Join(h.ContainerConfig.Cmd, " "), 457 EmptyLayer: state.empty, 458 } 459 460 if !state.empty { 461 diffIDs = append([]digest.Digest{state.diffID}, diffIDs...) 462 463 } 464 } 465 466 return history, diffIDs, nil 467 } 468 469 type fsLayer struct { 470 BlobSum digest.Digest `json:"blobSum"` 471 } 472 473 type history struct { 474 V1Compatibility string `json:"v1Compatibility"` 475 } 476 477 type manifest struct { 478 FSLayers []fsLayer `json:"fsLayers"` 479 History []history `json:"history"` 480 Layers json.RawMessage `json:"layers,omitempty"` // OCI manifest 481 Manifests json.RawMessage `json:"manifests,omitempty"` // OCI index 482 } 483 484 type v1History struct { 485 Author string `json:"author,omitempty"` 486 Created time.Time `json:"created"` 487 Comment string `json:"comment,omitempty"` 488 ThrowAway *bool `json:"throwaway,omitempty"` 489 Size *int `json:"Size,omitempty"` // used before ThrowAway field 490 ContainerConfig struct { 491 Cmd []string `json:"Cmd,omitempty"` 492 } `json:"container_config,omitempty"` 493 } 494 495 // isEmptyLayer returns whether the v1 compatibility history describes an 496 // empty layer. A return value of true indicates the layer is empty, 497 // however false does not indicate non-empty. 498 func isEmptyLayer(compatHistory []byte) (bool, error) { 499 var h v1History 500 if err := json.Unmarshal(compatHistory, &h); err != nil { 501 return false, err 502 } 503 504 if h.ThrowAway != nil { 505 return *h.ThrowAway, nil 506 } 507 if h.Size != nil { 508 return *h.Size == 0, nil 509 } 510 511 // If no `Size` or `throwaway` field is given, then 512 // it cannot be determined whether the layer is empty 513 // from the history, return false 514 return false, nil 515 } 516 517 type signature struct { 518 Signatures []jsParsedSignature `json:"signatures"` 519 } 520 521 type jsParsedSignature struct { 522 Protected string `json:"protected"` 523 } 524 525 type protectedBlock struct { 526 Length int `json:"formatLength"` 527 Tail string `json:"formatTail"` 528 } 529 530 // joseBase64UrlDecode decodes the given string using the standard base64 url 531 // decoder but first adds the appropriate number of trailing '=' characters in 532 // accordance with the jose specification. 533 // http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2 534 func joseBase64UrlDecode(s string) ([]byte, error) { 535 switch len(s) % 4 { 536 case 0: 537 case 2: 538 s += "==" 539 case 3: 540 s += "=" 541 default: 542 return nil, errors.New("illegal base64url string") 543 } 544 return base64.URLEncoding.DecodeString(s) 545 } 546 547 func stripSignature(b []byte) ([]byte, error) { 548 var sig signature 549 if err := json.Unmarshal(b, &sig); err != nil { 550 return nil, err 551 } 552 if len(sig.Signatures) == 0 { 553 return nil, errors.New("no signatures") 554 } 555 pb, err := joseBase64UrlDecode(sig.Signatures[0].Protected) 556 if err != nil { 557 return nil, errors.Wrapf(err, "could not decode %s", sig.Signatures[0].Protected) 558 } 559 560 var protected protectedBlock 561 if err := json.Unmarshal(pb, &protected); err != nil { 562 return nil, err 563 } 564 565 if protected.Length > len(b) { 566 return nil, errors.New("invalid protected length block") 567 } 568 569 tail, err := joseBase64UrlDecode(protected.Tail) 570 if err != nil { 571 return nil, errors.Wrap(err, "invalid tail base 64 value") 572 } 573 574 return append(b[:protected.Length], tail...), nil 575 } 576 577 type blobStateCalculator struct { 578 empty bool 579 digester digest.Digester 580 } 581 582 func newBlobStateCalculator() *blobStateCalculator { 583 return &blobStateCalculator{ 584 empty: true, 585 digester: digest.Canonical.Digester(), 586 } 587 } 588 589 func (c *blobStateCalculator) Write(p []byte) (int, error) { 590 if c.empty { 591 for _, b := range p { 592 if b != 0x00 { 593 c.empty = false 594 break 595 } 596 } 597 } 598 return c.digester.Hash().Write(p) 599 } 600 601 func (c *blobStateCalculator) State() blobState { 602 return blobState{ 603 empty: c.empty, 604 diffID: c.digester.Digest(), 605 } 606 }