diff options
author | baude <bbaude@redhat.com> | 2018-02-27 12:25:25 -0600 |
---|---|---|
committer | Atomic Bot <atomic-devel@projectatomic.io> | 2018-03-08 19:31:31 +0000 |
commit | bb6f0f8e266a77852c2690d1ee956ecbf23d28ff (patch) | |
tree | fbb6a14c5c22766b3912f5dd1272700fbc4cef71 | |
parent | 54f32f2cc024090c3f284f7b0b6832f2b19a6660 (diff) | |
download | podman-bb6f0f8e266a77852c2690d1ee956ecbf23d28ff.tar.gz podman-bb6f0f8e266a77852c2690d1ee956ecbf23d28ff.tar.bz2 podman-bb6f0f8e266a77852c2690d1ee956ecbf23d28ff.zip |
Image Resolution Stage 1
This is the stage 1 effort for an image library that can be eventually used by buildah and
podman alike. In eventuality, the main goal of the library (package) is to:
* provide a consistent approach to resolving image names in various forms (from users).
* based on the result of the above, provide image methods that in a singular spot but separate from the runtime.
* reduce the cruft and bloat in the current podman runtime.
The goal of stage 1 is to demonstrate fast, accurate image resolution for both local and remote images resulting in
an image object as part of the return.
Signed-off-by: baude <bbaude@redhat.com>
Closes: #463
Approved by: baude
-rw-r--r-- | .travis.yml | 4 | ||||
-rw-r--r-- | libpod/image/image.go | 191 | ||||
-rw-r--r-- | libpod/image/image_test.go | 184 | ||||
-rw-r--r-- | libpod/image/parts.go | 56 | ||||
-rw-r--r-- | libpod/image/utils.go | 42 | ||||
-rw-r--r-- | libpod/runtime_img.go | 6 |
6 files changed, 479 insertions, 4 deletions
diff --git a/.travis.yml b/.travis.yml index f1098d885..138a9c9f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,12 +29,12 @@ jobs: - make lint go: 1.9.x - script: - - make localunit + - make testunit - make go: 1.8.x - stage: Build and Verify script: - - make localunit + - make testunit - make go: 1.9.x - stage: Integration Test diff --git a/libpod/image/image.go b/libpod/image/image.go new file mode 100644 index 000000000..a3f0bce83 --- /dev/null +++ b/libpod/image/image.go @@ -0,0 +1,191 @@ +package image + +import ( + "fmt" + "io" + "os" + + "github.com/containers/image/docker/reference" + "github.com/containers/storage" + "github.com/pkg/errors" + "github.com/projectatomic/libpod/libpod" + "github.com/projectatomic/libpod/pkg/inspect" +) + +// Image is the primary struct for dealing with images +// It is still very much a work in progress +type Image struct { + inspect.ImageData + InputName string + Local bool + runtime *libpod.Runtime + image *storage.Image +} + +// NewFromLocal creates a new image object that is intended +// to only deal with local images already in the store (or +// its aliases) +func NewFromLocal(name string, runtime *libpod.Runtime) (Image, error) { + image := Image{ + InputName: name, + Local: true, + runtime: runtime, + } + localImage, err := image.getLocalImage() + if err != nil { + return Image{}, err + } + image.image = localImage + return image, nil +} + +// New creates a new image object where the image could be local +// or remote +func New(name string, runtime *libpod.Runtime) (Image, error) { + // We don't know if the image is local or not ... check local first + newImage := Image{ + InputName: name, + Local: false, + runtime: runtime, + } + localImage, err := newImage.getLocalImage() + if err == nil { + newImage.Local = true + newImage.image = localImage + return newImage, nil + } + + // The image is not local + pullNames, err := newImage.createNamesToPull() + if err != nil { + return newImage, err + } + if len(pullNames) == 0 { + return newImage, errors.Errorf("unable to pull %s", newImage.InputName) + } + var writer io.Writer + writer = os.Stderr + for _, p := range pullNames { + _, err := newImage.pull(p, writer, runtime) + if err == nil { + newImage.InputName = p + img, err := newImage.getLocalImage() + newImage.image = img + return newImage, err + } + } + return newImage, errors.Errorf("unable to find %s", name) +} + +// getLocalImage resolves an unknown input describing an image and +// returns a storage.Image or an error. It is used by NewFromLocal. +func (i *Image) getLocalImage() (*storage.Image, error) { + imageError := fmt.Sprintf("unable to find '%s' in local storage\n", i.InputName) + if i.InputName == "" { + return nil, errors.Errorf("input name is blank") + } + var taggedName string + img, err := i.runtime.GetImage(i.InputName) + if err == nil { + return img, err + } + + // container-storage wasn't able to find it in its current form + // check if the input name has a tag, and if not, run it through + // again + decomposedImage, err := decompose(i.InputName) + if err != nil { + return nil, err + } + // the inputname isn't tagged, so we assume latest and try again + if !decomposedImage.isTagged { + taggedName = fmt.Sprintf("%s:latest", i.InputName) + img, err = i.runtime.GetImage(taggedName) + if err == nil { + return img, nil + } + } + hasReg, err := i.hasRegistry() + if err != nil { + return nil, errors.Wrapf(err, imageError) + } + + // if the input name has a registry in it, the image isnt here + if hasReg { + return nil, errors.Errorf("%s", imageError) + } + + // grab all the local images + images, err := i.runtime.GetImages(&libpod.ImageFilterParams{}) + if err != nil { + return nil, err + } + + // check the repotags of all images for a match + repoImage, err := findImageInRepotags(decomposedImage, images) + if err == nil { + return repoImage, nil + } + + return nil, errors.Errorf("%s", imageError) +} + +// hasRegistry returns a bool/err response if the image has a registry in its +// name +func (i *Image) hasRegistry() (bool, error) { + imgRef, err := reference.Parse(i.InputName) + if err != nil { + return false, err + } + registry := reference.Domain(imgRef.(reference.Named)) + if registry != "" { + return true, nil + } + return false, nil +} + +// ID returns the image ID as a string +func (i *Image) ID() string { + return i.image.ID +} + +// createNamesToPull looks at a decomposed image and determines the possible +// images names to try pulling in combination with the registries.conf file as well +func (i *Image) createNamesToPull() ([]string, error) { + var pullNames []string + decomposedImage, err := decompose(i.InputName) + if err != nil { + return nil, err + } + + if decomposedImage.hasRegistry { + pullNames = append(pullNames, i.InputName) + } else { + registries, err := libpod.GetRegistries() + if err != nil { + return nil, err + } + for _, registry := range registries { + decomposedImage.registry = registry + pullNames = append(pullNames, decomposedImage.assemble()) + } + } + return pullNames, nil +} + +// pull is a temporary function for stage1 to be able to pull images during the image +// resolution tests. it will be replaced in stage2 with a more robust function. +func (i *Image) pull(name string, writer io.Writer, r *libpod.Runtime) (string, error) { + options := libpod.CopyOptions{ + Writer: writer, + SignaturePolicyPath: r.GetConfig().SignaturePolicyPath, + } + return i.runtime.PullImage(name, options) +} + +// Remove an image +// This function is only complete enough for the stage 1 tests. +func (i *Image) Remove(force bool) error { + _, err := i.runtime.RemoveImage(i.image, force) + return err +} diff --git a/libpod/image/image_test.go b/libpod/image/image_test.go new file mode 100644 index 000000000..4b5b782b1 --- /dev/null +++ b/libpod/image/image_test.go @@ -0,0 +1,184 @@ +package image + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "testing" + + "github.com/containers/storage" + "github.com/containers/storage/pkg/reexec" + "github.com/pkg/errors" + "github.com/projectatomic/libpod/libpod" + "github.com/stretchr/testify/assert" +) + +var ( + bbNames = []string{"docker.io/library/busybox:latest", "docker.io/library/busybox", "docker.io/busybox:latest", "docker.io/busybox", "busybox:latest", "busybox"} + bbGlibcNames = []string{"docker.io/library/busybox:glibc", "docker.io/busybox:glibc", "busybox:glibc"} + fedoraNames = []string{"registry.fedoraproject.org/fedora-minimal:latest", "registry.fedoraproject.org/fedora-minimal", "fedora-minimal:latest", "fedora-minimal"} +) + +// setup a runtime for the tests in an alternative location on the filesystem +func setupRuntime(workdir string) (*libpod.Runtime, error) { + if reexec.Init() { + return nil, errors.Errorf("dude") + } + sc := libpod.WithStorageConfig(storage.StoreOptions{ + GraphRoot: workdir, + RunRoot: workdir, + }) + sd := libpod.WithStaticDir(path.Join(workdir, "libpod_tmp")) + td := libpod.WithTmpDir(path.Join(workdir, "tmpdir")) + + options := []libpod.RuntimeOption{sc, sd, td} + return libpod.NewRuntime(options...) +} + +// getImage is only used to build a test matrix for testing local images +func getImage(r *libpod.Runtime, fqImageName string) (*storage.Image, error) { + img, err := NewFromLocal(fqImageName, r) + if err != nil { + return nil, err + } + return img.image, nil +} + +func tagImage(r *libpod.Runtime, fqImageName, tagName string) error { + img, err := NewFromLocal(fqImageName, r) + if err != nil { + return err + } + r.TagImage(img.image, tagName) + return nil +} + +type localImageTest struct { + fqname, taggedName string + img *storage.Image + names []string +} + +// make a temporary directory for the runtime +func mkWorkDir() (string, error) { + return ioutil.TempDir("", "podman-test") +} + +// shutdown the runtime and clean behind it +func cleanup(r *libpod.Runtime, workdir string) { + r.Shutdown(true) + err := os.RemoveAll(workdir) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func makeLocalMatrix(r *libpod.Runtime) ([]localImageTest, error) { + var l []localImageTest + // busybox + busybox := localImageTest{ + fqname: "docker.io/library/busybox:latest", + taggedName: "bb:latest", + } + b, err := getImage(r, busybox.fqname) + if err != nil { + return nil, err + } + busybox.img = b + busybox.names = bbNames + busybox.names = append(busybox.names, []string{"bb:latest", "bb", b.ID, b.ID[0:7], fmt.Sprintf("busybox@%s", b.Digest.String())}...) + + //fedora + fedora := localImageTest{ + fqname: "registry.fedoraproject.org/fedora-minimal:latest", + taggedName: "f27:latest", + } + f, err := getImage(r, fedora.fqname) + if err != nil { + return nil, err + } + fedora.img = f + fedora.names = fedoraNames + + // busybox-glibc + busyboxGlibc := localImageTest{ + fqname: "docker.io/library/busybox:glibc", + taggedName: "bb:glibc", + } + bg, err := getImage(r, busyboxGlibc.fqname) + if err != nil { + return nil, err + } + busyboxGlibc.img = bg + busyboxGlibc.names = bbGlibcNames + + l = append(l, busybox, fedora) + return l, nil + +} + +// TestImage_NewFromLocal tests finding the image locally by various names, +// tags, and aliases +func TestImage_NewFromLocal(t *testing.T) { + workdir, err := mkWorkDir() + assert.NoError(t, err) + runtime, err := setupRuntime(workdir) + assert.NoError(t, err) + + // Need images to be present for this test + _, err = runtime.PullImage("docker.io/library/busybox:latest", libpod.CopyOptions{}) + assert.NoError(t, err) + _, err = runtime.PullImage("docker.io/library/busybox:glibc", libpod.CopyOptions{}) + assert.NoError(t, err) + _, err = runtime.PullImage("registry.fedoraproject.org/fedora-minimal:latest", libpod.CopyOptions{}) + assert.NoError(t, err) + + tm, err := makeLocalMatrix(runtime) + assert.NoError(t, err) + for _, image := range tm { + // tag our images + err = tagImage(runtime, image.fqname, image.taggedName) + assert.NoError(t, err) + for _, name := range image.names { + newImage, err := NewFromLocal(name, runtime) + assert.NoError(t, err) + assert.Equal(t, newImage.ID(), image.img.ID) + } + } + + // Shutdown the runtime and remove the temporary storage + cleanup(runtime, workdir) +} + +// TestImage_New tests pulling the image by various names, tags, and from +// different registries +func TestImage_New(t *testing.T) { + var names []string + workdir, err := mkWorkDir() + assert.NoError(t, err) + runtime, err := setupRuntime(workdir) + assert.NoError(t, err) + + // Build the list of pull names + names = append(names, bbNames...) + names = append(names, fedoraNames...) + + // Iterate over the names and delete the image + // after the pull + for _, img := range names { + _, err := runtime.GetImage(img) + if err == nil { + os.Exit(1) + } + newImage, err := New(img, runtime) + assert.NoError(t, err) + assert.NotEqual(t, newImage.ID(), "") + err = newImage.Remove(false) + assert.NoError(t, err) + } + + // Shutdown the runtime and remove the temporary storage + cleanup(runtime, workdir) +} diff --git a/libpod/image/parts.go b/libpod/image/parts.go new file mode 100644 index 000000000..e4ae489f9 --- /dev/null +++ b/libpod/image/parts.go @@ -0,0 +1,56 @@ +package image + +import ( + "fmt" + + "github.com/containers/image/docker/reference" +) + +// imageParts describes the parts of an image's name +type imageParts struct { + transport string + registry string + name string + tag string + isTagged bool + hasRegistry bool +} + +// decompose breaks an input name into an imageParts description +func decompose(input string) (imageParts, error) { + var ( + parts imageParts + hasRegistry bool + tag string + ) + imgRef, err := reference.Parse(input) + if err != nil { + return parts, err + } + ntag, isTagged, err := getTags(input) + if err != nil { + return parts, err + } + if !isTagged { + tag = "latest" + } else { + tag = ntag.Tag() + } + registry := reference.Domain(imgRef.(reference.Named)) + if registry != "" { + hasRegistry = true + } + imageName := reference.Path(imgRef.(reference.Named)) + return imageParts{ + registry: registry, + hasRegistry: hasRegistry, + name: imageName, + tag: tag, + isTagged: isTagged, + }, nil +} + +// assemble concatenates an image's parts into a string +func (ip *imageParts) assemble() string { + return fmt.Sprintf("%s/%s:%s", ip.registry, ip.name, ip.tag) +} diff --git a/libpod/image/utils.go b/libpod/image/utils.go new file mode 100644 index 000000000..f312c8e4d --- /dev/null +++ b/libpod/image/utils.go @@ -0,0 +1,42 @@ +package image + +import ( + "github.com/containers/image/docker/reference" + "github.com/containers/storage" + "github.com/pkg/errors" +) + +func getTags(nameInput string) (reference.NamedTagged, bool, error) { + inputRef, err := reference.Parse(nameInput) + if err != nil { + return nil, false, errors.Wrapf(err, "unable to obtain tag from input name") + } + tagged, isTagged := inputRef.(reference.NamedTagged) + + return tagged, isTagged, nil +} + +// findImageInRepotags takes an imageParts struct and searches images' repotags for +// a match on name:tag +func findImageInRepotags(search imageParts, images []*storage.Image) (*storage.Image, error) { + var results []*storage.Image + for _, image := range images { + for _, name := range image.Names { + d, err := decompose(name) + // if we get an error, ignore and keep going + if err != nil { + continue + } + if d.name == search.name && d.tag == search.tag { + results = append(results, image) + break + } + } + } + if len(results) == 0 { + return &storage.Image{}, errors.Errorf("unable to find a name and tag match for %s in repotags", search) + } else if len(results) > 1 { + return &storage.Image{}, errors.Errorf("found multiple name and tag matches for %s in repotags", search) + } + return results[0], nil +} diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go index bc328d5b9..8d2c3a1d6 100644 --- a/libpod/runtime_img.go +++ b/libpod/runtime_img.go @@ -884,9 +884,11 @@ func (r *Runtime) RemoveImage(image *storage.Image, force bool) (string, error) if len(image.Names) > 1 && !force { return "", fmt.Errorf("unable to delete %s (must force) - image is referred to in multiple tags", image.ID) } - // If it is forced, we have to untag the image so that it can be deleted - image.Names = image.Names[:0] + // If it is forced, we have to untag the image so that it can be deleted + if err = r.store.SetNames(image.ID, image.Names[:0]); err != nil { + return "", err + } _, err = r.store.DeleteImage(image.ID, true) if err != nil { return "", err |