github.com/estesp/manifest-tool@v1.0.3/docker/inspect_v2.go (about) 1 package docker 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "runtime" 8 "strings" 9 10 "github.com/docker/distribution" 11 "github.com/docker/distribution/manifest/manifestlist" 12 "github.com/docker/distribution/manifest/schema1" 13 "github.com/docker/distribution/manifest/schema2" 14 "github.com/docker/distribution/reference" 15 "github.com/docker/distribution/registry/api/errcode" 16 engineTypes "github.com/docker/docker/api/types" 17 dockerdistribution "github.com/docker/docker/distribution" 18 "github.com/docker/docker/image" 19 "github.com/docker/docker/image/v1" 20 "github.com/docker/docker/registry" 21 "github.com/estesp/manifest-tool/types" 22 "github.com/opencontainers/go-digest" 23 "github.com/sirupsen/logrus" 24 "golang.org/x/net/context" 25 ) 26 27 type v2ManifestFetcher struct { 28 endpoint registry.APIEndpoint 29 repoInfo *registry.RepositoryInfo 30 repo distribution.Repository 31 confirmedV2 bool 32 includeTags bool 33 authConfig engineTypes.AuthConfig 34 service registry.Service 35 } 36 37 type manifestInfo struct { 38 blobDigests []digest.Digest 39 layers []string 40 digest digest.Digest 41 platform manifestlist.PlatformSpec 42 length int64 43 jsonBytes []byte 44 } 45 46 func (mf *v2ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) ([]types.ImageInspect, error) { 47 var err error 48 49 mf.repo, mf.confirmedV2, err = dockerdistribution.NewV2Repository(ctx, mf.repoInfo, mf.endpoint, nil, &mf.authConfig, "pull") 50 if err != nil { 51 logrus.Debugf("Error getting v2 registry: %v", err) 52 return nil, err 53 } 54 55 images, err := mf.fetchWithRepository(ctx, ref) 56 if err != nil { 57 if _, ok := err.(fallbackError); ok { 58 return nil, err 59 } 60 if continueOnError(err) { 61 logrus.Errorf("Error trying v2 registry: %v", err) 62 return nil, fallbackError{err: err, confirmedV2: mf.confirmedV2, transportOK: true} 63 } 64 } 65 for _, img := range images { 66 img.MediaType = schema2.MediaTypeManifest 67 } 68 return images, err 69 } 70 71 func (mf *v2ManifestFetcher) fetchWithRepository(ctx context.Context, ref reference.Named) ([]types.ImageInspect, error) { 72 var ( 73 manifest distribution.Manifest 74 tagOrDigest string // Used for logging/progress only 75 tagList []string 76 imageList = []types.ImageInspect{} 77 ) 78 79 manSvc, err := mf.repo.Manifests(ctx) 80 if err != nil { 81 return nil, err 82 } 83 ref = reference.TagNameOnly(ref) 84 85 if tagged, isTagged := ref.(reference.NamedTagged); isTagged { 86 // NOTE: not using TagService.Get, since it uses HEAD requests 87 // against the manifests endpoint, which are not supported by 88 // all registry versions. 89 manifest, err = manSvc.Get(ctx, "", distribution.WithTag(tagged.Tag())) 90 if err != nil { 91 return nil, allowV1Fallback(err) 92 } 93 tagOrDigest = tagged.Tag() 94 } else if digested, isDigested := ref.(reference.Canonical); isDigested { 95 manifest, err = manSvc.Get(ctx, digested.Digest()) 96 if err != nil { 97 return nil, err 98 } 99 tagOrDigest = digested.Digest().String() 100 } else { 101 return nil, fmt.Errorf("internal error: reference has neither a tag nor a digest: %s", ref.String()) 102 } 103 104 if manifest == nil { 105 return nil, fmt.Errorf("image manifest does not exist for tag or digest %q", tagOrDigest) 106 } 107 108 // If manSvc.Get succeeded, we can be confident that the registry on 109 // the other side speaks the v2 protocol. 110 mf.confirmedV2 = true 111 112 if mf.includeTags { 113 tagList, err = mf.repo.Tags(ctx).All(ctx) 114 if err != nil { 115 // If this repository doesn't exist on V2, we should 116 // permit a fallback to V1. 117 if !strings.Contains(err.Error(), "unauthorized") { 118 // only error out if the the "list all tags" endpoint isn't blocked by the registry 119 // some registries may have a reason to not allow complete tag list queries 120 return nil, allowV1Fallback(err) 121 } 122 } 123 } 124 125 var ( 126 images []*image.Image 127 mfInfos []manifestInfo 128 mediaType []string 129 ) 130 131 switch v := manifest.(type) { 132 case *schema1.SignedManifest: 133 image, mfInfo, err := mf.pullSchema1(ctx, ref, v) 134 images = append(images, image) 135 mfInfos = append(mfInfos, mfInfo) 136 mediaType = append(mediaType, schema1.MediaTypeManifest) 137 if err != nil { 138 return nil, err 139 } 140 case *schema2.DeserializedManifest: 141 image, mfInfo, err := mf.pullSchema2(ctx, ref, v) 142 images = append(images, image) 143 mfInfos = append(mfInfos, mfInfo) 144 mediaType = append(mediaType, schema2.MediaTypeManifest) 145 if err != nil { 146 return nil, err 147 } 148 case *manifestlist.DeserializedManifestList: 149 images, mfInfos, mediaType, err = mf.pullManifestList(ctx, ref, v) 150 if err != nil { 151 return nil, err 152 } 153 default: 154 return nil, errors.New("unsupported manifest format") 155 } 156 157 for idx, img := range images { 158 imgReturn := makeImageInspect(img, tagOrDigest, mfInfos[idx], mediaType[idx], tagList) 159 imageList = append(imageList, *imgReturn) 160 } 161 return imageList, nil 162 } 163 164 func (mf *v2ManifestFetcher) pullSchema1(ctx context.Context, ref reference.Named, unverifiedManifest *schema1.SignedManifest) (img *image.Image, mfInfo manifestInfo, err error) { 165 mfInfo = manifestInfo{} 166 var verifiedManifest *schema1.Manifest 167 verifiedManifest, err = verifySchema1Manifest(unverifiedManifest, ref) 168 if err != nil { 169 return nil, mfInfo, err 170 } 171 172 // remove duplicate layers and check parent chain validity 173 err = fixManifestLayers(verifiedManifest) 174 if err != nil { 175 return nil, mfInfo, err 176 } 177 178 // Image history converted to the new format 179 var history []image.History 180 181 // Note that the order of this loop is in the direction of bottom-most 182 // to top-most, so that the downloads slice gets ordered correctly. 183 for i := len(verifiedManifest.FSLayers) - 1; i >= 0; i-- { 184 var throwAway struct { 185 ThrowAway bool `json:"throwaway,omitempty"` 186 } 187 if err := json.Unmarshal([]byte(verifiedManifest.History[i].V1Compatibility), &throwAway); err != nil { 188 return nil, mfInfo, err 189 } 190 191 h, err := v1.HistoryFromConfig([]byte(verifiedManifest.History[i].V1Compatibility), throwAway.ThrowAway) 192 if err != nil { 193 return nil, mfInfo, err 194 } 195 history = append(history, h) 196 mfInfo.blobDigests = append(mfInfo.blobDigests, verifiedManifest.FSLayers[i].BlobSum) 197 } 198 199 seen := make(map[string]bool) 200 for i := 0; i < len(verifiedManifest.FSLayers); i++ { 201 digest := verifiedManifest.FSLayers[i].BlobSum.String() 202 if _, ok := seen[digest]; ok { 203 continue 204 } 205 seen[digest] = true 206 mfInfo.layers = append(mfInfo.layers, digest) 207 } 208 209 rootFS := image.NewRootFS() 210 configRaw, _ := makeRawConfigFromV1Config([]byte(verifiedManifest.History[0].V1Compatibility), rootFS, history) 211 212 config, err := json.Marshal(configRaw) 213 if err != nil { 214 return nil, mfInfo, err 215 } 216 217 img, err = image.NewFromJSON(config) 218 if err != nil { 219 return nil, mfInfo, err 220 } 221 222 mfInfo.digest = digest.FromBytes(unverifiedManifest.Canonical) 223 // add the size of the manifest to the info struct; needed for assembling proper 224 // manifest lists 225 mfInfo.length = int64(len(unverifiedManifest.Canonical)) 226 mfInfo.jsonBytes, err = unverifiedManifest.MarshalJSON() 227 if err != nil { 228 return nil, mfInfo, err 229 } 230 return img, mfInfo, nil 231 } 232 233 func verifySchema1Manifest(signedManifest *schema1.SignedManifest, ref reference.Named) (m *schema1.Manifest, err error) { 234 // If pull by digest, then verify the manifest digest. NOTE: It is 235 // important to do this first, before any other content validation. If the 236 // digest cannot be verified, don't even bother with those other things. 237 if digested, isCanonical := ref.(reference.Canonical); isCanonical { 238 verifier := digested.Digest().Verifier() 239 if _, err := verifier.Write(signedManifest.Canonical); err != nil { 240 return nil, err 241 } 242 if !verifier.Verified() { 243 err := fmt.Errorf("image verification failed for digest %s", digested.Digest()) 244 logrus.Error(err) 245 return nil, err 246 } 247 } 248 m = &signedManifest.Manifest 249 250 if m.SchemaVersion != 1 { 251 return nil, fmt.Errorf("unsupported schema version %d for %q", m.SchemaVersion, ref.String()) 252 } 253 if len(m.FSLayers) != len(m.History) { 254 return nil, fmt.Errorf("length of history not equal to number of layers for %q", ref.String()) 255 } 256 if len(m.FSLayers) == 0 { 257 return nil, fmt.Errorf("no FSLayers in manifest for %q", ref.String()) 258 } 259 return m, nil 260 } 261 262 func fixManifestLayers(m *schema1.Manifest) error { 263 imgs := make([]*image.V1Image, len(m.FSLayers)) 264 for i := range m.FSLayers { 265 img := &image.V1Image{} 266 267 if err := json.Unmarshal([]byte(m.History[i].V1Compatibility), img); err != nil { 268 return err 269 } 270 271 imgs[i] = img 272 if err := v1.ValidateID(img.ID); err != nil { 273 return err 274 } 275 } 276 277 if imgs[len(imgs)-1].Parent != "" && runtime.GOOS != "windows" { 278 // Windows base layer can point to a base layer parent that is not in manifest. 279 return errors.New("Invalid parent ID in the base layer of the image") 280 } 281 282 // check general duplicates to error instead of a deadlock 283 idmap := make(map[string]struct{}) 284 285 var lastID string 286 for _, img := range imgs { 287 // skip IDs that appear after each other, we handle those later 288 if _, exists := idmap[img.ID]; img.ID != lastID && exists { 289 return fmt.Errorf("ID %+v appears multiple times in manifest", img.ID) 290 } 291 lastID = img.ID 292 idmap[lastID] = struct{}{} 293 } 294 295 // backwards loop so that we keep the remaining indexes after removing items 296 for i := len(imgs) - 2; i >= 0; i-- { 297 if imgs[i].ID == imgs[i+1].ID { // repeated ID. remove and continue 298 m.FSLayers = append(m.FSLayers[:i], m.FSLayers[i+1:]...) 299 m.History = append(m.History[:i], m.History[i+1:]...) 300 } else if imgs[i].Parent != imgs[i+1].ID { 301 return fmt.Errorf("Invalid parent ID. Expected %v, got %v", imgs[i+1].ID, imgs[i].Parent) 302 } 303 } 304 305 return nil 306 } 307 308 func (mf *v2ManifestFetcher) pullSchema2(ctx context.Context, ref reference.Named, mfst *schema2.DeserializedManifest) (img *image.Image, mfInfo manifestInfo, err error) { 309 mfInfo.digest, err = schema2ManifestDigest(ref, mfst) 310 if err != nil { 311 return nil, mfInfo, err 312 } 313 314 target := mfst.Target() 315 316 configChan := make(chan []byte, 1) 317 errChan := make(chan error, 1) 318 var cancel func() 319 ctx, cancel = context.WithCancel(ctx) 320 321 // Pull the image config 322 go func() { 323 configJSON, err := mf.pullSchema2ImageConfig(ctx, target.Digest) 324 if err != nil { 325 errChan <- ImageConfigPullError{Err: err} 326 cancel() 327 return 328 } 329 configChan <- configJSON 330 }() 331 332 var ( 333 configJSON []byte // raw serialized image config 334 unmarshalledConfig image.Image // deserialized image config 335 ) 336 if runtime.GOOS == "windows" { 337 configJSON, unmarshalledConfig, err = receiveConfig(configChan, errChan) 338 if err != nil { 339 return nil, mfInfo, err 340 } 341 if unmarshalledConfig.RootFS == nil { 342 return nil, mfInfo, errors.New("image config has no rootfs section") 343 } 344 } 345 346 if configJSON == nil { 347 configJSON, unmarshalledConfig, err = receiveConfig(configChan, errChan) 348 if err != nil { 349 return nil, mfInfo, err 350 } 351 } 352 353 // collect all references so that we can ask for cross-repo blob 354 // mount in the cases where a manifest list is created from images 355 // outside the target repo 356 for _, descriptor := range mfst.References() { 357 mfInfo.blobDigests = append(mfInfo.blobDigests, descriptor.Digest) 358 } 359 for _, descriptor := range mfst.Layers { 360 mfInfo.layers = append(mfInfo.layers, descriptor.Digest.String()) 361 } 362 363 img, err = image.NewFromJSON(configJSON) 364 if err != nil { 365 return nil, mfInfo, err 366 } 367 // add the size of the manifest to the image response; needed for assembling proper 368 // manifest lists 369 _, mfBytes, err := mfst.Payload() 370 if err != nil { 371 return nil, mfInfo, err 372 } 373 mfInfo.length = int64(len(mfBytes)) 374 mfInfo.jsonBytes = mfBytes 375 return img, mfInfo, nil 376 } 377 378 func (mf *v2ManifestFetcher) pullSchema2ImageConfig(ctx context.Context, dgst digest.Digest) (configJSON []byte, err error) { 379 blobs := mf.repo.Blobs(ctx) 380 configJSON, err = blobs.Get(ctx, dgst) 381 if err != nil { 382 return nil, err 383 } 384 385 // Verify image config digest 386 verifier := dgst.Verifier() 387 if _, err := verifier.Write(configJSON); err != nil { 388 return nil, err 389 } 390 if !verifier.Verified() { 391 err := fmt.Errorf("image config verification failed for digest %s", dgst) 392 logrus.Error(err) 393 return nil, err 394 } 395 396 return configJSON, nil 397 } 398 399 func receiveConfig(configChan <-chan []byte, errChan <-chan error) ([]byte, image.Image, error) { 400 select { 401 case configJSON := <-configChan: 402 var unmarshalledConfig image.Image 403 if err := json.Unmarshal(configJSON, &unmarshalledConfig); err != nil { 404 return nil, image.Image{}, err 405 } 406 return configJSON, unmarshalledConfig, nil 407 case err := <-errChan: 408 return nil, image.Image{}, err 409 // Don't need a case for ctx.Done in the select because cancellation 410 // will trigger an error in p.pullSchema2ImageConfig. 411 } 412 } 413 414 // ImageConfigPullError is an error pulling the image config blob 415 // (only applies to schema2). 416 type ImageConfigPullError struct { 417 Err error 418 } 419 420 // Error returns the error string for ImageConfigPullError. 421 func (e ImageConfigPullError) Error() string { 422 return "error pulling image configuration: " + e.Err.Error() 423 } 424 425 // allowV1Fallback checks if the error is a possible reason to fallback to v1 426 // (even if confirmedV2 has been set already), and if so, wraps the error in 427 // a fallbackError with confirmedV2 set to false. Otherwise, it returns the 428 // error unmodified. 429 func allowV1Fallback(err error) error { 430 switch v := err.(type) { 431 case errcode.Errors: 432 if len(v) != 0 { 433 if v0, ok := v[0].(errcode.Error); ok && shouldV2Fallback(v0) { 434 return fallbackError{err: err, confirmedV2: false, transportOK: true} 435 } 436 } 437 case errcode.Error: 438 if shouldV2Fallback(v) { 439 return fallbackError{err: err, confirmedV2: false, transportOK: true} 440 } 441 } 442 return err 443 } 444 445 // schema2ManifestDigest computes the manifest digest, and, if pulling by 446 // digest, ensures that it matches the requested digest. 447 func schema2ManifestDigest(ref reference.Named, mfst distribution.Manifest) (digest.Digest, error) { 448 _, canonical, err := mfst.Payload() 449 if err != nil { 450 return "", err 451 } 452 453 // If pull by digest, then verify the manifest digest. 454 if digested, isDigested := ref.(reference.Canonical); isDigested { 455 verifier := digested.Digest().Verifier() 456 if _, err := verifier.Write(canonical); err != nil { 457 return "", err 458 } 459 if !verifier.Verified() { 460 err := fmt.Errorf("manifest verification failed for digest %s", digested.Digest()) 461 logrus.Error(err) 462 return "", err 463 } 464 return digested.Digest(), nil 465 } 466 467 return digest.FromBytes(canonical), nil 468 } 469 470 // pullManifestList handles "manifest lists" which point to various 471 // platform-specific manifests. 472 func (mf *v2ManifestFetcher) pullManifestList(ctx context.Context, ref reference.Named, mfstList *manifestlist.DeserializedManifestList) ([]*image.Image, []manifestInfo, []string, error) { 473 var ( 474 imageList = []*image.Image{} 475 mfInfos = []manifestInfo{} 476 mediaType = []string{} 477 ) 478 manifestListDigest, err := schema2ManifestDigest(ref, mfstList) 479 if err != nil { 480 return nil, nil, nil, err 481 } 482 logrus.Debugf("Pulling manifest list entries for ML digest %v", manifestListDigest) 483 484 // for displaying basic information on the "outer" manifest list entry, we will 485 // create the first entry in the returned arrays to hold the manifest list details: 486 mfInfos = append(mfInfos, manifestInfo{digest: manifestListDigest}) 487 mediaType = append(mediaType, mfstList.MediaType) 488 imageList = append(imageList, &image.Image{}) 489 490 for _, manifestDescriptor := range mfstList.Manifests { 491 manSvc, err := mf.repo.Manifests(ctx) 492 if err != nil { 493 return nil, nil, nil, err 494 } 495 496 thisDigest := manifestDescriptor.Digest 497 thisPlatform := manifestDescriptor.Platform 498 manifest, err := manSvc.Get(ctx, thisDigest) 499 if err != nil { 500 return nil, nil, nil, err 501 } 502 503 manifestRef, err := reference.WithDigest(ref, thisDigest) 504 if err != nil { 505 return nil, nil, nil, err 506 } 507 508 switch v := manifest.(type) { 509 case *schema1.SignedManifest: 510 img, mfInfo, err := mf.pullSchema1(ctx, manifestRef, v) 511 imageList = append(imageList, img) 512 mfInfo.platform = thisPlatform 513 mfInfos = append(mfInfos, mfInfo) 514 mediaType = append(mediaType, schema1.MediaTypeManifest) 515 if err != nil { 516 return nil, nil, nil, err 517 } 518 case *schema2.DeserializedManifest: 519 img, mfInfo, err := mf.pullSchema2(ctx, manifestRef, v) 520 imageList = append(imageList, img) 521 mfInfo.platform = thisPlatform 522 mfInfos = append(mfInfos, mfInfo) 523 mediaType = append(mediaType, schema2.MediaTypeManifest) 524 if err != nil { 525 return nil, nil, nil, err 526 } 527 default: 528 return nil, nil, nil, errors.New("unsupported manifest format") 529 } 530 } 531 532 return imageList, mfInfos, mediaType, err 533 }