github.com/estesp/manifest-tool@v1.0.3/docker/createml.go (about) 1 package docker 2 3 import ( 4 "bytes" 5 "fmt" 6 "net" 7 "net/http" 8 "net/url" 9 "strings" 10 "time" 11 12 "github.com/docker/distribution/manifest/manifestlist" 13 "github.com/docker/distribution/reference" 14 "github.com/docker/distribution/registry/api/v2" 15 "github.com/docker/distribution/registry/client/auth" 16 "github.com/docker/distribution/registry/client/transport" 17 "github.com/docker/docker/dockerversion" 18 "github.com/docker/docker/registry" 19 "github.com/opencontainers/go-digest" 20 "github.com/sirupsen/logrus" 21 22 "github.com/estesp/manifest-tool/types" 23 ) 24 25 // we will store up a list of blobs we must ask the registry 26 // to cross-mount into our target namespace 27 type blobMount struct { 28 FromRepo string 29 Digest string 30 } 31 32 // if we have mounted blobs referenced from manifests from 33 // outside the target repository namespace we will need to 34 // push them to our target's repo as they will be references 35 // from the final manifest list object we push 36 type manifestPush struct { 37 Name string 38 Digest string 39 JSONBytes []byte 40 MediaType string 41 } 42 43 // PutManifestList takes an authentication variable and a yaml spec struct and pushes an image list based on the spec 44 func PutManifestList(a *types.AuthInfo, yamlInput types.YAMLInput, ignoreMissing, insecure bool) (string, int, error) { 45 var ( 46 manifestList manifestlist.ManifestList 47 blobMountRequests []blobMount 48 manifestRequests []manifestPush 49 ) 50 51 // process the final image name reference for the manifest list 52 targetRef, err := reference.ParseNormalizedNamed(yamlInput.Image) 53 if err != nil { 54 return "", 0, fmt.Errorf("Error parsing name for manifest list (%s): %v", yamlInput.Image, err) 55 } 56 targetRepo, err := registry.ParseRepositoryInfo(targetRef) 57 if err != nil { 58 return "", 0, fmt.Errorf("Error parsing repository name for manifest list (%s): %v", yamlInput.Image, err) 59 } 60 targetEndpoint, repoName, err := setupRepo(targetRepo, insecure) 61 if err != nil { 62 return "", 0, fmt.Errorf("Error setting up repository endpoint and references for %q: %v", targetRef, err) 63 } 64 65 // Now create the manifest list payload by looking up the manifest schemas 66 // for the constituent images: 67 logrus.Info("Retrieving digests of images...") 68 for _, img := range yamlInput.Manifests { 69 mfstData, repoInfo, err := GetImageData(a, img.Image, insecure, false) 70 if err != nil { 71 // if ignoreMissing is true, we will skip this error and simply 72 // log a warning that we couldn't find it in the registry 73 if ignoreMissing { 74 logrus.Warnf("Couldn't find or access image reference %q. Skipping image.", img.Image) 75 continue 76 } 77 return "", 0, fmt.Errorf("Inspect of image %q failed with error: %v", img.Image, err) 78 } 79 if reference.Domain(repoInfo.Name) != reference.Domain(targetRepo.Name) { 80 return "", 0, fmt.Errorf("Cannot use source images from a different registry than the target image: %s != %s", reference.Domain(repoInfo.Name), reference.Domain(targetRepo.Name)) 81 } 82 if len(mfstData) > 1 { 83 // too many responses--can only happen if a manifest list was returned for the name lookup 84 return "", 0, fmt.Errorf("You specified a manifest list entry from a digest that points to a current manifest list. Manifest lists do not allow recursion") 85 } 86 // the non-manifest list case will always have exactly one manifest response 87 imgMfst := mfstData[0] 88 89 // fill os/arch from inspected image if not specified in input YAML 90 if img.Platform.OS == "" && img.Platform.Architecture == "" { 91 // prefer a full platform object, if one is already available (and appears to have meaningful content) 92 if imgMfst.Platform.OS != "" || imgMfst.Platform.Architecture != "" { 93 img.Platform = imgMfst.Platform 94 } else if imgMfst.Os != "" || imgMfst.Architecture != "" { 95 img.Platform.OS = imgMfst.Os 96 img.Platform.Architecture = imgMfst.Architecture 97 } 98 } 99 100 // if the origin image has OSFeature and/or OSVersion information, and 101 // these values were not specified in the creation YAML, then 102 // retain the origin values in the Platform definition for the manifest list: 103 if imgMfst.OSVersion != "" && img.Platform.OSVersion == "" { 104 img.Platform.OSVersion = imgMfst.OSVersion 105 } 106 if len(imgMfst.OSFeatures) > 0 && len(img.Platform.OSFeatures) == 0 { 107 img.Platform.OSFeatures = imgMfst.OSFeatures 108 } 109 110 // validate os/arch input 111 if !isValidOSArch(img.Platform.OS, img.Platform.Architecture, img.Platform.Variant) { 112 return "", 0, fmt.Errorf("Manifest entry for image %s has unsupported os/arch or os/arch/variant combination: %s/%s/%s", img.Image, img.Platform.OS, img.Platform.Architecture, img.Platform.Variant) 113 } 114 115 manifest := manifestlist.ManifestDescriptor{ 116 Platform: img.Platform, 117 } 118 manifest.Descriptor.Digest, err = digest.Parse(imgMfst.Digest) 119 manifest.Size = imgMfst.Size 120 manifest.MediaType = imgMfst.MediaType 121 122 if err != nil { 123 return "", 0, fmt.Errorf("Digest parse of image %q failed with error: %v", img.Image, err) 124 } 125 logrus.Infof("Image %q is digest %s; size: %d", img.Image, imgMfst.Digest, imgMfst.Size) 126 127 // if this image is in a different repo, we need to add the layer & config digests to the list of 128 // requested blob mounts (cross-repository push) before pushing the manifest list 129 if repoName != reference.Path(repoInfo.Name) { 130 logrus.Debugf("Adding manifest references of %q to blob mount requests", img.Image) 131 for _, layer := range imgMfst.References { 132 blobMountRequests = append(blobMountRequests, blobMount{FromRepo: reference.Path(repoInfo.Name), Digest: layer}) 133 } 134 // also must add the manifest to be pushed in the target namespace 135 logrus.Debugf("Adding manifest %q -> to be pushed to %q as a manifest reference", reference.Path(repoInfo.Name), repoName) 136 manifestRequests = append(manifestRequests, manifestPush{ 137 Name: reference.Path(repoInfo.Name), 138 Digest: imgMfst.Digest, 139 JSONBytes: imgMfst.CanonicalJSON, 140 MediaType: imgMfst.MediaType, 141 }) 142 } 143 manifestList.Manifests = append(manifestList.Manifests, manifest) 144 } 145 146 if ignoreMissing && len(manifestList.Manifests) == 0 { 147 // we need to verify we at least have one valid entry in the list 148 // otherwise our manifest list will be totally empty 149 return "", 0, fmt.Errorf("all entries were skipped due to missing source image references; no manifest list to push") 150 } 151 // Set the schema version 152 manifestList.Versioned = manifestlist.SchemaVersion 153 154 urlBuilder, err := v2.NewURLBuilderFromString(targetEndpoint.URL.String(), false) 155 if err != nil { 156 return "", 0, fmt.Errorf("Can't create URL builder from endpoint (%s): %v", targetEndpoint.URL.String(), err) 157 } 158 pushURL, err := createManifestURLFromRef(targetRef, urlBuilder) 159 if err != nil { 160 return "", 0, fmt.Errorf("Error setting up repository endpoint and references for %q: %v", targetRef, err) 161 } 162 logrus.Debugf("Manifest list push url: %s", pushURL) 163 164 deserializedManifestList, err := manifestlist.FromDescriptors(manifestList.Manifests) 165 if err != nil { 166 return "", 0, fmt.Errorf("Cannot deserialize manifest list: %v", err) 167 } 168 mediaType, p, err := deserializedManifestList.Payload() 169 logrus.Debugf("mediaType of manifestList: %s", mediaType) 170 if err != nil { 171 return "", 0, fmt.Errorf("Cannot retrieve payload for HTTP PUT of manifest list: %v", err) 172 173 } 174 manifestLen := len(p) 175 putRequest, err := http.NewRequest("PUT", pushURL, bytes.NewReader(p)) 176 if err != nil { 177 return "", 0, fmt.Errorf("HTTP PUT request creation failed: %v", err) 178 } 179 putRequest.Header.Set("Content-Type", mediaType) 180 181 httpClient, err := getHTTPClient(a, targetRepo, targetEndpoint, repoName) 182 if err != nil { 183 return "", 0, fmt.Errorf("Failed to setup HTTP client to repository: %v", err) 184 } 185 186 // before we push the manifest list, if we have any blob mount requests, we need 187 // to ask the registry to mount those blobs in our target so they are available 188 // as references 189 if err := mountBlobs(httpClient, urlBuilder, targetRef, blobMountRequests); err != nil { 190 return "", 0, fmt.Errorf("Couldn't mount blobs for cross-repository push: %v", err) 191 } 192 193 // we also must push any manifests that are referenced in the manifest list into 194 // the target namespace 195 if err := pushReferences(httpClient, urlBuilder, targetRef, manifestRequests); err != nil { 196 return "", 0, fmt.Errorf("Couldn't push manifests referenced in our manifest list: %v", err) 197 } 198 199 resp, err := httpClient.Do(putRequest) 200 if err != nil { 201 return "", 0, fmt.Errorf("V2 registry PUT of manifest list failed: %v", err) 202 } 203 defer resp.Body.Close() 204 205 var finalDigest string 206 if statusSuccess(resp.StatusCode) { 207 dgstHeader := resp.Header.Get("Docker-Content-Digest") 208 dgst, err := digest.Parse(dgstHeader) 209 if err != nil { 210 return "", 0, err 211 } 212 finalDigest = string(dgst) 213 } else { 214 return "", 0, fmt.Errorf("Registry push unsuccessful: response %d: %s", resp.StatusCode, resp.Status) 215 } 216 // if the YAML includes additional tags, push the added tag references. No other work 217 // should be required as we have already made sure all target blobs are cross-repo 218 // mounted and all referenced manifests are already pushed. 219 for _, tag := range yamlInput.Tags { 220 newRef, err := reference.WithTag(targetRef, tag) 221 if err != nil { 222 return "", 0, fmt.Errorf("Error creating tagged reference for added tag %q: %v", tag, err) 223 } 224 pushURL, err := createManifestURLFromRef(newRef, urlBuilder) 225 if err != nil { 226 return "", 0, fmt.Errorf("Error setting up repository endpoint and references for %q: %v", newRef, err) 227 } 228 logrus.Debugf("[extra tag %q] push url: %s", tag, pushURL) 229 putRequest, err := http.NewRequest("PUT", pushURL, bytes.NewReader(p)) 230 if err != nil { 231 return "", 0, fmt.Errorf("[extra tag %q] HTTP PUT request creation failed: %v", tag, err) 232 } 233 putRequest.Header.Set("Content-Type", mediaType) 234 resp, err := httpClient.Do(putRequest) 235 if err != nil { 236 return "", 0, fmt.Errorf("[extra tag %q] V2 registry PUT of manifest list failed: %v", tag, err) 237 } 238 defer resp.Body.Close() 239 240 if statusSuccess(resp.StatusCode) { 241 dgstHeader := resp.Header.Get("Docker-Content-Digest") 242 dgst, err := digest.Parse(dgstHeader) 243 if err != nil { 244 return "", 0, err 245 } 246 if string(dgst) != finalDigest { 247 logrus.Warnf("Extra tag %q push resulted in non-matching digest %s (should be %s", tag, string(dgst), finalDigest) 248 } 249 } else { 250 return "", 0, fmt.Errorf("[extra tag %q] Registry push unsuccessful: response %d: %s", tag, resp.StatusCode, resp.Status) 251 } 252 } 253 return finalDigest, manifestLen, nil 254 } 255 256 func getHTTPClient(a *types.AuthInfo, repoInfo *registry.RepositoryInfo, endpoint registry.APIEndpoint, repoName string) (*http.Client, error) { 257 // get the http transport, this will be used in a client to upload manifest 258 // TODO - add separate function get client 259 base := &http.Transport{ 260 Proxy: http.ProxyFromEnvironment, 261 Dial: (&net.Dialer{ 262 Timeout: 30 * time.Second, 263 KeepAlive: 30 * time.Second, 264 DualStack: true, 265 }).Dial, 266 TLSHandshakeTimeout: 10 * time.Second, 267 TLSClientConfig: endpoint.TLSConfig, 268 DisableKeepAlives: true, 269 } 270 authConfig, err := getAuthConfig(a, repoInfo.Index) 271 if err != nil { 272 return nil, fmt.Errorf("Cannot retrieve authconfig: %v", err) 273 } 274 modifiers := registry.Headers(dockerversion.DockerUserAgent(nil), http.Header{}) 275 authTransport := transport.NewTransport(base, modifiers...) 276 challengeManager, _, err := registry.PingV2Registry(endpoint.URL, authTransport) 277 if err != nil { 278 return nil, fmt.Errorf("Ping of V2 registry failed: %v", err) 279 } 280 if authConfig.RegistryToken != "" { 281 passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken} 282 modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler)) 283 } else { 284 creds := dumbCredentialStore{auth: &authConfig} 285 tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, "push", "pull") 286 basicHandler := auth.NewBasicHandler(creds) 287 modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) 288 } 289 tr := transport.NewTransport(base, modifiers...) 290 291 httpClient := &http.Client{ 292 Transport: tr, 293 CheckRedirect: checkHTTPRedirect, 294 } 295 return httpClient, nil 296 } 297 298 func createManifestURLFromRef(targetRef reference.Named, urlBuilder *v2.URLBuilder) (string, error) { 299 // get rid of hostname so the target URL is constructed properly 300 hostname, name := splitHostname(targetRef.String()) 301 targetRef, err := getNamedRefWithoutHostname(name) 302 if err != nil { 303 return "", fmt.Errorf("Can't parse target image repository name from reference: %v", err) 304 } 305 306 // Set the tag to latest, if no tag found in YAML 307 if _, isTagged := targetRef.(reference.NamedTagged); !isTagged { 308 targetRef = reference.TagNameOnly(targetRef) 309 } else { 310 tagged, _ := targetRef.(reference.NamedTagged) 311 targetRef, err = reference.WithTag(targetRef, tagged.Tag()) 312 if err != nil { 313 return "", fmt.Errorf("Error referencing specified tag to target repository name: %v", err) 314 } 315 } 316 317 manifestURL, err := buildManifestURL(urlBuilder, hostname, targetRef) 318 if err != nil { 319 return "", fmt.Errorf("Failed to build manifest URL from target reference: %v", err) 320 } 321 return manifestURL, nil 322 } 323 324 func setupRepo(repoInfo *registry.RepositoryInfo, insecure bool) (registry.APIEndpoint, string, error) { 325 326 options := registry.ServiceOptions{} 327 if insecure { 328 options.InsecureRegistries = append(options.InsecureRegistries, reference.Domain(repoInfo.Name)) 329 } 330 registryService, err := registry.NewService(options) 331 if err != nil { 332 return registry.APIEndpoint{}, "", err 333 } 334 335 endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoInfo.Name)) 336 if err != nil { 337 return registry.APIEndpoint{}, "", err 338 } 339 logrus.Debugf("endpoints: %v", endpoints) 340 // take highest priority endpoint 341 endpoint := endpoints[0] 342 // if insecure, and there is an "http" endpoint, prefer that 343 if insecure { 344 for _, ep := range endpoints { 345 if ep.URL.Scheme == "http" { 346 endpoint = ep 347 } 348 } 349 endpoint.TLSConfig.InsecureSkipVerify = true 350 } 351 352 repoName := repoInfo.Name.Name() 353 // If endpoint does not support CanonicalName, use the Name's path instead 354 if endpoint.TrimHostname { 355 repoName = reference.Path(repoInfo.Name) 356 logrus.Debugf("repoName: %v", repoName) 357 } 358 return endpoint, repoName, nil 359 } 360 361 func pushReferences(httpClient *http.Client, urlBuilder *v2.URLBuilder, ref reference.Named, manifests []manifestPush) error { 362 // for each referenced manifest object in the manifest list (that is outside of our current repo/name) 363 // we need to push by digest the manifest so that it is added as a valid reference in the current 364 // repo. This will allow us to push the manifest list properly later and have all valid references. 365 366 // first get rid of possible hostname so the target URL is constructed properly 367 hostname, name := splitHostname(ref.String()) 368 ref, err := getNamedRefWithoutHostname(name) 369 if err != nil { 370 return fmt.Errorf("Error parsing repo/name portion of reference without hostname: %s: %v", name, err) 371 } 372 for _, manifest := range manifests { 373 dgst, err := digest.Parse(manifest.Digest) 374 if err != nil { 375 return fmt.Errorf("Error parsing manifest digest (%s) for referenced manifest %q: %v", manifest.Digest, manifest.Name, err) 376 } 377 targetRef, err := reference.WithDigest(reference.TrimNamed(ref), dgst) 378 if err != nil { 379 return fmt.Errorf("Error creating manifest digest target for referenced manifest %q: %v", manifest.Name, err) 380 } 381 pushURL, err := buildManifestURL(urlBuilder, hostname, targetRef) 382 if err != nil { 383 return fmt.Errorf("Error setting up manifest push URL for manifest references for %q: %v", manifest.Name, err) 384 } 385 logrus.Debugf("manifest reference push URL: %s", pushURL) 386 387 pushRequest, err := http.NewRequest("PUT", pushURL, bytes.NewReader(manifest.JSONBytes)) 388 if err != nil { 389 return fmt.Errorf("HTTP PUT request creation for manifest reference push failed: %v", err) 390 } 391 pushRequest.Header.Set("Content-Type", manifest.MediaType) 392 resp, err := httpClient.Do(pushRequest) 393 if err != nil { 394 return fmt.Errorf("PUT of manifest reference failed: %v", err) 395 } 396 397 resp.Body.Close() 398 if !statusSuccess(resp.StatusCode) { 399 return fmt.Errorf("Referenced manifest push unsuccessful: response %d: %s", resp.StatusCode, resp.Status) 400 } 401 dgstHeader := resp.Header.Get("Docker-Content-Digest") 402 dgstResult, err := digest.Parse(dgstHeader) 403 if err != nil { 404 return fmt.Errorf("Couldn't parse pushed manifest digest response: %v", err) 405 } 406 if string(dgstResult) != manifest.Digest { 407 return fmt.Errorf("Pushed referenced manifest received a different digest: expected %s, got %s", manifest.Digest, string(dgst)) 408 } 409 logrus.Debugf("referenced manifest %q pushed; digest matches: %s", manifest.Name, string(dgst)) 410 } 411 return nil 412 } 413 414 func mountBlobs(httpClient *http.Client, urlBuilder *v2.URLBuilder, ref reference.Named, blobsRequested []blobMount) error { 415 // get rid of hostname so the target URL is constructed properly 416 hostname, name := splitHostname(ref.String()) 417 targetRef, err := getNamedRefWithoutHostname(name) 418 if err != nil { 419 return fmt.Errorf("Can't parse reference without hostname: %v", err) 420 } 421 422 for _, blob := range blobsRequested { 423 // create URL request 424 url, err := buildBlobUploadURL(urlBuilder, hostname, targetRef, url.Values{"from": {blob.FromRepo}, "mount": {blob.Digest}}) 425 if err != nil { 426 return fmt.Errorf("Failed to create blob mount URL: %v", err) 427 } 428 mountRequest, err := http.NewRequest("POST", url, nil) 429 if err != nil { 430 return fmt.Errorf("HTTP POST request creation for blob mount failed: %v", err) 431 } 432 mountRequest.Header.Set("Content-Length", "0") 433 resp, err := httpClient.Do(mountRequest) 434 if err != nil { 435 return fmt.Errorf("V2 registry POST of blob mount failed: %v", err) 436 } 437 438 resp.Body.Close() 439 if !statusSuccess(resp.StatusCode) { 440 return fmt.Errorf("Blob mount failed to url %s: HTTP status %d", url, resp.StatusCode) 441 } 442 logrus.Debugf("Mount of blob %s succeeded, location: %q", blob.Digest, resp.Header.Get("Location")) 443 } 444 return nil 445 } 446 447 func buildManifestURL(ub *v2.URLBuilder, hostname string, targetRef reference.Named) (string, error) { 448 if !isHubLibraryRef(targetRef, hostname) { 449 return ub.BuildManifestURL(targetRef) 450 } 451 // this is a library reference and we don't want to lose the "library/" part of the URL ref 452 baseURL, err := ub.BuildBaseURL() 453 if err != nil { 454 return "", err 455 } 456 tagOrDigest := "" 457 switch v := targetRef.(type) { 458 case reference.Tagged: 459 tagOrDigest = v.Tag() 460 case reference.Digested: 461 tagOrDigest = v.Digest().String() 462 } 463 baseURL = fmt.Sprintf("%s%s/%s/%s", baseURL, reference.Path(targetRef), "manifests", tagOrDigest) 464 return baseURL, nil 465 } 466 467 func buildBlobUploadURL(ub *v2.URLBuilder, hostname string, targetRef reference.Named, values url.Values) (string, error) { 468 if !isHubLibraryRef(targetRef, hostname) { 469 return ub.BuildBlobUploadURL(targetRef, values) 470 } 471 // this is a library reference and we don't want to lose the "library/" part of the URL ref 472 baseURL, err := ub.BuildBaseURL() 473 if err != nil { 474 return "", err 475 } 476 baseURL = fmt.Sprintf("%s%s/%s", baseURL, reference.Path(targetRef), "blobs/uploads/") 477 return appendValues(baseURL, values), nil 478 } 479 480 func isHubLibraryRef(targetRef reference.Named, hostname string) bool { 481 return strings.HasPrefix(reference.Path(targetRef), DefaultRepoPrefix) && hostname == DefaultHostname 482 } 483 484 func getNamedRefWithoutHostname(ref string) (reference.Named, error) { 485 targetRef, err := reference.Parse(ref) 486 if err != nil { 487 return nil, fmt.Errorf("Can't parse reference without hostname: %v", err) 488 } 489 named, isNamed := targetRef.(reference.Named) 490 if !isNamed { 491 return nil, fmt.Errorf("Parsed reference is not a Named object: %s", ref) 492 } 493 return named, nil 494 } 495 496 // NOTE: these two functions are copied from github.com/docker/distribution/registry/api/v2/urls.go 497 // to handle the issue of needing to preserve non-normalized names for pushing to "library/" on 498 // DockerHub 499 // 500 // appendValuesURL appends the parameters to the url. 501 func appendValuesURL(u *url.URL, values ...url.Values) *url.URL { 502 merged := u.Query() 503 504 for _, v := range values { 505 for k, vv := range v { 506 merged[k] = append(merged[k], vv...) 507 } 508 } 509 u.RawQuery = merged.Encode() 510 return u 511 } 512 513 // appendValues appends the parameters to the url. Panics if the string is not 514 // a url. 515 func appendValues(u string, values ...url.Values) string { 516 up, err := url.Parse(u) 517 518 if err != nil { 519 panic(err) // should never happen 520 } 521 522 return appendValuesURL(up, values...).String() 523 } 524 525 func statusSuccess(status int) bool { 526 return status >= 200 && status <= 399 527 }