Skip to content

Commit

Permalink
Sketch code for downloading images to cache
Browse files Browse the repository at this point in the history
Rough-and-ready code for downloading images into the cache. Manifests
referred to by digest get written out; otherwise, they get tagged.
  • Loading branch information
squaremo committed Jan 31, 2020
1 parent d144f34 commit c0ca684
Show file tree
Hide file tree
Showing 8 changed files with 631 additions and 20 deletions.
16 changes: 7 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@ module github.com/jkcfg/jk
require (
github.com/ghodss/yaml v1.0.0
github.com/google/flatbuffers v1.10.0
github.com/google/go-containerregistry v0.0.0-20200128171736-43a8003f9213
github.com/hashicorp/hcl v1.0.0
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jkcfg/v8worker2 v0.0.0-20191022163158-90e467066938
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1
github.com/pkg/errors v0.8.1
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3
github.com/stretchr/testify v1.2.2
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.4.0
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.1.0
golang.org/x/text v0.3.0
golang.org/x/tools v0.0.0-20190815212832-922a4ee32d1a
gopkg.in/yaml.v2 v2.2.1
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20200115165105-de0b1760071a
gopkg.in/yaml.v2 v2.2.4
)

go 1.13
385 changes: 385 additions & 0 deletions go.sum

Large diffs are not rendered by default.

25 changes: 15 additions & 10 deletions pkg/image/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,14 @@ package cache
// - if not present, download it and verify it
// 3. construct an overlay filesytem out of the layers

// Ref for downloading things:
// https://github.com/containers/skopeo/blob/master/cmd/skopeo/copy.go

import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"

"github.com/google/go-containerregistry/pkg/name"
oci_v1 "github.com/opencontainers/image-spec/specs-go/v1"

"github.com/jkcfg/jk/pkg/image/overlay"
Expand Down Expand Up @@ -66,17 +64,24 @@ func (cache *Cache) layerPath(algo, digest string) string {
return filepath.Join(cache.base, layersDir, algo, digest)
}

// Standardises the construction of the path to a manifest.
func (cache *Cache) manifestPath(image, ref string) string {
return filepath.Join(cache.base, manifestsDir, image, ref)
// Standardises the construction of the path to a manifest. The ref
// can be a digest or a tag.
func (cache *Cache) manifestPath(imageRef name.Reference) string {
if tag, ok := imageRef.(name.Tag); ok {
return filepath.Join(cache.base, manifestsDir, imageRef.Context().Name(), "tag", tag.TagStr())
} else if dig, ok := imageRef.(name.Digest); ok {
return filepath.Join(cache.base, manifestsDir, imageRef.Context().Name(), dig.DigestStr())
}
return ""
}

// FileSystemForImage takes an image name and ref (tag), and
// constructs a vfs.FileSystem from the image's layers as found in the
// cache. It assumes the manifest and layers will be present in the
// cache.
func (cache *Cache) FileSystemForImage(image, ref string) (vfs.FileSystem, error) {
m := cache.manifestPath(image, ref)
// cache. TODO accept the image as just one string, and parse it with
// go-containerregistry/pkg/name
func (cache *Cache) FileSystemForImage(image name.Reference) (vfs.FileSystem, error) {
m := cache.manifestPath(image)
mfile, err := os.Open(m)
if err != nil {
return nil, fmt.Errorf("cannot stat manifest at implied path %s: %s", m, err.Error())
Expand Down Expand Up @@ -105,5 +110,5 @@ func (cache *Cache) FileSystemForImage(image, ref string) (vfs.FileSystem, error
// the layers are bottom-most first in the manifest.
layers[layerCount-i-1] = http.Dir(layerPath)
}
return vfs.User(image+":"+ref+"!", overlay.New(layers...)), nil
return vfs.User(image.String()+"!", overlay.New(layers...)), nil
}
11 changes: 10 additions & 1 deletion pkg/image/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@ import (
"io/ioutil"
"testing"

"github.com/google/go-containerregistry/pkg/name"
"github.com/stretchr/testify/assert"
)

func mustParseRef(ref string) name.Reference {
parsed, err := name.ParseReference(ref)
if err != nil {
panic(err)
}
return parsed
}

func TestCacheOverlayCreation(t *testing.T) {
c := New("testfiles/dotcache")
ov, err := c.FileSystemForImage("image-repo", "image-tag")
ov, err := c.FileSystemForImage(mustParseRef("image-repo:image-tag"))
assert.NoError(t, err)

// Open a file known to be present. This verifies that the overlay
Expand Down
169 changes: 169 additions & 0 deletions pkg/image/cache/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package cache

// The read part of the cache constructs a filesystem given an image
// ref. The download part, here, gets the constituents of images and
// puts them into the cache. The contract between the two is in the
// layout of the directories used by the cache, described in the
// package documentation.

import (
"archive/tar"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"

"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
)

// FIXME don't force permissions on the actual files, since some may
// need to be executable
const cacheFileMode = os.FileMode(0400)
const cacheDirMode = os.FileMode(0700)

// Download makes sure the manifest and layers for a particular image
// are present in the cache.
func (c *Cache) Download(image string) error { // <-- could return the digest?
// Ref for this code:
// https://github.com/google/go-containerregistry/blob/master/pkg/crane/pull.go#Save
ref, err := name.ParseReference(image)
if err != nil {
return err
}

manifestPath := c.manifestPath(ref)
if manifestPath == "" {
return fmt.Errorf("cannot make manifest path for image ref %q", ref)
}

// Whichever kind of image ref, if the manifest is already in the
// filesystem, we must have completed this previously.
_, err = os.Stat(manifestPath)
if err == nil {
return nil
}
if !os.IsNotExist(err) {
return err
}

img, err := remote.Image(ref)
if err != nil {
return err
}

// If it's a reference with a tag, specifically, then look for the
// manifest at the digest as well
var tagManifestPath string
if _, ok := ref.(name.Tag); ok {
dig, err := img.Digest()
if err != nil {
return err
}
digestManifestPath := c.manifestPath(ref.Context().Digest(dig.String()))
if digestManifestPath == "" {
return fmt.Errorf("unable to construct path to manifest for %q", dig)
}
// we'll be writing to the digest path and symlinking the tag
// path to the digest path
tagManifestPath, manifestPath = manifestPath, digestManifestPath
_, err = os.Stat(manifestPath)
if err == nil {
// TODO(michael): factor this out
if err = os.MkdirAll(filepath.Dir(tagManifestPath), cacheDirMode); err != nil {
return err
}
err = os.Symlink(manifestPath, tagManifestPath)
return err
}
if !os.IsNotExist(err) {
return err
}
}

// TODO any kind of verification

layers, err := img.Layers()
if err != nil {
return err
}
for _, layer := range layers {
// Check for the layer in the layers directory
digest, err := layer.Digest()
if err != nil {
return err
}
p := c.layerPath(digest.Algorithm, digest.Hex)
_, err = os.Stat(p)
if err == nil { // already have it
continue
}

// TODO(michael): if there's a problem after this, clean up
// the expanded layer before returning, so it doesn't look
// like we've succeeded. In fact, better to expand somewhere
// else, then rename it to the right place if it succeeds.
if err = os.MkdirAll(p, cacheDirMode); err != nil {
return err
}

// Write the layer by expanding it into the directory.

layerReader, err := layer.Uncompressed()
if err != nil {
return err
}
rdr := tar.NewReader(layerReader)
for {
hdr, err := rdr.Next()
if err == io.EOF {
break
}
targetPath := filepath.Join(p, hdr.Name)
info := hdr.FileInfo()
if info.IsDir() {
if os.MkdirAll(targetPath, 0755); err != nil {
// TODO cleanup
return err
}
} else {
f, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, cacheFileMode)
if err != nil {
return err // TODO cleanup
}
if _, err := io.Copy(f, rdr); err != nil {
f.Close()
return err // TODO cleanup
}
f.Close()
}
}
}

// TODO(michael) figure out if we will always get an OCI v1
// (compatible) manifest
man, err := img.RawManifest()
if err != nil {
// TODO cleanup
return err
}

if err = os.MkdirAll(filepath.Dir(manifestPath), cacheDirMode); err != nil {
return err
}
if err = ioutil.WriteFile(manifestPath, man, cacheFileMode); err != nil {
// TODO cleanup
return err
}

if tagManifestPath != "" {
if err = os.MkdirAll(filepath.Dir(tagManifestPath), cacheDirMode); err != nil {
return err
}
err = os.Symlink(manifestPath, tagManifestPath)
return err
}

return nil
}
45 changes: 45 additions & 0 deletions pkg/image/cache/download_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cache

import (
"io/ioutil"
"net/http/httptest"
"os"
"testing"

"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/registry"
"github.com/stretchr/testify/assert"
)

func setupRegistry(t *testing.T) (*httptest.Server, string) {
regHandler := registry.New()
regSrv := httptest.NewServer(regHandler)
img, err := crane.Load("./testfiles/helloworld.tar")
assert.NoError(t, err)
newImg := regSrv.URL[len("http://"):] + "/helloworld:linux"
assert.NoError(t, crane.Push(img, newImg))
return regSrv, newImg
}

func TestDownloadToCache(t *testing.T) {
tmp, err := ioutil.TempDir("", "jk-test")
assert.NoError(t, err)
defer os.RemoveAll(tmp)

cache := New(tmp)

regSrv, img := setupRegistry(t)
defer regSrv.Close()

err = cache.Download(img)
assert.NoError(t, err)

ov, err := cache.FileSystemForImage(mustParseRef(img))
assert.NoError(t, err)
f, err := ov.Open("/hello")
assert.NoError(t, err)
defer f.Close()

_, err = ioutil.ReadAll(f)
assert.NoError(t, err)
}
Binary file added pkg/image/cache/testfiles/helloworld.tar
Binary file not shown.

0 comments on commit c0ca684

Please sign in to comment.