diff --git a/integration/copy_test.go b/integration/copy_test.go index 29843b8c..f0e0b39c 100644 --- a/integration/copy_test.go +++ b/integration/copy_test.go @@ -96,8 +96,11 @@ func fileFromFixture(c *check.C, inputPath string, edits map[string]string) stri return path } -// The most basic (skopeo copy) use: -func (s *CopySuite) TestCopySimple(c *check.C) { +func (s *CopySuite) TestCopyFailsWithManifestList(c *check.C) { + assertSkopeoFails(c, ".*can not copy docker://estesp/busybox:latest: manifest contains multiple images.*", "copy", "docker://estesp/busybox:latest", "dir:somedir") +} + +func (s *CopySuite) TestCopySimpleAtomicRegistry(c *check.C) { dir1, err := ioutil.TempDir("", "copy-1") c.Assert(err, check.IsNil) defer os.RemoveAll(dir1) @@ -107,13 +110,35 @@ func (s *CopySuite) TestCopySimple(c *check.C) { // FIXME: It would be nice to use one of the local Docker registries instead of neeeding an Internet connection. // "pull": docker: → dir: - assertSkopeoSucceeds(c, "", "copy", "docker://estesp/busybox:latest", "dir:"+dir1) + assertSkopeoSucceeds(c, "", "copy", "docker://estesp/busybox:amd64", "dir:"+dir1) // "push": dir: → atomic: assertSkopeoSucceeds(c, "", "--tls-verify=false", "--debug", "copy", "dir:"+dir1, "atomic:localhost:5000/myns/unsigned:unsigned") // The result of pushing and pulling is an unmodified image. assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "atomic:localhost:5000/myns/unsigned:unsigned", "dir:"+dir2) out := combinedOutputOfCommand(c, "diff", "-urN", dir1, dir2) c.Assert(out, check.Equals, "") +} + +// The most basic (skopeo copy) use: +func (s *CopySuite) TestCopySimple(c *check.C) { + const ourRegistry = "docker://" + v2DockerRegistryURL + "/" + + dir1, err := ioutil.TempDir("", "copy-1") + c.Assert(err, check.IsNil) + defer os.RemoveAll(dir1) + dir2, err := ioutil.TempDir("", "copy-2") + c.Assert(err, check.IsNil) + defer os.RemoveAll(dir2) + + // FIXME: It would be nice to use one of the local Docker registries instead of neeeding an Internet connection. + // "pull": docker: → dir: + assertSkopeoSucceeds(c, "", "copy", "docker://busybox", "dir:"+dir1) + // "push": dir: → docker(v2s2): + assertSkopeoSucceeds(c, "", "--tls-verify=false", "--debug", "copy", "dir:"+dir1, ourRegistry+"busybox:unsigned") + // The result of pushing and pulling is an unmodified image. + assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", ourRegistry+"busybox:unsigned", "dir:"+dir2) + out := combinedOutputOfCommand(c, "diff", "-urN", dir1, dir2) + c.Assert(out, check.Equals, "") // docker v2s2 -> OCI image layout // ociDest will be created by oci: if it doesn't exist @@ -123,8 +148,6 @@ func (s *CopySuite) TestCopySimple(c *check.C) { assertSkopeoSucceeds(c, "", "copy", "docker://busybox:latest", "oci:"+ociDest) _, err = os.Stat(ociDest) c.Assert(err, check.IsNil) - - // FIXME: Also check pushing to docker:// } // Streaming (skopeo copy) diff --git a/vendor/github.com/containers/image/copy/copy.go b/vendor/github.com/containers/image/copy/copy.go index 91d4cef6..450827cc 100644 --- a/vendor/github.com/containers/image/copy/copy.go +++ b/vendor/github.com/containers/image/copy/copy.go @@ -112,6 +112,14 @@ func Image(ctx *types.SystemContext, policyContext *signature.PolicyContext, des src := image.FromSource(rawSource) defer src.Close() + multiImage, err := src.IsMultiImage() + if err != nil { + return err + } + if multiImage { + return fmt.Errorf("can not copy %s: manifest contains multiple images", transports.ImageName(srcRef)) + } + // Please keep this policy check BEFORE reading any other information about the image. if allowed, err := policyContext.IsRunningImageAllowed(src); !allowed || err != nil { // Be paranoid and fail if either return value indicates so. return fmt.Errorf("Source image rejected: %v", err) diff --git a/vendor/github.com/containers/image/directory/directory_src.go b/vendor/github.com/containers/image/directory/directory_src.go index c87b0a3b..16ab50f9 100644 --- a/vendor/github.com/containers/image/directory/directory_src.go +++ b/vendor/github.com/containers/image/directory/directory_src.go @@ -1,6 +1,7 @@ package directory import ( + "fmt" "io" "io/ioutil" "os" @@ -37,6 +38,10 @@ func (s *dirImageSource) GetManifest() ([]byte, string, error) { return m, "", err } +func (s *dirImageSource) GetTargetManifest(digest string) ([]byte, string, error) { + return nil, "", fmt.Errorf("Getting target manifest not supported by dir:") +} + // GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown). func (s *dirImageSource) GetBlob(digest string) (io.ReadCloser, int64, error) { r, err := os.Open(s.ref.layerPath(digest)) diff --git a/vendor/github.com/containers/image/docker/docker_client.go b/vendor/github.com/containers/image/docker/docker_client.go index 5900b458..ae986de4 100644 --- a/vendor/github.com/containers/image/docker/docker_client.go +++ b/vendor/github.com/containers/image/docker/docker_client.go @@ -243,49 +243,58 @@ func (c *dockerClient) getBearerToken(realm, service, scope string) (string, err return tokenStruct.Token, nil } -func getAuth(hostname string) (string, string, error) { +func getAuth(registry string) (string, string, error) { // TODO(runcom): get this from *cli.Context somehow //if username != "" && password != "" { //return username, password, nil //} - if hostname == dockerHostname { - hostname = dockerAuthRegistry - } + var dockerAuth dockerConfigFile dockerCfgPath := filepath.Join(getDefaultConfigDir(".docker"), dockerCfgFileName) if _, err := os.Stat(dockerCfgPath); err == nil { j, err := ioutil.ReadFile(dockerCfgPath) if err != nil { return "", "", err } - var dockerAuth dockerConfigFile if err := json.Unmarshal(j, &dockerAuth); err != nil { return "", "", err } - // try the normal case - if c, ok := dockerAuth.AuthConfigs[hostname]; ok { - return decodeDockerAuth(c.Auth) - } + } else if os.IsNotExist(err) { + // try old config path oldDockerCfgPath := filepath.Join(getDefaultConfigDir(dockerCfgObsolete)) if _, err := os.Stat(oldDockerCfgPath); err != nil { - return "", "", nil //missing file is not an error + if os.IsNotExist(err) { + return "", "", nil + } + return "", "", fmt.Errorf("%s - %v", oldDockerCfgPath, err) } + j, err := ioutil.ReadFile(oldDockerCfgPath) if err != nil { return "", "", err } - var dockerAuthOld map[string]dockerAuthConfigObsolete - if err := json.Unmarshal(j, &dockerAuthOld); err != nil { + if err := json.Unmarshal(j, &dockerAuth.AuthConfigs); err != nil { return "", "", err } - if c, ok := dockerAuthOld[hostname]; ok { - return decodeDockerAuth(c.Auth) - } - } else { - // if file is there but we can't stat it for any reason other - // than it doesn't exist then stop + + } else if err != nil { return "", "", fmt.Errorf("%s - %v", dockerCfgPath, err) } + + // I'm feeling lucky + if c, exists := dockerAuth.AuthConfigs[registry]; exists { + return decodeDockerAuth(c.Auth) + } + + // bad luck; let's normalize the entries first + registry = normalizeRegistry(registry) + normalizedAuths := map[string]dockerAuthConfig{} + for k, v := range dockerAuth.AuthConfigs { + normalizedAuths[normalizeRegistry(k)] = v + } + if c, exists := normalizedAuths[registry]; exists { + return decodeDockerAuth(c.Auth) + } return "", "", nil } @@ -342,10 +351,6 @@ func getDefaultConfigDir(confPath string) string { return filepath.Join(homedir.Get(), confPath) } -type dockerAuthConfigObsolete struct { - Auth string `json:"auth"` -} - type dockerAuthConfig struct { Auth string `json:"auth,omitempty"` } @@ -368,3 +373,28 @@ func decodeDockerAuth(s string) (string, string, error) { password := strings.Trim(parts[1], "\x00") return user, password, nil } + +// convertToHostname converts a registry url which has http|https prepended +// to just an hostname. +// Copied from github.com/docker/docker/registry/auth.go +func convertToHostname(url string) string { + stripped := url + if strings.HasPrefix(url, "http://") { + stripped = strings.TrimPrefix(url, "http://") + } else if strings.HasPrefix(url, "https://") { + stripped = strings.TrimPrefix(url, "https://") + } + + nameParts := strings.SplitN(stripped, "/", 2) + + return nameParts[0] +} + +func normalizeRegistry(registry string) string { + normalized := convertToHostname(registry) + switch normalized { + case "registry-1.docker.io", "docker.io": + return "index.docker.io" + } + return normalized +} diff --git a/vendor/github.com/containers/image/docker/docker_image_src.go b/vendor/github.com/containers/image/docker/docker_image_src.go index 47d47f72..627c778d 100644 --- a/vendor/github.com/containers/image/docker/docker_image_src.go +++ b/vendor/github.com/containers/image/docker/docker_image_src.go @@ -15,12 +15,13 @@ import ( "github.com/containers/image/types" ) -type errFetchManifest struct { +// ErrFetchManifest provides the error when fetching the manifest fails +type ErrFetchManifest struct { statusCode int body []byte } -func (e errFetchManifest) Error() string { +func (e ErrFetchManifest) Error() string { return fmt.Sprintf("error fetching manifest: status code: %d, body: %s", e.statusCode, string(e.body)) } @@ -83,6 +84,31 @@ func (s *dockerImageSource) GetManifest() ([]byte, string, error) { return s.cachedManifest, s.cachedManifestMIMEType, nil } +func (s *dockerImageSource) fetchManifest(tagOrDigest string) ([]byte, string, error) { + url := fmt.Sprintf(manifestURL, s.ref.ref.RemoteName(), tagOrDigest) + headers := make(map[string][]string) + headers["Accept"] = s.requestedManifestMIMETypes + res, err := s.c.makeRequest("GET", url, headers, nil) + if err != nil { + return nil, "", err + } + defer res.Body.Close() + manblob, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, "", err + } + if res.StatusCode != http.StatusOK { + return nil, "", ErrFetchManifest{res.StatusCode, manblob} + } + return manblob, simplifyContentType(res.Header.Get("Content-Type")), nil +} + +// GetTargetManifest returns an image's manifest given a digest. +// This is mainly used to retrieve a single image's manifest out of a manifest list. +func (s *dockerImageSource) GetTargetManifest(digest string) ([]byte, string, error) { + return s.fetchManifest(digest) +} + // ensureManifestIsLoaded sets s.cachedManifest and s.cachedManifestMIMEType // // ImageSource implementations are not required or expected to do any caching, @@ -99,26 +125,14 @@ func (s *dockerImageSource) ensureManifestIsLoaded() error { if err != nil { return err } - url := fmt.Sprintf(manifestURL, s.ref.ref.RemoteName(), reference) - // TODO(runcom) set manifest version header! schema1 for now - then schema2 etc etc and v1 - // TODO(runcom) NO, switch on the resulter manifest like Docker is doing - headers := make(map[string][]string) - headers["Accept"] = s.requestedManifestMIMETypes - res, err := s.c.makeRequest("GET", url, headers, nil) + + manblob, mt, err := s.fetchManifest(reference) if err != nil { return err } - defer res.Body.Close() - manblob, err := ioutil.ReadAll(res.Body) - if err != nil { - return err - } - if res.StatusCode != http.StatusOK { - return errFetchManifest{res.StatusCode, manblob} - } // We might validate manblob against the Docker-Content-Digest header here to protect against transport errors. s.cachedManifest = manblob - s.cachedManifestMIMEType = simplifyContentType(res.Header.Get("Content-Type")) + s.cachedManifestMIMEType = mt return nil } diff --git a/vendor/github.com/containers/image/image/docker_list.go b/vendor/github.com/containers/image/image/docker_list.go new file mode 100644 index 00000000..57f1763e --- /dev/null +++ b/vendor/github.com/containers/image/image/docker_list.go @@ -0,0 +1,52 @@ +package image + +import ( + "encoding/json" + "errors" + "runtime" + + "github.com/containers/image/types" +) + +type platformSpec struct { + Architecture string `json:"architecture"` + OS string `json:"os"` + OSVersion string `json:"os.version,omitempty"` + OSFeatures []string `json:"os.features,omitempty"` + Variant string `json:"variant,omitempty"` + Features []string `json:"features,omitempty"` +} + +// A manifestDescriptor references a platform-specific manifest. +type manifestDescriptor struct { + descriptor + Platform platformSpec `json:"platform"` +} + +type manifestList struct { + SchemaVersion int `json:"schemaVersion"` + MediaType string `json:"mediaType"` + Manifests []manifestDescriptor `json:"manifests"` +} + +func manifestSchema2FromManifestList(src types.ImageSource, manblob []byte) (genericManifest, error) { + list := manifestList{} + if err := json.Unmarshal(manblob, &list); err != nil { + return nil, err + } + var targetManifestDigest string + for _, d := range list.Manifests { + if d.Platform.Architecture == runtime.GOARCH && d.Platform.OS == runtime.GOOS { + targetManifestDigest = d.Digest + break + } + } + if targetManifestDigest == "" { + return nil, errors.New("no supported platform found in manifest list") + } + manblob, mt, err := src.GetTargetManifest(targetManifestDigest) + if err != nil { + return nil, err + } + return manifestInstanceFromBlob(src, manblob, mt) +} diff --git a/vendor/github.com/containers/image/image/image.go b/vendor/github.com/containers/image/image/image.go index 2dda50a2..23c2a0a2 100644 --- a/vendor/github.com/containers/image/image/image.go +++ b/vendor/github.com/containers/image/image/image.go @@ -120,6 +120,18 @@ func (i *genericImage) getParsedManifest() (genericManifest, error) { if err != nil { return nil, err } + return manifestInstanceFromBlob(i.src, manblob, mt) +} + +func (i *genericImage) IsMultiImage() (bool, error) { + _, mt, err := i.Manifest() + if err != nil { + return false, err + } + return mt == manifest.DockerV2ListMediaType, nil +} + +func manifestInstanceFromBlob(src types.ImageSource, manblob []byte, mt string) (genericManifest, error) { switch mt { // "application/json" is a valid v2s1 value per https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md . // This works for now, when nothing else seems to return "application/json"; if that were not true, the mapping/detection might @@ -127,7 +139,9 @@ func (i *genericImage) getParsedManifest() (genericManifest, error) { case manifest.DockerV2Schema1MediaType, manifest.DockerV2Schema1SignedMediaType, "application/json": return manifestSchema1FromManifest(manblob) case manifest.DockerV2Schema2MediaType: - return manifestSchema2FromManifest(i.src, manblob) + return manifestSchema2FromManifest(src, manblob) + case manifest.DockerV2ListMediaType: + return manifestSchema2FromManifestList(src, manblob) case "": return nil, errors.New("could not guess manifest media type") default: diff --git a/vendor/github.com/containers/image/manifest/manifest.go b/vendor/github.com/containers/image/manifest/manifest.go index 80f57083..2e4ba206 100644 --- a/vendor/github.com/containers/image/manifest/manifest.go +++ b/vendor/github.com/containers/image/manifest/manifest.go @@ -30,6 +30,7 @@ var DefaultRequestedManifestMIMETypes = []string{ DockerV2Schema2MediaType, DockerV2Schema1SignedMediaType, DockerV2Schema1MediaType, + DockerV2ListMediaType, } // GuessMIMEType guesses MIME type of a manifest and returns it _if it is recognized_, or "" if unknown or unrecognized. diff --git a/vendor/github.com/containers/image/openshift/openshift.go b/vendor/github.com/containers/image/openshift/openshift.go index 0451eaee..e77a175b 100644 --- a/vendor/github.com/containers/image/openshift/openshift.go +++ b/vendor/github.com/containers/image/openshift/openshift.go @@ -196,6 +196,13 @@ func (s *openshiftImageSource) Close() { } } +func (s *openshiftImageSource) GetTargetManifest(digest string) ([]byte, string, error) { + if err := s.ensureImageIsResolved(); err != nil { + return nil, "", err + } + return s.docker.GetTargetManifest(digest) +} + func (s *openshiftImageSource) GetManifest() ([]byte, string, error) { if err := s.ensureImageIsResolved(); err != nil { return nil, "", err diff --git a/vendor/github.com/containers/image/types/types.go b/vendor/github.com/containers/image/types/types.go index c9c296f2..c4a15b3b 100644 --- a/vendor/github.com/containers/image/types/types.go +++ b/vendor/github.com/containers/image/types/types.go @@ -106,6 +106,9 @@ type ImageSource interface { // GetManifest returns the image's manifest along with its MIME type. The empty string is returned if the MIME type is unknown. // It may use a remote (= slow) service. GetManifest() ([]byte, string, error) + // GetTargetManifest returns an image's manifest given a digest. This is mainly used to retrieve a single image's manifest + // out of a manifest list. + GetTargetManifest(digest string) ([]byte, string, error) // GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown). GetBlob(digest string) (io.ReadCloser, int64, error) // GetSignatures returns the image's signatures. It may use a remote (= slow) service. @@ -180,6 +183,8 @@ type Image interface { // UpdatedManifest returns the image's manifest modified according to options. // This does not change the state of the Image object. UpdatedManifest(options ManifestUpdateOptions) ([]byte, error) + // IsMultiImage returns true if the image's manifest is a list of images, false otherwise. + IsMultiImage() (bool, error) } // ManifestUpdateOptions is a way to pass named optional arguments to Image.UpdatedManifest