diff options
author | baude <bbaude@redhat.com> | 2018-03-08 15:45:52 -0600 |
---|---|---|
committer | Atomic Bot <atomic-devel@projectatomic.io> | 2018-03-14 20:21:31 +0000 |
commit | b85b217f55993955da9ad0cae7735747b2f24390 (patch) | |
tree | 3e633600e50bf3bedc9bdc59abe789fa01f55601 | |
parent | bc358eb396aa87f3122f0449945efc03ed64bfd2 (diff) | |
download | podman-b85b217f55993955da9ad0cae7735747b2f24390.tar.gz podman-b85b217f55993955da9ad0cae7735747b2f24390.tar.bz2 podman-b85b217f55993955da9ad0cae7735747b2f24390.zip |
Stage3 Image Library
This represents the stage3 implementation for the image library. At this point, we
are moving the image-centric functions to pkg/image including migration of args and
object-oriented references. This is a not a one-for-one migration of funcs and some
funcs will need to continue to reside in runtime_img as they are overly specific to
libpod and probably not useful to others.
Signed-off-by: baude <bbaude@redhat.com>
Closes: #484
Approved by: baude
-rw-r--r-- | cmd/podman/create.go | 5 | ||||
-rw-r--r-- | cmd/podman/exec.go | 3 | ||||
-rw-r--r-- | cmd/podman/inspect.go | 3 | ||||
-rw-r--r-- | cmd/podman/ps.go | 3 | ||||
-rw-r--r-- | libpod/container_internal.go | 3 | ||||
-rw-r--r-- | libpod/container_top.go | 3 | ||||
-rw-r--r-- | libpod/image/docker_registry_options.go | 46 | ||||
-rw-r--r-- | libpod/image/image.go | 370 | ||||
-rw-r--r-- | libpod/image/image_test.go | 120 | ||||
-rw-r--r-- | libpod/image/parts.go | 6 | ||||
-rw-r--r-- | libpod/image/pull.go | 246 | ||||
-rw-r--r-- | libpod/image/signing_options.go | 10 | ||||
-rw-r--r-- | libpod/image/utils.go | 52 | ||||
-rw-r--r-- | libpod/runtime_img.go | 3 | ||||
-rw-r--r-- | libpod/util.go | 10 | ||||
-rw-r--r-- | libpod/util_test.go | 13 | ||||
-rw-r--r-- | pkg/util/utils.go | 10 | ||||
-rw-r--r-- | pkg/util/utils_test.go | 19 |
18 files changed, 745 insertions, 180 deletions
diff --git a/cmd/podman/create.go b/cmd/podman/create.go index 46429b335..b923ad458 100644 --- a/cmd/podman/create.go +++ b/cmd/podman/create.go @@ -18,6 +18,7 @@ import ( "github.com/pkg/errors" "github.com/projectatomic/libpod/libpod" "github.com/projectatomic/libpod/pkg/inspect" + "github.com/projectatomic/libpod/pkg/util" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -299,7 +300,7 @@ func isPortInPortBindings(pb map[nat.Port][]nat.PortBinding, port nat.Port) bool for _, i := range pb { hostPorts = append(hostPorts, i[0].HostPort) } - return libpod.StringInSlice(port.Port(), hostPorts) + return util.StringInSlice(port.Port(), hostPorts) } // isPortInImagePorts determines if an exposed host port was given to us by metadata @@ -625,7 +626,7 @@ func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime, imageName string, } // Check for . and dns-search domains - if libpod.StringInSlice(".", c.StringSlice("dns-search")) && len(c.StringSlice("dns-search")) > 1 { + if util.StringInSlice(".", c.StringSlice("dns-search")) && len(c.StringSlice("dns-search")) > 1 { return nil, errors.Errorf("cannot pass additional search domains when also specifying '.'") } diff --git a/cmd/podman/exec.go b/cmd/podman/exec.go index 07ef3a0cd..81b69953b 100644 --- a/cmd/podman/exec.go +++ b/cmd/podman/exec.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" "github.com/projectatomic/libpod/libpod" + "github.com/projectatomic/libpod/pkg/util" "github.com/urfave/cli" ) @@ -89,7 +90,7 @@ func execCmd(c *cli.Context) error { // key and value to the environment variables. this is needed to set // PATH for example. for k, v := range defaultEnvVariables { - if !libpod.StringInSlice(k, userEnvKeys) { + if !util.StringInSlice(k, userEnvKeys) { envs = append(envs, fmt.Sprintf("%s=%s", k, v)) } } diff --git a/cmd/podman/inspect.go b/cmd/podman/inspect.go index 8bf6f96be..cfd257af4 100644 --- a/cmd/podman/inspect.go +++ b/cmd/podman/inspect.go @@ -9,6 +9,7 @@ import ( "github.com/projectatomic/libpod/cmd/podman/formats" "github.com/projectatomic/libpod/libpod" "github.com/projectatomic/libpod/pkg/inspect" + "github.com/projectatomic/libpod/pkg/util" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -68,7 +69,7 @@ func inspectCmd(c *cli.Context) error { } defer runtime.Shutdown(false) - if !libpod.StringInSlice(inspectType, []string{inspectTypeContainer, inspectTypeImage, inspectAll}) { + if !util.StringInSlice(inspectType, []string{inspectTypeContainer, inspectTypeImage, inspectAll}) { return errors.Errorf("the only recognized types are %q, %q, and %q", inspectTypeContainer, inspectTypeImage, inspectAll) } diff --git a/cmd/podman/ps.go b/cmd/podman/ps.go index 4dd7133bc..ca4c4ca82 100644 --- a/cmd/podman/ps.go +++ b/cmd/podman/ps.go @@ -17,6 +17,7 @@ import ( "github.com/pkg/errors" "github.com/projectatomic/libpod/cmd/podman/formats" "github.com/projectatomic/libpod/libpod" + "github.com/projectatomic/libpod/pkg/util" "github.com/sirupsen/logrus" "github.com/urfave/cli" "k8s.io/apimachinery/pkg/fields" @@ -275,7 +276,7 @@ func generateContainerFilterFuncs(filter, filterValue string, runtime *libpod.Ru return false }, nil case "status": - if !libpod.StringInSlice(filterValue, []string{"created", "restarting", "running", "paused", "exited", "unknown"}) { + if !util.StringInSlice(filterValue, []string{"created", "restarting", "running", "paused", "exited", "unknown"}) { return nil, errors.Errorf("%s is not a valid status", filterValue) } return func(c *libpod.Container) bool { diff --git a/libpod/container_internal.go b/libpod/container_internal.go index ba11dd2c6..85c4283b3 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -26,6 +26,7 @@ import ( "github.com/pkg/errors" crioAnnotations "github.com/projectatomic/libpod/pkg/annotations" "github.com/projectatomic/libpod/pkg/chrootuser" + "github.com/projectatomic/libpod/pkg/util" "github.com/sirupsen/logrus" "github.com/ulule/deepcopier" "golang.org/x/sys/unix" @@ -642,7 +643,7 @@ func (c *Container) generateResolvConf() (string, error) { if len(c.config.DNSSearch) > 0 { resolv.searchDomains = nil // The . character means the user doesnt want any search domains in the container - if !StringInSlice(".", c.config.DNSSearch) { + if !util.StringInSlice(".", c.config.DNSSearch) { resolv.searchDomains = append(resolv.searchDomains, c.Config().DNSSearch...) } } diff --git a/libpod/container_top.go b/libpod/container_top.go index 241e3a3e7..bc007c408 100644 --- a/libpod/container_top.go +++ b/libpod/container_top.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/pkg/errors" + "github.com/projectatomic/libpod/pkg/util" "github.com/projectatomic/libpod/utils" "github.com/sirupsen/logrus" ) @@ -91,7 +92,7 @@ func filterPids(psOutput string, pids []string) ([]string, error) { } cols := fieldsASCII(l) pid := cols[pidIndex] - if StringInSlice(pid, pids) { + if util.StringInSlice(pid, pids) { output = append(output, l) } } diff --git a/libpod/image/docker_registry_options.go b/libpod/image/docker_registry_options.go new file mode 100644 index 000000000..bbb49df28 --- /dev/null +++ b/libpod/image/docker_registry_options.go @@ -0,0 +1,46 @@ +package image + +import "github.com/containers/image/types" + +// DockerRegistryOptions encapsulates settings that affect how we connect or +// authenticate to a remote registry. +type DockerRegistryOptions struct { + // DockerRegistryCreds is the user name and password to supply in case + // we need to pull an image from a registry, and it requires us to + // authenticate. + DockerRegistryCreds *types.DockerAuthConfig + // DockerCertPath is the location of a directory containing CA + // certificates which will be used to verify the registry's certificate + // (all files with names ending in ".crt"), and possibly client + // certificates and private keys (pairs of files with the same name, + // except for ".cert" and ".key" suffixes). + DockerCertPath string + // DockerInsecureSkipTLSVerify turns off verification of TLS + // certificates and allows connecting to registries without encryption. + DockerInsecureSkipTLSVerify bool +} + +// GetSystemContext constructs a new system context from the given signaturePolicy path and the +// values in the DockerRegistryOptions +func (o DockerRegistryOptions) GetSystemContext(signaturePolicyPath, authFile string, forceCompress bool) *types.SystemContext { + sc := &types.SystemContext{ + SignaturePolicyPath: signaturePolicyPath, + DockerAuthConfig: o.DockerRegistryCreds, + DockerCertPath: o.DockerCertPath, + DockerInsecureSkipTLSVerify: o.DockerInsecureSkipTLSVerify, + AuthFilePath: authFile, + DirForceCompress: forceCompress, + } + return sc +} + +// GetSystemContext Constructs a new containers/image/types.SystemContext{} struct from the given signaturePolicy path +func GetSystemContext(signaturePolicyPath, authFilePath string, forceCompress bool) *types.SystemContext { + sc := &types.SystemContext{} + if signaturePolicyPath != "" { + sc.SignaturePolicyPath = signaturePolicyPath + } + sc.AuthFilePath = authFilePath + sc.DirForceCompress = forceCompress + return sc +} diff --git a/libpod/image/image.go b/libpod/image/image.go index a3f0bce83..b218c7d67 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -1,15 +1,26 @@ package image import ( + "encoding/json" "fmt" "io" - "os" + "strings" + "syscall" + "time" + types2 "github.com/containernetworking/cni/pkg/types" + cp "github.com/containers/image/copy" "github.com/containers/image/docker/reference" + is "github.com/containers/image/storage" + "github.com/containers/image/transports/alltransports" + "github.com/containers/image/types" "github.com/containers/storage" + "github.com/containers/storage/pkg/reexec" + "github.com/opencontainers/go-digest" + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" - "github.com/projectatomic/libpod/libpod" "github.com/projectatomic/libpod/pkg/inspect" + "github.com/projectatomic/libpod/pkg/util" ) // Image is the primary struct for dealing with images @@ -18,63 +29,113 @@ type Image struct { inspect.ImageData InputName string Local bool - runtime *libpod.Runtime - image *storage.Image + //runtime *libpod.Runtime + image *storage.Image + imageruntime *Runtime +} + +// Runtime contains the store +type Runtime struct { + store storage.Store +} + +// NewImageRuntime creates an Image Runtime including the store given +// store options +func NewImageRuntime(options storage.StoreOptions) (*Runtime, error) { + if reexec.Init() { + return nil, errors.Errorf("unable to reexec") + } + store, err := setStore(options) + if err != nil { + return nil, err + } + + return &Runtime{ + store: store, + }, nil +} + +func setStore(options storage.StoreOptions) (storage.Store, error) { + store, err := storage.GetStore(options) + if err != nil { + return nil, err + } + is.Transport.SetStore(store) + return store, nil +} + +// newFromStorage creates a new image object from a storage.Image +func (ir *Runtime) newFromStorage(img *storage.Image) *Image { + image := Image{ + InputName: img.ID, + Local: true, + imageruntime: ir, + image: img, + } + return &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) { +func (ir *Runtime) NewFromLocal(name string) (*Image, error) { + image := Image{ - InputName: name, - Local: true, - runtime: runtime, + InputName: name, + Local: true, + imageruntime: ir, } localImage, err := image.getLocalImage() if err != nil { - return Image{}, err + return nil, err } image.image = localImage - return image, nil + 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) { +func (ir *Runtime) New(name, signaturePolicyPath, authfile string, writer io.Writer, dockeroptions *DockerRegistryOptions, signingoptions SigningOptions) (*Image, error) { // We don't know if the image is local or not ... check local first newImage := Image{ - InputName: name, - Local: false, - runtime: runtime, + InputName: name, + Local: false, + imageruntime: ir, } localImage, err := newImage.getLocalImage() if err == nil { newImage.Local = true newImage.image = localImage - return newImage, nil + return &newImage, nil } // The image is not local - pullNames, err := newImage.createNamesToPull() + + imageName, err := newImage.pullImage(writer, authfile, signaturePolicyPath, signingoptions, dockeroptions) if err != nil { - return newImage, err + return &newImage, errors.Errorf("unable to pull %s", name) } - 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 - } + + newImage.InputName = imageName + img, err := newImage.getLocalImage() + newImage.image = img + return &newImage, nil +} + +// Shutdown closes down the storage and require a bool arg as to +// whether it should do so forcibly. +func (ir *Runtime) Shutdown(force bool) error { + _, err := ir.store.Shutdown(force) + return err +} + +func (i *Image) reloadImage() error { + newImage, err := i.imageruntime.getImage(i.ID()) + if err != nil { + return errors.Wrapf(err, "unable to reload image") } - return newImage, errors.Errorf("unable to find %s", name) + i.image = newImage.image + return nil } // getLocalImage resolves an unknown input describing an image and @@ -85,9 +146,9 @@ func (i *Image) getLocalImage() (*storage.Image, error) { return nil, errors.Errorf("input name is blank") } var taggedName string - img, err := i.runtime.GetImage(i.InputName) + img, err := i.imageruntime.getImage(i.InputName) if err == nil { - return img, err + return img.image, err } // container-storage wasn't able to find it in its current form @@ -100,9 +161,9 @@ func (i *Image) getLocalImage() (*storage.Image, error) { // 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) + img, err = i.imageruntime.getImage(taggedName) if err == nil { - return img, nil + return img.image, nil } } hasReg, err := i.hasRegistry() @@ -116,7 +177,7 @@ func (i *Image) getLocalImage() (*storage.Image, error) { } // grab all the local images - images, err := i.runtime.GetImages(&libpod.ImageFilterParams{}) + images, err := i.imageruntime.GetImages() if err != nil { return nil, err } @@ -149,43 +210,226 @@ 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) +// Digest returns the image's Manifest +func (i *Image) Digest() digest.Digest { + return i.image.Digest +} + +// Names returns a string array of names associated with the image +func (i *Image) Names() []string { + return i.image.Names +} + +// Created returns the time the image was created +func (i *Image) Created() time.Time { + return i.image.Created +} + +// TopLayer returns the top layer id as a string +func (i *Image) TopLayer() string { + return i.image.TopLayer +} + +// Remove an image; container removal for the image must be done +// outside the context of images +func (i *Image) Remove(force bool) error { + _, err := i.imageruntime.store.DeleteImage(i.ID(), true) + return err +} + +func annotations(manifest []byte, manifestType string) map[string]string { + annotations := make(map[string]string) + switch manifestType { + case ociv1.MediaTypeImageManifest: + var m ociv1.Manifest + if err := json.Unmarshal(manifest, &m); err == nil { + for k, v := range m.Annotations { + annotations[k] = v + } + } + } + return annotations +} + +// Decompose an Image +func (i *Image) Decompose() error { + return types2.NotImplementedError +} + +// TODO: Rework this method to not require an assembly of the fq name with transport +/* +// GetManifest tries to GET an images manifest, returns nil on success and err on failure +func (i *Image) GetManifest() error { + pullRef, err := alltransports.ParseImageName(i.assembleFqNameTransport()) + if err != nil { + return errors.Errorf("unable to parse '%s'", i.Names()[0]) + } + imageSource, err := pullRef.NewImageSource(nil) + if err != nil { + return errors.Wrapf(err, "unable to create new image source") + } + _, _, err = imageSource.GetManifest(nil) + if err == nil { + return nil + } + return err +} +*/ + +// getImage retrieves an image matching the given name or hash from system +// storage +// If no matching image can be found, an error is returned +func (ir *Runtime) getImage(image string) (*Image, error) { + var img *storage.Image + ref, err := is.Transport.ParseStoreReference(ir.store, image) + if err == nil { + img, err = is.Transport.GetStoreImage(ir.store, ref) + } + if err != nil { + img2, err2 := ir.store.Image(image) + if err2 != nil { + if ref == nil { + return nil, errors.Wrapf(err, "error parsing reference to image %q", image) + } + return nil, errors.Wrapf(err, "unable to locate image %q", image) + } + img = img2 + } + newImage := ir.newFromStorage(img) + return newImage, nil +} + +// GetImages retrieves all images present in storage +func (ir *Runtime) GetImages() ([]*Image, error) { + var newImages []*Image + images, err := ir.store.Images() if err != nil { return nil, err } + for _, i := range images { + newImages = append(newImages, ir.newFromStorage(&i)) + } + return newImages, nil +} - if decomposedImage.hasRegistry { - pullNames = append(pullNames, i.InputName) - } else { - registries, err := libpod.GetRegistries() - if err != nil { - return nil, err +// getImageDigest creates an image object and uses the hex value of the digest as the image ID +// for parsing the store reference +func getImageDigest(src types.ImageReference, ctx *types.SystemContext) (string, error) { + newImg, err := src.NewImage(ctx) + if err != nil { + return "", err + } + defer newImg.Close() + digest := newImg.ConfigInfo().Digest + if err = digest.Validate(); err != nil { + return "", errors.Wrapf(err, "error getting config info") + } + return "@" + digest.Hex(), nil +} + +// TagImage adds a tag to the given image +func (i *Image) TagImage(tag string) error { + tags := i.Names() + if util.StringInSlice(tag, tags) { + return nil + } + tags = append(tags, tag) + i.reloadImage() + return i.imageruntime.store.SetNames(i.ID(), tags) +} + +// PushImage pushes the given image to a location described by the given path +func (i *Image) PushImage(destination, manifestMIMEType, authFile, signaturePolicyPath string, writer io.Writer, forceCompress bool, signingOptions SigningOptions, dockerRegistryOptions *DockerRegistryOptions) error { + // PushImage pushes the src image to the destination + //func PushImage(source, destination string, options CopyOptions) error { + if destination == "" { + return errors.Wrapf(syscall.EINVAL, "destination image name must be specified") + } + + // Get the destination Image Reference + dest, err := alltransports.ParseImageName(destination) + if err != nil { + if hasTransport(destination) { + return errors.Wrapf(err, "error getting destination imageReference for %q", destination) } - for _, registry := range registries { - decomposedImage.registry = registry - pullNames = append(pullNames, decomposedImage.assemble()) + // Try adding the images default transport + destination2 := DefaultTransport + destination + dest, err = alltransports.ParseImageName(destination2) + if err != nil { + return err } } - return pullNames, nil + + sc := GetSystemContext(signaturePolicyPath, authFile, forceCompress) + + policyContext, err := getPolicyContext(sc) + if err != nil { + return err + } + defer policyContext.Destroy() + + // Look up the source image, expecting it to be in local storage + src, err := is.Transport.ParseStoreReference(i.imageruntime.store, i.ID()) + if err != nil { + return errors.Wrapf(err, "error getting source imageReference for %q", i.InputName) + } + + copyOptions := getCopyOptions(writer, signaturePolicyPath, nil, dockerRegistryOptions, signingOptions, authFile, manifestMIMEType, forceCompress) + + // Copy the image to the remote destination + err = cp.Image(policyContext, dest, src, copyOptions) + if err != nil { + return errors.Wrapf(err, "Error copying image to the remote destination") + } + return nil +} + +// MatchesID returns a bool based on if the input id +// matches the image's id +func (i *Image) MatchesID(id string) bool { + return strings.HasPrefix(i.ID(), id) } -// 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, +// toStorageReference returns a *storageReference from an Image +func (i *Image) toStorageReference() (types.ImageReference, error) { + return is.Transport.ParseStoreReference(i.imageruntime.store, i.ID()) +} + +// toImageRef returns an Image Reference type from an image +func (i *Image) toImageRef() (types.Image, error) { + ref, err := is.Transport.ParseStoreReference(i.imageruntime.store, "@"+i.ID()) + if err != nil { + return nil, errors.Wrapf(err, "error parsing reference to image %q", i.ID()) + } + imgRef, err := ref.NewImage(nil) + if err != nil { + return nil, errors.Wrapf(err, "error reading image %q", i.ID()) } - return i.runtime.PullImage(name, options) + return imgRef, nil } -// 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 +// sizer knows its size. +type sizer interface { + Size() (int64, error) +} + +//Size returns the size of the image +func (i *Image) Size() (*uint64, error) { + storeRef, err := is.Transport.ParseStoreReference(i.imageruntime.store, i.ID()) + if err != nil { + return nil, err + } + systemContext := &types.SystemContext{} + img, err := storeRef.NewImageSource(systemContext) + if err != nil { + return nil, err + } + if s, ok := img.(sizer); ok { + if sum, err := s.Size(); err == nil { + usum := uint64(sum) + return &usum, nil + } + } + return nil, errors.Errorf("unable to determine size") + } diff --git a/libpod/image/image_test.go b/libpod/image/image_test.go index 4b5b782b1..0e2f0c241 100644 --- a/libpod/image/image_test.go +++ b/libpod/image/image_test.go @@ -2,15 +2,12 @@ package image import ( "fmt" + "io" "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" ) @@ -20,43 +17,9 @@ var ( 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 + img *Image names []string } @@ -66,8 +29,11 @@ func mkWorkDir() (string, error) { } // shutdown the runtime and clean behind it -func cleanup(r *libpod.Runtime, workdir string) { - r.Shutdown(true) +func cleanup(workdir string, ir *Runtime) { + if err := ir.Shutdown(false); err != nil { + fmt.Println(err) + os.Exit(1) + } err := os.RemoveAll(workdir) if err != nil { fmt.Println(err) @@ -75,46 +41,27 @@ func cleanup(r *libpod.Runtime, workdir string) { } } -func makeLocalMatrix(r *libpod.Runtime) ([]localImageTest, error) { +func makeLocalMatrix(b, bg *Image) ([]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.names = b.Names() + busybox.names = append(busybox.names, []string{"bb:latest", "bb", b.ID(), b.ID()[0:7], fmt.Sprintf("busybox@%s", b.Digest())}...) // 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) + l = append(l, busybox, busyboxGlibc) return l, nil } @@ -124,32 +71,37 @@ func makeLocalMatrix(r *libpod.Runtime) ([]localImageTest, error) { func TestImage_NewFromLocal(t *testing.T) { workdir, err := mkWorkDir() assert.NoError(t, err) - runtime, err := setupRuntime(workdir) - assert.NoError(t, err) + so := storage.StoreOptions{ + RunRoot: workdir, + GraphRoot: workdir, + } + var writer io.Writer + writer = os.Stdout // Need images to be present for this test - _, err = runtime.PullImage("docker.io/library/busybox:latest", libpod.CopyOptions{}) + ir, err := NewImageRuntime(so) assert.NoError(t, err) - _, err = runtime.PullImage("docker.io/library/busybox:glibc", libpod.CopyOptions{}) + bb, err := ir.New("docker.io/library/busybox:latest", "", "", writer, nil, SigningOptions{}) assert.NoError(t, err) - _, err = runtime.PullImage("registry.fedoraproject.org/fedora-minimal:latest", libpod.CopyOptions{}) + bbglibc, err := ir.New("docker.io/library/busybox:glibc", "", "", writer, nil, SigningOptions{}) assert.NoError(t, err) - tm, err := makeLocalMatrix(runtime) + tm, err := makeLocalMatrix(bb, bbglibc) assert.NoError(t, err) + for _, image := range tm { // tag our images - err = tagImage(runtime, image.fqname, image.taggedName) + image.img.TagImage(image.taggedName) assert.NoError(t, err) for _, name := range image.names { - newImage, err := NewFromLocal(name, runtime) + newImage, err := ir.NewFromLocal(name) assert.NoError(t, err) - assert.Equal(t, newImage.ID(), image.img.ID) + assert.Equal(t, newImage.ID(), image.img.ID()) } } // Shutdown the runtime and remove the temporary storage - cleanup(runtime, workdir) + cleanup(workdir, ir) } // TestImage_New tests pulling the image by various names, tags, and from @@ -158,21 +110,23 @@ func TestImage_New(t *testing.T) { var names []string workdir, err := mkWorkDir() assert.NoError(t, err) - runtime, err := setupRuntime(workdir) - assert.NoError(t, err) + so := storage.StoreOptions{ + RunRoot: workdir, + GraphRoot: workdir, + } + ir, err := NewImageRuntime(so) + assert.NoError(t, err) // Build the list of pull names names = append(names, bbNames...) names = append(names, fedoraNames...) + var writer io.Writer + writer = os.Stdout // 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) + newImage, err := ir.New(img, "", "", writer, nil, SigningOptions{}) assert.NoError(t, err) assert.NotEqual(t, newImage.ID(), "") err = newImage.Remove(false) @@ -180,5 +134,5 @@ func TestImage_New(t *testing.T) { } // Shutdown the runtime and remove the temporary storage - cleanup(runtime, workdir) + cleanup(workdir, ir) } diff --git a/libpod/image/parts.go b/libpod/image/parts.go index e4ae489f9..979f223fc 100644 --- a/libpod/image/parts.go +++ b/libpod/image/parts.go @@ -47,6 +47,7 @@ func decompose(input string) (imageParts, error) { name: imageName, tag: tag, isTagged: isTagged, + transport: DefaultTransport, }, nil } @@ -54,3 +55,8 @@ func decompose(input string) (imageParts, error) { func (ip *imageParts) assemble() string { return fmt.Sprintf("%s/%s:%s", ip.registry, ip.name, ip.tag) } + +// assemble concatenates an image's parts with transport into a string +func (ip *imageParts) assembleWithTransport() string { + return fmt.Sprintf("%s%s/%s:%s", ip.transport, ip.registry, ip.name, ip.tag) +} diff --git a/libpod/image/pull.go b/libpod/image/pull.go new file mode 100644 index 000000000..52ef175d4 --- /dev/null +++ b/libpod/image/pull.go @@ -0,0 +1,246 @@ +package image + +import ( + "fmt" + "io" + "os" + "strings" + + cp "github.com/containers/image/copy" + "github.com/containers/image/directory" + "github.com/containers/image/docker" + dockerarchive "github.com/containers/image/docker/archive" + "github.com/containers/image/docker/tarfile" + ociarchive "github.com/containers/image/oci/archive" + "github.com/containers/image/pkg/sysregistries" + is "github.com/containers/image/storage" + "github.com/containers/image/tarball" + "github.com/containers/image/transports/alltransports" + "github.com/containers/image/types" + "github.com/pkg/errors" +) + +var ( + // DockerArchive is the transport we prepend to an image name + // when saving to docker-archive + DockerArchive = dockerarchive.Transport.Name() + // OCIArchive is the transport we prepend to an image name + // when saving to oci-archive + OCIArchive = ociarchive.Transport.Name() + // DirTransport is the transport for pushing and pulling + // images to and from a directory + DirTransport = directory.Transport.Name() + // TransportNames are the supported transports in string form + TransportNames = [...]string{DefaultTransport, DockerArchive, OCIArchive, "ostree:", "dir:"} + // TarballTransport is the transport for importing a tar archive + // and creating a filesystem image + TarballTransport = tarball.Transport.Name() + // DockerTransport is the transport for docker registries + DockerTransport = docker.Transport.Name() + "://" + // AtomicTransport is the transport for atomic registries + AtomicTransport = "atomic" + // DefaultTransport is a prefix that we apply to an image name + DefaultTransport = DockerTransport +) + +type pullStruct struct { + image string + srcRef types.ImageReference + dstRef types.ImageReference +} + +func (ir *Runtime) getPullStruct(srcRef types.ImageReference, destName string) (*pullStruct, error) { + reference := destName + if srcRef.DockerReference() != nil { + reference = srcRef.DockerReference().String() + } + destRef, err := is.Transport.ParseStoreReference(ir.store, reference) + if err != nil { + return nil, errors.Errorf("error parsing dest reference name: %v", err) + } + return &pullStruct{ + image: destName, + srcRef: srcRef, + dstRef: destRef, + }, nil +} + +// returns a list of pullStruct with the srcRef and DstRef based on the transport being used +func (ir *Runtime) getPullListFromRef(srcRef types.ImageReference, imgName string, sc *types.SystemContext) ([]*pullStruct, error) { + var pullStructs []*pullStruct + splitArr := strings.Split(imgName, ":") + archFile := splitArr[len(splitArr)-1] + + // supports pulling from docker-archive, oci, and registries + if srcRef.Transport().Name() == DockerArchive { + tarSource, err := tarfile.NewSourceFromFile(archFile) + if err != nil { + return nil, err + } + manifest, err := tarSource.LoadTarManifest() + + if err != nil { + return nil, errors.Errorf("error retrieving manifest.json: %v", err) + } + // to pull the first image stored in the tar file + if len(manifest) == 0 { + // use the hex of the digest if no manifest is found + reference, err := getImageDigest(srcRef, sc) + if err != nil { + return nil, err + } + pullInfo, err := ir.getPullStruct(srcRef, reference) + if err != nil { + return nil, err + } + pullStructs = append(pullStructs, pullInfo) + } else { + var dest string + if len(manifest[0].RepoTags) > 0 { + dest = manifest[0].RepoTags[0] + } else { + // If the input image has no repotags, we need to feed it a dest anyways + dest, err = getImageDigest(srcRef, sc) + if err != nil { + return nil, err + } + } + pullInfo, err := ir.getPullStruct(srcRef, dest) + if err != nil { + return nil, err + } + pullStructs = append(pullStructs, pullInfo) + } + } else if srcRef.Transport().Name() == OCIArchive { + // retrieve the manifest from index.json to access the image name + manifest, err := ociarchive.LoadManifestDescriptor(srcRef) + if err != nil { + return nil, errors.Wrapf(err, "error loading manifest for %q", srcRef) + } + + if manifest.Annotations == nil || manifest.Annotations["org.opencontainers.image.ref.name"] == "" { + return nil, errors.Errorf("error, archive doesn't have a name annotation. Cannot store image with no name") + } + pullInfo, err := ir.getPullStruct(srcRef, manifest.Annotations["org.opencontainers.image.ref.name"]) + if err != nil { + return nil, err + } + pullStructs = append(pullStructs, pullInfo) + } else if srcRef.Transport().Name() == DirTransport { + // supports pull from a directory + image := splitArr[1] + // remove leading "/" + if image[:1] == "/" { + image = image[1:] + } + pullInfo, err := ir.getPullStruct(srcRef, image) + if err != nil { + return nil, err + } + pullStructs = append(pullStructs, pullInfo) + } else { + pullInfo, err := ir.getPullStruct(srcRef, imgName) + if err != nil { + return nil, err + } + pullStructs = append(pullStructs, pullInfo) + } + return pullStructs, nil +} + +// pullImage pulls an image from configured registries +// By default, only the latest tag (or a specific tag if requested) will be +// pulled. +func (i *Image) pullImage(writer io.Writer, authfile, signaturePolicyPath string, signingOptions SigningOptions, dockerOptions *DockerRegistryOptions) (string, error) { + // pullImage copies the image from the source to the destination + var pullStructs []*pullStruct + sc := GetSystemContext(signaturePolicyPath, authfile, false) + srcRef, err := alltransports.ParseImageName(i.InputName) + if err != nil { + // could be trying to pull from registry with short name + pullStructs, err = i.createNamesToPull() + if err != nil { + return "", errors.Wrap(err, "error getting default registries to try") + } + } else { + pullStructs, err = i.imageruntime.getPullListFromRef(srcRef, i.InputName, sc) + if err != nil { + return "", errors.Wrapf(err, "error getting pullStruct info to pull image %q", i.InputName) + } + } + policyContext, err := getPolicyContext(sc) + if err != nil { + return "", err + } + defer policyContext.Destroy() + + copyOptions := getCopyOptions(writer, signaturePolicyPath, dockerOptions, nil, signingOptions, authfile, "", false) + for _, imageInfo := range pullStructs { + // Print the following statement only when pulling from a docker or atomic registry + if writer != nil && (imageInfo.srcRef.Transport().Name() == DockerTransport || imageInfo.srcRef.Transport().Name() == AtomicTransport) { + io.WriteString(writer, fmt.Sprintf("Trying to pull %s...\n", imageInfo.image)) + } + if err = cp.Image(policyContext, imageInfo.dstRef, imageInfo.srcRef, copyOptions); err != nil { + if writer != nil { + io.WriteString(writer, "Failed\n") + } + } else { + return imageInfo.image, nil + } + } + return "", errors.Wrapf(err, "error pulling image from") +} + +// 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() ([]*pullStruct, error) { + var pullNames []*pullStruct + decomposedImage, err := decompose(i.InputName) + if err != nil { + return nil, err + } + if decomposedImage.hasRegistry { + srcRef, err := alltransports.ParseImageName(decomposedImage.assembleWithTransport()) + if err != nil { + return nil, errors.Errorf("unable to parse '%s'", i.InputName) + } + ps := pullStruct{ + image: i.InputName, + srcRef: srcRef, + } + pullNames = append(pullNames, &ps) + + } else { + registryConfigPath := "" + envOverride := os.Getenv("REGISTRIES_CONFIG_PATH") + if len(envOverride) > 0 { + registryConfigPath = envOverride + } + searchRegistries, err := sysregistries.GetRegistries(&types.SystemContext{SystemRegistriesConfPath: registryConfigPath}) + if err != nil { + return nil, err + } + for _, registry := range searchRegistries { + decomposedImage.registry = registry + srcRef, err := alltransports.ParseImageName(decomposedImage.assembleWithTransport()) + if err != nil { + return nil, errors.Errorf("unable to parse '%s'", i.InputName) + } + ps := pullStruct{ + image: decomposedImage.assemble(), + srcRef: srcRef, + } + pullNames = append(pullNames, &ps) + } + } + + for _, pStruct := range pullNames { + destRef, err := is.Transport.ParseStoreReference(i.imageruntime.store, pStruct.image) + if err != nil { + return nil, errors.Errorf("error parsing dest reference name: %v", err) + } + pStruct.dstRef = destRef + } + + return pullNames, nil +} diff --git a/libpod/image/signing_options.go b/libpod/image/signing_options.go new file mode 100644 index 000000000..f310da749 --- /dev/null +++ b/libpod/image/signing_options.go @@ -0,0 +1,10 @@ +package image + +// SigningOptions encapsulates settings that control whether or not we strip or +// add signatures to images when writing them. +type SigningOptions struct { + // RemoveSignatures directs us to remove any signatures which are already present. + RemoveSignatures bool + // SignBy is a key identifier of some kind, indicating that a signature should be generated using the specified private key and stored with the image. + SignBy string +} diff --git a/libpod/image/utils.go b/libpod/image/utils.go index f312c8e4d..adc795e3a 100644 --- a/libpod/image/utils.go +++ b/libpod/image/utils.go @@ -1,9 +1,16 @@ package image import ( + "io" + + cp "github.com/containers/image/copy" "github.com/containers/image/docker/reference" "github.com/containers/storage" "github.com/pkg/errors" + + "github.com/containers/image/signature" + "github.com/containers/image/types" + "strings" ) func getTags(nameInput string) (reference.NamedTagged, bool, error) { @@ -18,17 +25,17 @@ func getTags(nameInput string) (reference.NamedTagged, bool, error) { // 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) { +func findImageInRepotags(search imageParts, images []*Image) (*storage.Image, error) { var results []*storage.Image for _, image := range images { - for _, name := range image.Names { + 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) + results = append(results, image.image) break } } @@ -40,3 +47,42 @@ func findImageInRepotags(search imageParts, images []*storage.Image) (*storage.I } return results[0], nil } + +// getCopyOptions constructs a new containers/image/copy.Options{} struct from the given parameters +func getCopyOptions(reportWriter io.Writer, signaturePolicyPath string, srcDockerRegistry, destDockerRegistry *DockerRegistryOptions, signing SigningOptions, authFile, manifestType string, forceCompress bool) *cp.Options { + if srcDockerRegistry == nil { + srcDockerRegistry = &DockerRegistryOptions{} + } + if destDockerRegistry == nil { + destDockerRegistry = &DockerRegistryOptions{} + } + srcContext := srcDockerRegistry.GetSystemContext(signaturePolicyPath, authFile, forceCompress) + destContext := destDockerRegistry.GetSystemContext(signaturePolicyPath, authFile, forceCompress) + return &cp.Options{ + RemoveSignatures: signing.RemoveSignatures, + SignBy: signing.SignBy, + ReportWriter: reportWriter, + SourceCtx: srcContext, + DestinationCtx: destContext, + ForceManifestMIMEType: manifestType, + } +} + +// getPolicyContext sets up, intializes and returns a new context for the specified policy +func getPolicyContext(ctx *types.SystemContext) (*signature.PolicyContext, error) { + policy, err := signature.DefaultPolicy(ctx) + if err != nil { + return nil, err + } + + policyContext, err := signature.NewPolicyContext(policy) + if err != nil { + return nil, err + } + return policyContext, nil +} + +// hasTransport determines if the image string contains '://', returns bool +func hasTransport(image string) bool { + return strings.Contains(image, "://") +} diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go index 53d39ffca..fe9864dca 100644 --- a/libpod/runtime_img.go +++ b/libpod/runtime_img.go @@ -29,6 +29,7 @@ import ( "github.com/projectatomic/libpod/libpod/common" "github.com/projectatomic/libpod/libpod/driver" "github.com/projectatomic/libpod/pkg/inspect" + "github.com/projectatomic/libpod/pkg/util" ) // Runtime API @@ -312,7 +313,7 @@ func (k *Image) Decompose() error { if err != nil { return nil } - if StringInSlice(k.Registry, registries) { + if util.StringInSlice(k.Registry, registries) { return nil } // We need to check if the registry name is legit diff --git a/libpod/util.go b/libpod/util.go index ca93fc097..c258af307 100644 --- a/libpod/util.go +++ b/libpod/util.go @@ -39,16 +39,6 @@ func WriteFile(content string, path string) error { return nil } -// StringInSlice determines if a string is in a string slice, returns bool -func StringInSlice(s string, sl []string) bool { - for _, i := range sl { - if i == s { - return true - } - } - return false -} - // FuncTimer helps measure the execution time of a function // For debug purposes, do not leave in code // used like defer FuncTimer("foo") diff --git a/libpod/util_test.go b/libpod/util_test.go index 7b9d19a43..70e989e1a 100644 --- a/libpod/util_test.go +++ b/libpod/util_test.go @@ -5,19 +5,6 @@ import ( "testing" ) -var ( - sliceData = []string{"one", "two", "three", "four"} -) - -func TestStringInSlice(t *testing.T) { - // string is in the slice - assert.True(t, StringInSlice("one", sliceData)) - // string is not in the slice - assert.False(t, StringInSlice("five", sliceData)) - // string is not in empty slice - assert.False(t, StringInSlice("one", []string{})) -} - func TestRemoveScientificNotationFromFloat(t *testing.T) { numbers := []float64{0.0, .5, 1.99999932, 1.04e+10} results := []float64{0.0, .5, 1.99999932, 1.04} diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 9a93021e4..edcf63f80 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -44,3 +44,13 @@ func ParseRegistryCreds(creds string) (*types.DockerAuthConfig, error) { Password: password, }, nil } + +// StringInSlice determines if a string is in a string slice, returns bool +func StringInSlice(s string, sl []string) bool { + for _, i := range sl { + if i == s { + return true + } + } + return false +} diff --git a/pkg/util/utils_test.go b/pkg/util/utils_test.go new file mode 100644 index 000000000..f47c0b7ad --- /dev/null +++ b/pkg/util/utils_test.go @@ -0,0 +1,19 @@ +package util + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +var ( + sliceData = []string{"one", "two", "three", "four"} +) + +func TestStringInSlice(t *testing.T) { + // string is in the slice + assert.True(t, StringInSlice("one", sliceData)) + // string is not in the slice + assert.False(t, StringInSlice("five", sliceData)) + // string is not in empty slice + assert.False(t, StringInSlice("one", []string{})) +} |