summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbaude <bbaude@redhat.com>2018-03-08 15:45:52 -0600
committerAtomic Bot <atomic-devel@projectatomic.io>2018-03-14 20:21:31 +0000
commitb85b217f55993955da9ad0cae7735747b2f24390 (patch)
tree3e633600e50bf3bedc9bdc59abe789fa01f55601
parentbc358eb396aa87f3122f0449945efc03ed64bfd2 (diff)
downloadpodman-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.go5
-rw-r--r--cmd/podman/exec.go3
-rw-r--r--cmd/podman/inspect.go3
-rw-r--r--cmd/podman/ps.go3
-rw-r--r--libpod/container_internal.go3
-rw-r--r--libpod/container_top.go3
-rw-r--r--libpod/image/docker_registry_options.go46
-rw-r--r--libpod/image/image.go370
-rw-r--r--libpod/image/image_test.go120
-rw-r--r--libpod/image/parts.go6
-rw-r--r--libpod/image/pull.go246
-rw-r--r--libpod/image/signing_options.go10
-rw-r--r--libpod/image/utils.go52
-rw-r--r--libpod/runtime_img.go3
-rw-r--r--libpod/util.go10
-rw-r--r--libpod/util_test.go13
-rw-r--r--pkg/util/utils.go10
-rw-r--r--pkg/util/utils_test.go19
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{}))
+}