summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbaude <bbaude@redhat.com>2018-02-27 12:25:25 -0600
committerAtomic Bot <atomic-devel@projectatomic.io>2018-03-08 19:31:31 +0000
commitbb6f0f8e266a77852c2690d1ee956ecbf23d28ff (patch)
treefbb6a14c5c22766b3912f5dd1272700fbc4cef71
parent54f32f2cc024090c3f284f7b0b6832f2b19a6660 (diff)
downloadpodman-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.yml4
-rw-r--r--libpod/image/image.go191
-rw-r--r--libpod/image/image_test.go184
-rw-r--r--libpod/image/parts.go56
-rw-r--r--libpod/image/utils.go42
-rw-r--r--libpod/runtime_img.go6
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