Przeglądaj źródła

support fetching images in OCI format

this enables fetching images and then `docker load`ing them in a build
Alex Suraci 7 lat temu
rodzic
commit
19f7e0ed1a
6 zmienionych plików z 145 dodań i 28 usunięć
  1. 22 6
      README.md
  2. 7 7
      check_test.go
  3. 36 8
      cmd/in/main.go
  4. 38 6
      in_test.go
  5. 30 1
      suite_test.go
  6. 12 0
      types.go

+ 22 - 6
README.md

@@ -41,17 +41,33 @@ Reports the current digest that the registry has for the given tag.
 
 ### `in`: Fetch the image's rootfs and metadata.
 
-This image is meant to be used for fetching Concourse task images and
-pipeline-provided resource types.
+#### Parameters
 
-So by default, this resource will produce the following files:
+* `format`: *Optional. Default `rootfs`.* The format to fetch as. (See below.)
+
+
+#### Formats
+
+##### `rootfs`
+
+The `rootfs` format will fetch and unpack the image for use by Concourse task
+and resource type images.
+
+This the default for the sake of brevity in pipelines and task configs.
+
+In this format, the resource will produce the following files:
 
 * `rootfs/...`: the unpacked rootfs produced by the image.
 * `metadata.json`: the runtime information to propagate to Concourse.
 
-In a later release, it might be a good idea to optionally fetch the image in
-other formats that are more useful for e.g. Docker's tooling (perhaps a file
-equivalent to `docker save`).
+##### `oci`
+
+The `oci` format will fetch the image and write it to disk in OCI format. This
+is analogous to running `docker save`.
+
+In this format, the resource will produce the following files:
+
+* `image.tar`: the OCI image tarball, suitable for passing to `docker load`.
 
 
 ### `out`: Push an image up to the registry under the given tags.

+ 7 - 7
check_test.go

@@ -50,7 +50,7 @@ var _ = Describe("Check", func() {
 
 		It("returns the current digest", func() {
 			Expect(res).To(Equal([]resource.Version{
-				{Digest: "sha256:64a6988c58cbdd634198f56452e8f8945e5b54a4bbca4bff7e960e1c830671ff"},
+				{Digest: LATEST_STATIC_DIGEST},
 			}))
 		})
 	})
@@ -63,13 +63,13 @@ var _ = Describe("Check", func() {
 			}
 
 			req.Version = &resource.Version{
-				Digest: "sha256:64a6988c58cbdd634198f56452e8f8945e5b54a4bbca4bff7e960e1c830671ff",
+				Digest: LATEST_STATIC_DIGEST,
 			}
 		})
 
 		It("returns the given digest", func() {
 			Expect(res).To(Equal([]resource.Version{
-				{Digest: "sha256:64a6988c58cbdd634198f56452e8f8945e5b54a4bbca4bff7e960e1c830671ff"},
+				{Digest: LATEST_STATIC_DIGEST},
 			}))
 		})
 	})
@@ -83,14 +83,14 @@ var _ = Describe("Check", func() {
 
 			req.Version = &resource.Version{
 				// this was previously pushed to the 'latest' tag
-				Digest: "sha256:031567a617423a84ad68b62267c30693185bd2b92c2668732efc8c70b036bd3a",
+				Digest: OLDER_STATIC_DIGEST,
 			}
 		})
 
 		It("returns the previous digest and the current digest", func() {
 			Expect(res).To(Equal([]resource.Version{
-				{Digest: "sha256:031567a617423a84ad68b62267c30693185bd2b92c2668732efc8c70b036bd3a"},
-				{Digest: "sha256:64a6988c58cbdd634198f56452e8f8945e5b54a4bbca4bff7e960e1c830671ff"},
+				{Digest: OLDER_STATIC_DIGEST},
+				{Digest: LATEST_STATIC_DIGEST},
 			}))
 		})
 	})
@@ -110,7 +110,7 @@ var _ = Describe("Check", func() {
 
 		It("returns only the current digest", func() {
 			Expect(res).To(Equal([]resource.Version{
-				{Digest: "sha256:64a6988c58cbdd634198f56452e8f8945e5b54a4bbca4bff7e960e1c830671ff"},
+				{Digest: LATEST_STATIC_DIGEST},
 			}))
 		})
 	})

+ 36 - 8
cmd/in/main.go

@@ -9,13 +9,16 @@ import (
 	resource "github.com/concourse/registry-image-resource"
 	color "github.com/fatih/color"
 	"github.com/google/go-containerregistry/pkg/name"
+	"github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/remote"
+	"github.com/google/go-containerregistry/pkg/v1/tarball"
 	"github.com/sirupsen/logrus"
 )
 
 type InRequest struct {
-	Source  resource.Source  `json:"source"`
-	Version resource.Version `json:"version"`
+	Source  resource.Source    `json:"source"`
+	Params  resource.GetParams `json:"params"`
+	Version resource.Version   `json:"version"`
 }
 
 type InResponse struct {
@@ -74,7 +77,37 @@ func main() {
 		return
 	}
 
-	err = unpackImage(filepath.Join(dest, "rootfs"), image, req.Source.Debug)
+	switch req.Params.Format() {
+	case "oci":
+		ociFormat(dest, req, image)
+	case "rootfs":
+		rootfsFormat(dest, req, image)
+	}
+
+	json.NewEncoder(os.Stdout).Encode(InResponse{
+		Version:  req.Version,
+		Metadata: []resource.MetadataField{},
+	})
+}
+
+func ociFormat(dest string, req InRequest, image v1.Image) {
+	tag, err := name.NewTag(req.Source.Repository+":"+req.Source.Tag(), name.WeakValidation)
+	if err != nil {
+		logrus.Errorf("failed to construct tag reference: %s", err)
+		os.Exit(1)
+		return
+	}
+
+	err = tarball.WriteToFile(filepath.Join(dest, "image.tar"), tag, image, nil)
+	if err != nil {
+		logrus.Errorf("failed to write OCI image: %s", err)
+		os.Exit(1)
+		return
+	}
+}
+
+func rootfsFormat(dest string, req InRequest, image v1.Image) {
+	err := unpackImage(filepath.Join(dest, "rootfs"), image, req.Source.Debug)
 	if err != nil {
 		logrus.Errorf("failed to extract image: %s", err)
 		os.Exit(1)
@@ -111,9 +144,4 @@ func main() {
 		os.Exit(1)
 		return
 	}
-
-	json.NewEncoder(os.Stdout).Encode(InResponse{
-		Version:  req.Version,
-		Metadata: []resource.MetadataField{},
-	})
 }

+ 38 - 6
in_test.go

@@ -10,6 +10,9 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/google/go-containerregistry/pkg/name"
+	"github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/tarball"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 
@@ -21,6 +24,7 @@ var _ = Describe("In", func() {
 
 	var req struct {
 		Source  resource.Source
+		Params  resource.GetParams
 		Version resource.Version
 	}
 
@@ -179,10 +183,38 @@ var _ = Describe("In", func() {
 			Expect(cat(rootfsPath("b"))).To(Equal("replaced\n"))
 		})
 	})
-})
 
-func cat(path string) string {
-	bytes, err := ioutil.ReadFile(path)
-	Expect(err).ToNot(HaveOccurred())
-	return string(bytes)
-}
+	Describe("fetching in OCI format", func() {
+		var manifest *v1.Manifest
+
+		BeforeEach(func() {
+			req.Source.Repository = "concourse/test-image-static"
+			req.Params.RawFormat = "oci"
+
+			req.Version.Digest, manifest = latestManifest(req.Source.Repository)
+		})
+
+		It("saves the tagged image as image.tar instead of saving the rootfs", func() {
+			_, err := os.Stat(filepath.Join(destDir, "rootfs"))
+			Expect(os.IsNotExist(err)).To(BeTrue())
+
+			_, err = os.Stat(filepath.Join(destDir, "manifest.json"))
+			Expect(os.IsNotExist(err)).To(BeTrue())
+
+			tag, err := name.NewTag("concourse/test-image-static:latest", name.WeakValidation)
+			Expect(err).ToNot(HaveOccurred())
+
+			img, err := tarball.ImageFromPath(filepath.Join(destDir, "image.tar"), &tag)
+			Expect(err).ToNot(HaveOccurred())
+
+			fetchedManifest, err := img.Manifest()
+			Expect(err).ToNot(HaveOccurred())
+
+			// cannot assert against digest because the saved image's manifest isn't
+			// JSON-prettified, so it has a different sha256. so just assert against
+			// digest within manifest, which is what ends up being the 'image id'
+			// anyway.
+			Expect(fetchedManifest.Config.Digest).To(Equal(manifest.Config.Digest))
+		})
+	})
+})

+ 30 - 1
suite_test.go

@@ -2,10 +2,12 @@ package resource_test
 
 import (
 	"encoding/json"
+	"io/ioutil"
 	"os"
 	"testing"
 
 	"github.com/google/go-containerregistry/pkg/name"
+	"github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/remote"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
@@ -18,6 +20,10 @@ var bins struct {
 	Check string `json:"check"`
 }
 
+// see testdata/static/Dockerfile
+const OLDER_STATIC_DIGEST = "sha256:031567a617423a84ad68b62267c30693185bd2b92c2668732efc8c70b036bd3a"
+const LATEST_STATIC_DIGEST = "sha256:64a6988c58cbdd634198f56452e8f8945e5b54a4bbca4bff7e960e1c830671ff"
+
 var _ = SynchronizedBeforeSuite(func() []byte {
 	var err error
 
@@ -53,7 +59,8 @@ var _ = SynchronizedBeforeSuite(func() []byte {
 	Expect(err).ToNot(HaveOccurred())
 })
 
-var _ = AfterSuite(func() {
+var _ = SynchronizedAfterSuite(func() {
+}, func() {
 	gexec.CleanupBuildArtifacts()
 })
 
@@ -74,3 +81,25 @@ func latestDigest(ref string) string {
 
 	return digest.String()
 }
+
+func latestManifest(ref string) (string, *v1.Manifest) {
+	n, err := name.ParseReference(ref, name.WeakValidation)
+	Expect(err).ToNot(HaveOccurred())
+
+	image, err := remote.Image(n)
+	Expect(err).ToNot(HaveOccurred())
+
+	manifest, err := image.Manifest()
+	Expect(err).ToNot(HaveOccurred())
+
+	digest, err := image.Digest()
+	Expect(err).ToNot(HaveOccurred())
+
+	return digest.String(), manifest
+}
+
+func cat(path string) string {
+	bytes, err := ioutil.ReadFile(path)
+	Expect(err).ToNot(HaveOccurred())
+	return string(bytes)
+}

+ 12 - 0
types.go

@@ -25,3 +25,15 @@ type MetadataField struct {
 	Name  string `json:"name"`
 	Value string `json:"value"`
 }
+
+type GetParams struct {
+	RawFormat string `json:"format"`
+}
+
+func (p GetParams) Format() string {
+	if p.RawFormat == "" {
+		return "rootfs"
+	}
+
+	return p.RawFormat
+}