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
This commit is contained in:
baude
2018-03-08 15:45:52 -06:00
committed by Atomic Bot
parent bc358eb396
commit b85b217f55
18 changed files with 746 additions and 181 deletions

View File

@ -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 '.'")
}

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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...)
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
//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,
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,
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
newImage.InputName = imageName
img, err := newImage.getLocalImage()
newImage.image = img
return newImage, err
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
}
return newImage, errors.Errorf("unable to find %s", name)
func (i *Image) reloadImage() error {
newImage, err := i.imageruntime.getImage(i.ID())
if err != nil {
return errors.Wrapf(err, "unable to reload image")
}
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)
if err != nil {
return nil, err
// Digest returns the image's Manifest
func (i *Image) Digest() digest.Digest {
return i.image.Digest
}
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
// Names returns a string array of names associated with the image
func (i *Image) Names() []string {
return i.image.Names
}
// 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)
// Created returns the time the image was created
func (i *Image) Created() time.Time {
return i.image.Created
}
// Remove an image
// This function is only complete enough for the stage 1 tests.
// 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.runtime.RemoveImage(i.image, force)
_, 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
}
// 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)
}
// Try adding the images default transport
destination2 := DefaultTransport + destination
dest, err = alltransports.ParseImageName(destination2)
if err != nil {
return err
}
}
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)
}
// 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 imgRef, nil
}
// 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")
}

View File

@ -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)
}

View File

@ -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)
}

246
libpod/image/pull.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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, "://")
}

View File

@ -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

View File

@ -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")

View File

@ -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}

View File

@ -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
}

19
pkg/util/utils_test.go Normal file
View File

@ -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{}))
}