github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/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 c.pulledManifest = &m 260 261 return nil 262 } 263 264 func (c *Converter) fetchBlob(ctx context.Context, desc ocispec.Descriptor) error { 265 log.G(ctx).Debug("fetch blob") 266 267 var ( 268 ref = remotes.MakeRefKey(ctx, desc) 269 calc = newBlobStateCalculator() 270 compressMethod = compression.Gzip 271 ) 272 273 // size may be unknown, set to zero for content ingest 274 ingestDesc := desc 275 if ingestDesc.Size == -1 { 276 ingestDesc.Size = 0 277 } 278 279 cw, err := content.OpenWriter(ctx, c.contentStore, content.WithRef(ref), content.WithDescriptor(ingestDesc)) 280 if err != nil { 281 if !errdefs.IsAlreadyExists(err) { 282 return err 283 } 284 285 reuse, err := c.reuseLabelBlobState(ctx, desc) 286 if err != nil { 287 return err 288 } 289 290 if reuse { 291 return nil 292 } 293 294 ra, err := c.contentStore.ReaderAt(ctx, desc) 295 if err != nil { 296 return err 297 } 298 defer ra.Close() 299 300 r, err := compression.DecompressStream(content.NewReader(ra)) 301 if err != nil { 302 return err 303 } 304 305 compressMethod = r.GetCompression() 306 _, err = io.Copy(calc, r) 307 r.Close() 308 if err != nil { 309 return err 310 } 311 } else { 312 defer cw.Close() 313 314 rc, err := c.fetcher.Fetch(ctx, desc) 315 if err != nil { 316 return err 317 } 318 defer rc.Close() 319 320 eg, _ := errgroup.WithContext(ctx) 321 pr, pw := io.Pipe() 322 323 eg.Go(func() error { 324 r, err := compression.DecompressStream(pr) 325 if err != nil { 326 return err 327 } 328 329 compressMethod = r.GetCompression() 330 _, err = io.Copy(calc, r) 331 r.Close() 332 pr.CloseWithError(err) 333 return err 334 }) 335 336 eg.Go(func() error { 337 defer pw.Close() 338 339 return content.Copy(ctx, cw, io.TeeReader(rc, pw), ingestDesc.Size, ingestDesc.Digest) 340 }) 341 342 if err := eg.Wait(); err != nil { 343 return err 344 } 345 } 346 347 if desc.Size == -1 { 348 info, err := c.contentStore.Info(ctx, desc.Digest) 349 if err != nil { 350 return errors.Wrap(err, "failed to get blob info") 351 } 352 desc.Size = info.Size 353 } 354 355 if compressMethod == compression.Uncompressed { 356 log.G(ctx).WithField("id", desc.Digest).Debugf("changed media type for uncompressed schema1 layer blob") 357 desc.MediaType = images.MediaTypeDockerSchema2Layer 358 } 359 360 state := calc.State() 361 362 cinfo := content.Info{ 363 Digest: desc.Digest, 364 Labels: map[string]string{ 365 "containerd.io/uncompressed": state.diffID.String(), 366 labelDockerSchema1EmptyLayer: strconv.FormatBool(state.empty), 367 }, 368 } 369 370 if _, err := c.contentStore.Update(ctx, cinfo, "labels.containerd.io/uncompressed", fmt.Sprintf("labels.%s", labelDockerSchema1EmptyLayer)); err != nil { 371 return errors.Wrap(err, "failed to update uncompressed label") 372 } 373 374 c.mu.Lock() 375 c.blobMap[desc.Digest] = state 376 c.layerBlobs[state.diffID] = desc 377 c.mu.Unlock() 378 379 return nil 380 } 381 382 func (c *Converter) reuseLabelBlobState(ctx context.Context, desc ocispec.Descriptor) (bool, error) { 383 cinfo, err := c.contentStore.Info(ctx, desc.Digest) 384 if err != nil { 385 return false, errors.Wrap(err, "failed to get blob info") 386 } 387 desc.Size = cinfo.Size 388 389 diffID, ok := cinfo.Labels["containerd.io/uncompressed"] 390 if !ok { 391 return false, nil 392 } 393 394 emptyVal, ok := cinfo.Labels[labelDockerSchema1EmptyLayer] 395 if !ok { 396 return false, nil 397 } 398 399 isEmpty, err := strconv.ParseBool(emptyVal) 400 if err != nil { 401 log.G(ctx).WithField("id", desc.Digest).Warnf("failed to parse bool from label %s: %v", labelDockerSchema1EmptyLayer, isEmpty) 402 return false, nil 403 } 404 405 bState := blobState{empty: isEmpty} 406 407 if bState.diffID, err = digest.Parse(diffID); err != nil { 408 log.G(ctx).WithField("id", desc.Digest).Warnf("failed to parse digest from label containerd.io/uncompressed: %v", diffID) 409 return false, nil 410 } 411 412 // NOTE: there is no need to read header to get compression method 413 // because there are only two kinds of methods. 414 if bState.diffID == desc.Digest { 415 desc.MediaType = images.MediaTypeDockerSchema2Layer 416 } else { 417 desc.MediaType = images.MediaTypeDockerSchema2LayerGzip 418 } 419 420 c.mu.Lock() 421 c.blobMap[desc.Digest] = bState 422 c.layerBlobs[bState.diffID] = desc 423 c.mu.Unlock() 424 return true, nil 425 } 426 427 func (c *Converter) schema1ManifestHistory() ([]ocispec.History, []digest.Digest, error) { 428 if c.pulledManifest == nil { 429 return nil, nil, errors.New("missing schema 1 manifest for conversion") 430 } 431 m := *c.pulledManifest 432 433 if len(m.History) == 0 { 434 return nil, nil, errors.New("no history") 435 } 436 437 history := make([]ocispec.History, len(m.History)) 438 diffIDs := []digest.Digest{} 439 for i := range m.History { 440 var h v1History 441 if err := json.Unmarshal([]byte(m.History[i].V1Compatibility), &h); err != nil { 442 return nil, nil, errors.Wrap(err, "failed to unmarshal history") 443 } 444 445 blobSum := m.FSLayers[i].BlobSum 446 447 state := c.blobMap[blobSum] 448 449 history[len(history)-i-1] = ocispec.History{ 450 Author: h.Author, 451 Comment: h.Comment, 452 Created: &h.Created, 453 CreatedBy: strings.Join(h.ContainerConfig.Cmd, " "), 454 EmptyLayer: state.empty, 455 } 456 457 if !state.empty { 458 diffIDs = append([]digest.Digest{state.diffID}, diffIDs...) 459 460 } 461 } 462 463 return history, diffIDs, nil 464 } 465 466 type fsLayer struct { 467 BlobSum digest.Digest `json:"blobSum"` 468 } 469 470 type history struct { 471 V1Compatibility string `json:"v1Compatibility"` 472 } 473 474 type manifest struct { 475 FSLayers []fsLayer `json:"fsLayers"` 476 History []history `json:"history"` 477 } 478 479 type v1History struct { 480 Author string `json:"author,omitempty"` 481 Created time.Time `json:"created"` 482 Comment string `json:"comment,omitempty"` 483 ThrowAway *bool `json:"throwaway,omitempty"` 484 Size *int `json:"Size,omitempty"` // used before ThrowAway field 485 ContainerConfig struct { 486 Cmd []string `json:"Cmd,omitempty"` 487 } `json:"container_config,omitempty"` 488 } 489 490 // isEmptyLayer returns whether the v1 compatibility history describes an 491 // empty layer. A return value of true indicates the layer is empty, 492 // however false does not indicate non-empty. 493 func isEmptyLayer(compatHistory []byte) (bool, error) { 494 var h v1History 495 if err := json.Unmarshal(compatHistory, &h); err != nil { 496 return false, err 497 } 498 499 if h.ThrowAway != nil { 500 return *h.ThrowAway, nil 501 } 502 if h.Size != nil { 503 return *h.Size == 0, nil 504 } 505 506 // If no `Size` or `throwaway` field is given, then 507 // it cannot be determined whether the layer is empty 508 // from the history, return false 509 return false, nil 510 } 511 512 type signature struct { 513 Signatures []jsParsedSignature `json:"signatures"` 514 } 515 516 type jsParsedSignature struct { 517 Protected string `json:"protected"` 518 } 519 520 type protectedBlock struct { 521 Length int `json:"formatLength"` 522 Tail string `json:"formatTail"` 523 } 524 525 // joseBase64UrlDecode decodes the given string using the standard base64 url 526 // decoder but first adds the appropriate number of trailing '=' characters in 527 // accordance with the jose specification. 528 // http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2 529 func joseBase64UrlDecode(s string) ([]byte, error) { 530 switch len(s) % 4 { 531 case 0: 532 case 2: 533 s += "==" 534 case 3: 535 s += "=" 536 default: 537 return nil, errors.New("illegal base64url string") 538 } 539 return base64.URLEncoding.DecodeString(s) 540 } 541 542 func stripSignature(b []byte) ([]byte, error) { 543 var sig signature 544 if err := json.Unmarshal(b, &sig); err != nil { 545 return nil, err 546 } 547 if len(sig.Signatures) == 0 { 548 return nil, errors.New("no signatures") 549 } 550 pb, err := joseBase64UrlDecode(sig.Signatures[0].Protected) 551 if err != nil { 552 return nil, errors.Wrapf(err, "could not decode %s", sig.Signatures[0].Protected) 553 } 554 555 var protected protectedBlock 556 if err := json.Unmarshal(pb, &protected); err != nil { 557 return nil, err 558 } 559 560 if protected.Length > len(b) { 561 return nil, errors.New("invalid protected length block") 562 } 563 564 tail, err := joseBase64UrlDecode(protected.Tail) 565 if err != nil { 566 return nil, errors.Wrap(err, "invalid tail base 64 value") 567 } 568 569 return append(b[:protected.Length], tail...), nil 570 } 571 572 type blobStateCalculator struct { 573 empty bool 574 digester digest.Digester 575 } 576 577 func newBlobStateCalculator() *blobStateCalculator { 578 return &blobStateCalculator{ 579 empty: true, 580 digester: digest.Canonical.Digester(), 581 } 582 } 583 584 func (c *blobStateCalculator) Write(p []byte) (int, error) { 585 if c.empty { 586 for _, b := range p { 587 if b != 0x00 { 588 c.empty = false 589 break 590 } 591 } 592 } 593 return c.digester.Hash().Write(p) 594 } 595 596 func (c *blobStateCalculator) State() blobState { 597 return blobState{ 598 empty: c.empty, 599 diffID: c.digester.Digest(), 600 } 601 }