From 356df5d18eba5e535d895421c42255654ca82ba9 Mon Sep 17 00:00:00 2001 From: umohnani8 Date: Thu, 21 Sep 2017 15:21:56 -0400 Subject: [PATCH] Continue switching from libkpod to libpod Refactored rmi, images, diff, and history. Made fixes to kpod images in the way it was handing the templates as well as printing the image names Signed-off-by: umohnani8 --- cmd/kpod/diff.go | 19 +- cmd/kpod/history.go | 54 +---- cmd/kpod/images.go | 339 +++++++++++++++++--------- cmd/kpod/load.go | 6 +- cmd/kpod/rmi.go | 92 +------ cmd/kpod/save.go | 9 +- cmd/kpod/stats.go | 5 +- libpod/{images => }/copy_data.go | 138 ++++++++++- libpod/{images => }/copy_ref.go | 2 +- {libkpod => libpod}/diff.go | 23 +- libpod/images/copy.go | 208 ---------------- libpod/images/image.go | 288 ---------------------- libpod/images/image_data.go | 39 +++ libpod/images/rmi.go | 35 --- libpod/runtime_ctr.go | 31 ++- libpod/{image.go => runtime_img.go} | 355 ++++++++++++++++++++++++++-- test/kpod_inspect.bats | 2 +- test/kpod_tag.bats | 12 +- 18 files changed, 820 insertions(+), 837 deletions(-) rename libpod/{images => }/copy_data.go (79%) rename libpod/{images => }/copy_ref.go (99%) rename {libkpod => libpod}/diff.go (60%) delete mode 100644 libpod/images/copy.go delete mode 100644 libpod/images/image.go delete mode 100644 libpod/images/rmi.go rename libpod/{image.go => runtime_img.go} (51%) diff --git a/cmd/kpod/diff.go b/cmd/kpod/diff.go index 7e4f8c81..c28bdfce 100644 --- a/cmd/kpod/diff.go +++ b/cmd/kpod/diff.go @@ -2,9 +2,9 @@ package main import ( "fmt" + "github.com/containers/storage/pkg/archive" "github.com/kubernetes-incubator/cri-o/cmd/kpod/formats" - "github.com/kubernetes-incubator/cri-o/libkpod" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -74,25 +74,22 @@ func formatJSON(output []diffOutputParams) (diffJSONOutput, error) { } func diffCmd(c *cli.Context) error { - if len(c.Args()) != 1 { - return errors.Errorf("container, layer, or image name must be specified: kpod diff [options [...]] ID-NAME") - } if err := validateFlags(c, diffFlags); err != nil { return err } - config, err := getConfig(c) - if err != nil { - return errors.Wrapf(err, "could not get config") + + if len(c.Args()) != 1 { + return errors.Errorf("container, image, or layer name must be specified: kpod diff [options [...]] ID-NAME") } - server, err := libkpod.New(config) + runtime, err := getRuntime(c) if err != nil { - return errors.Wrapf(err, "could not get container server") + return errors.Wrapf(err, "could not get runtime") } - defer server.Shutdown() + defer runtime.Shutdown(false) to := c.Args().Get(0) - changes, err := server.GetDiff("", to) + changes, err := runtime.GetDiff("", to) if err != nil { return errors.Wrapf(err, "could not get changes for %q", to) } diff --git a/cmd/kpod/history.go b/cmd/kpod/history.go index 2eff7a97..dd0da38a 100644 --- a/cmd/kpod/history.go +++ b/cmd/kpod/history.go @@ -6,12 +6,9 @@ import ( "strings" "time" - is "github.com/containers/image/storage" "github.com/containers/image/types" - "github.com/containers/storage" units "github.com/docker/go-units" "github.com/kubernetes-incubator/cri-o/cmd/kpod/formats" - "github.com/kubernetes-incubator/cri-o/libpod/common" "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/urfave/cli" @@ -45,7 +42,6 @@ type historyJSONParams struct { // historyOptions stores cli flag values type historyOptions struct { - image string human bool noTrunc bool quiet bool @@ -88,14 +84,12 @@ func historyCmd(c *cli.Context) error { if err := validateFlags(c, historyFlags); err != nil { return err } - config, err := getConfig(c) + + runtime, err := getRuntime(c) if err != nil { return errors.Wrapf(err, "Could not get config") } - store, err := getStore(config) - if err != nil { - return err - } + defer runtime.Shutdown(false) format := genHistoryFormat(c.Bool("quiet")) if c.IsSet("format") { @@ -112,13 +106,18 @@ func historyCmd(c *cli.Context) error { imgName := args[0] opts := historyOptions{ - image: imgName, human: c.BoolT("human"), noTrunc: c.Bool("no-trunc"), quiet: c.Bool("quiet"), format: format, } - return generateHistoryOutput(store, opts) + + history, layers, imageID, err := runtime.GetHistory(imgName) + if err != nil { + return errors.Wrapf(err, "error getting history of image %q", imgName) + } + + return generateHistoryOutput(history, layers, imageID, opts) } func genHistoryFormat(quiet bool) (format string) { @@ -154,33 +153,6 @@ func (h *historyTemplateParams) headerMap() map[string]string { return values } -// getHistory gets the history of an image and information about its layers -func getHistory(store storage.Store, image string) ([]v1.History, []types.BlobInfo, string, error) { - ref, err := is.Transport.ParseStoreReference(store, image) - if err != nil { - return nil, nil, "", errors.Wrapf(err, "error parsing reference to image %q", image) - } - - img, err := is.Transport.GetStoreImage(store, ref) - if err != nil { - return nil, nil, "", errors.Wrapf(err, "no such image %q", image) - } - - systemContext := common.GetSystemContext("") - - src, err := ref.NewImage(systemContext) - if err != nil { - return nil, nil, "", errors.Wrapf(err, "error instantiating image %q", image) - } - - oci, err := src.OCIConfig() - if err != nil { - return nil, nil, "", err - } - - return oci.History, src.LayerInfos(), img.ID, nil -} - // getHistorytemplateOutput gets the modified history information to be printed in human readable format func getHistoryTemplateOutput(history []v1.History, layers []types.BlobInfo, imageID string, opts historyOptions) (historyOutput []historyTemplateParams) { var ( @@ -251,11 +223,7 @@ func getHistoryJSONOutput(history []v1.History, layers []types.BlobInfo, imageID } // generateHistoryOutput generates the history based on the format given -func generateHistoryOutput(store storage.Store, opts historyOptions) error { - history, layers, imageID, err := getHistory(store, opts.image) - if err != nil { - return errors.Wrapf(err, "error getting history of image %q", opts.image) - } +func generateHistoryOutput(history []v1.History, layers []types.BlobInfo, imageID string, opts historyOptions) error { if len(history) == 0 { return nil } diff --git a/cmd/kpod/images.go b/cmd/kpod/images.go index 535fdf34..d7824ba3 100644 --- a/cmd/kpod/images.go +++ b/cmd/kpod/images.go @@ -1,17 +1,46 @@ package main import ( + "fmt" "reflect" "strings" + "time" + "github.com/containers/image/types" "github.com/containers/storage" + "github.com/docker/go-units" "github.com/kubernetes-incubator/cri-o/cmd/kpod/formats" - libpod "github.com/kubernetes-incubator/cri-o/libpod/images" + "github.com/kubernetes-incubator/cri-o/libpod" + "github.com/kubernetes-incubator/cri-o/libpod/common" digest "github.com/opencontainers/go-digest" "github.com/pkg/errors" "github.com/urfave/cli" ) +type imagesTemplateParams struct { + ID string + Name string + Digest digest.Digest + CreatedAt string + Size string +} + +type imagesJSONParams struct { + ID string `json:"id"` + Name []string `json:"names"` + Digest digest.Digest `json:"digest"` + CreatedAt time.Time `json:"created"` + Size int64 `json:"size"` +} + +type imagesOptions struct { + quiet bool + noHeading bool + noTrunc bool + digests bool + format string +} + var ( imagesFlags = []cli.Flag{ cli.BoolFlag{ @@ -55,145 +84,88 @@ func imagesCmd(c *cli.Context) error { if err := validateFlags(c, imagesFlags); err != nil { return err } - config, err := getConfig(c) - if err != nil { - return errors.Wrapf(err, "Could not get config") - } - store, err := getStore(config) - if err != nil { - return err - } - quiet := false - if c.IsSet("quiet") { - quiet = c.Bool("quiet") + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "Could not get runtime") } - noheading := false - if c.IsSet("noheading") { - noheading = c.Bool("noheading") - } - truncate := true - if c.IsSet("no-trunc") { - truncate = !c.Bool("no-trunc") - } - digests := false - if c.IsSet("digests") { - digests = c.Bool("digests") - } - outputFormat := genImagesFormat(quiet, truncate, digests) + defer runtime.Shutdown(false) + + var format string if c.IsSet("format") { - outputFormat = c.String("format") + format = c.String("format") + } else { + format = genImagesFormat(c.Bool("quiet"), c.Bool("noheading"), c.Bool("digests")) } - name := "" + opts := imagesOptions{ + quiet: c.Bool("quiet"), + noHeading: c.Bool("noheading"), + noTrunc: c.Bool("no-trunc"), + digests: c.Bool("digests"), + format: format, + } + + var imageInput string if len(c.Args()) == 1 { - name = c.Args().Get(0) - } else if len(c.Args()) > 1 { + imageInput = c.Args().Get(0) + } + if len(c.Args()) > 1 { return errors.New("'kpod images' requires at most 1 argument") } - var params *libpod.FilterParams - if c.IsSet("filter") { - params, err = libpod.ParseFilter(store, c.String("filter")) - if err != nil { - return errors.Wrapf(err, "error parsing filter") - } - } else { - params = nil + params, err := runtime.ParseImageFilter(imageInput, c.String("filter")) + if err != nil { + return errors.Wrapf(err, "error parsing filter") } - imageList, err := libpod.GetImagesMatchingFilter(store, params, name) + // generate the different filters + labelFilter := generateImagesFilter(params, "label") + beforeImageFilter := generateImagesFilter(params, "before-image") + sinceImageFilter := generateImagesFilter(params, "since-image") + danglingFilter := generateImagesFilter(params, "dangling") + referenceFilter := generateImagesFilter(params, "reference") + imageInputFilter := generateImagesFilter(params, "image-input") + + images, err := runtime.GetImages(params, labelFilter, beforeImageFilter, sinceImageFilter, danglingFilter, referenceFilter, imageInputFilter) if err != nil { return errors.Wrapf(err, "could not get list of images matching filter") } - return outputImages(store, imageList, truncate, digests, quiet, outputFormat, noheading) + return generateImagesOutput(runtime, images, opts) } -func genImagesFormat(quiet, truncate, digests bool) (format string) { +func genImagesFormat(quiet, noHeading, digests bool) (format string) { if quiet { return formats.IDString } - if truncate { - format = "table {{ .ID | printf \"%-20.12s\" }} " - } else { - format = "table {{ .ID | printf \"%-64s\" }} " + format = "table {{.ID}}\t{{.Name}}\t" + if noHeading { + format = "{{.ID}}\t{{.Name}}\t" } - format += "{{ .Name | printf \"%-56s\" }} " - if digests { - format += "{{ .Digest | printf \"%-71s \"}} " + format += "{{.Digest}}\t" } - - format += "{{ .CreatedAt | printf \"%-22s\" }} {{.Size}}" + format += "{{.CreatedAt}}\t{{.Size}}\t" return } -func outputImages(store storage.Store, images []storage.Image, truncate, digests, quiet bool, outputFormat string, noheading bool) error { - imageOutput := []imageOutputParams{} - - lastID := "" - for _, img := range images { - if quiet && lastID == img.ID { - continue // quiet should not show the same ID multiple times +// imagesToGeneric creates an empty array of interfaces for output +func imagesToGeneric(templParams []imagesTemplateParams, JSONParams []imagesJSONParams) (genericParams []interface{}) { + if len(templParams) > 0 { + for _, v := range templParams { + genericParams = append(genericParams, interface{}(v)) } - createdTime := img.Created - - names := []string{""} - if len(img.Names) > 0 { - names = img.Names - } - - info, imageDigest, size, _ := libpod.InfoAndDigestAndSize(store, img) - if info != nil { - createdTime = info.Created - } - - params := imageOutputParams{ - ID: img.ID, - Name: names, - Digest: imageDigest, - CreatedAt: createdTime.Format("Jan 2, 2006 15:04"), - Size: libpod.FormattedSize(float64(size)), - } - imageOutput = append(imageOutput, params) + return } - - var out formats.Writer - - switch outputFormat { - case formats.JSONString: - out = formats.JSONStructArray{Output: toGeneric(imageOutput)} - default: - if len(imageOutput) == 0 { - out = formats.StdoutTemplateArray{} - } else { - out = formats.StdoutTemplateArray{Output: toGeneric(imageOutput), Template: outputFormat, Fields: imageOutput[0].headerMap()} - } + for _, v := range JSONParams { + genericParams = append(genericParams, interface{}(v)) } - - formats.Writer(out).Out() - - return nil + return } -type imageOutputParams struct { - ID string `json:"id"` - Name []string `json:"names"` - Digest digest.Digest `json:"digest"` - CreatedAt string `json:"created"` - Size string `json:"size"` -} - -func toGeneric(params []imageOutputParams) []interface{} { - genericParams := make([]interface{}, len(params)) - for i, v := range params { - genericParams[i] = interface{}(v) - } - return genericParams -} - -func (i *imageOutputParams) headerMap() map[string]string { +// generate the header based on the template provided +func (i *imagesTemplateParams) headerMap() map[string]string { v := reflect.Indirect(reflect.ValueOf(i)) values := make(map[string]string) @@ -207,3 +179,152 @@ func (i *imageOutputParams) headerMap() map[string]string { } return values } + +// getImagesTemplateOutput returns the images information to be printed in human readable format +func getImagesTemplateOutput(runtime *libpod.Runtime, images []*storage.Image, opts imagesOptions) (imagesOutput []imagesTemplateParams) { + var ( + lastID string + ) + for _, img := range images { + if opts.quiet && lastID == img.ID { + continue // quiet should not show the same ID multiple times + } + createdTime := img.Created + + imageID := img.ID + if !opts.noTrunc { + imageID = imageID[:idTruncLength] + } + + imageName := "" + if len(img.Names) > 0 { + imageName = img.Names[0] + } + + info, imageDigest, size, _ := runtime.InfoAndDigestAndSize(*img) + if info != nil { + createdTime = info.Created + } + + params := imagesTemplateParams{ + ID: imageID, + Name: imageName, + Digest: imageDigest, + CreatedAt: units.HumanDuration(time.Since((createdTime))) + " ago", + Size: units.HumanSize(float64(size)), + } + imagesOutput = append(imagesOutput, params) + } + return +} + +// getImagesJSONOutput returns the images information in its raw form +func getImagesJSONOutput(runtime *libpod.Runtime, images []*storage.Image) (imagesOutput []imagesJSONParams) { + for _, img := range images { + createdTime := img.Created + + info, imageDigest, size, _ := runtime.InfoAndDigestAndSize(*img) + if info != nil { + createdTime = info.Created + } + + params := imagesJSONParams{ + ID: img.ID, + Name: img.Names, + Digest: imageDigest, + CreatedAt: createdTime, + Size: size, + } + imagesOutput = append(imagesOutput, params) + } + return +} + +// generateImagesOutput generates the images based on the format provided +func generateImagesOutput(runtime *libpod.Runtime, images []*storage.Image, opts imagesOptions) error { + if len(images) == 0 { + return nil + } + + var out formats.Writer + + switch opts.format { + case formats.JSONString: + imagesOutput := getImagesJSONOutput(runtime, images) + out = formats.JSONStructArray{Output: imagesToGeneric([]imagesTemplateParams{}, imagesOutput)} + default: + imagesOutput := getImagesTemplateOutput(runtime, images, opts) + out = formats.StdoutTemplateArray{Output: imagesToGeneric(imagesOutput, []imagesJSONParams{}), Template: opts.format, Fields: imagesOutput[0].headerMap()} + + } + + return formats.Writer(out).Out() +} + +// generateImagesFilter returns an ImageFilter based on filterType +// to add more filters, define a new case and write what the ImageFilter function should do +func generateImagesFilter(params *libpod.ImageFilterParams, filterType string) libpod.ImageFilter { + switch filterType { + case "label": + return func(image *storage.Image, info *types.ImageInspectInfo) bool { + if params == nil || params.Label == "" { + return true + } + + pair := strings.SplitN(params.Label, "=", 2) + if val, ok := info.Labels[pair[0]]; ok { + if len(pair) == 2 && val == pair[1] { + return true + } + if len(pair) == 1 { + return true + } + } + return false + } + case "before-image": + return func(image *storage.Image, info *types.ImageInspectInfo) bool { + if params == nil || params.BeforeImage.IsZero() { + return true + } + return info.Created.Before(params.BeforeImage) + } + case "since-image": + return func(image *storage.Image, info *types.ImageInspectInfo) bool { + if params == nil || params.SinceImage.IsZero() { + return true + } + return info.Created.After(params.SinceImage) + } + case "dangling": + return func(image *storage.Image, info *types.ImageInspectInfo) bool { + if params == nil || params.Dangling == "" { + return true + } + if common.IsFalse(params.Dangling) && params.ImageName != "" { + return true + } + if common.IsTrue(params.Dangling) && params.ImageName == "" { + return true + } + return false + } + case "reference": + return func(image *storage.Image, info *types.ImageInspectInfo) bool { + if params == nil || params.ReferencePattern == "" { + return true + } + return libpod.MatchesReference(params.ImageName, params.ReferencePattern) + } + case "image-input": + return func(image *storage.Image, info *types.ImageInspectInfo) bool { + if params == nil || params.ImageInput == "" { + return true + } + return libpod.MatchesReference(params.ImageName, params.ImageInput) + } + default: + fmt.Println("invalid filter type", filterType) + return nil + } +} diff --git a/cmd/kpod/load.go b/cmd/kpod/load.go index 97401290..e3920805 100644 --- a/cmd/kpod/load.go +++ b/cmd/kpod/load.go @@ -5,7 +5,7 @@ import ( "io/ioutil" "os" - "github.com/kubernetes-incubator/cri-o/libpod/images" + "github.com/kubernetes-incubator/cri-o/libpod" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -91,9 +91,9 @@ func loadCmd(c *cli.Context) error { output = os.Stdout } - src := images.DockerArchive + ":" + input + src := libpod.DockerArchive + ":" + input if err := runtime.PullImage(src, false, "", output); err != nil { - src = images.OCIArchive + ":" + input + src = libpod.OCIArchive + ":" + input // generate full src name with specified image:tag if image != "" { src = src + ":" + image diff --git a/cmd/kpod/rmi.go b/cmd/kpod/rmi.go index fc4056d7..3713db45 100644 --- a/cmd/kpod/rmi.go +++ b/cmd/kpod/rmi.go @@ -3,8 +3,6 @@ package main import ( "fmt" - "github.com/containers/storage" - "github.com/kubernetes-incubator/cri-o/libpod/images" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -32,95 +30,27 @@ func rmiCmd(c *cli.Context) error { return err } - force := false - if c.IsSet("force") { - force = c.Bool("force") + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not get runtime") } + defer runtime.Shutdown(false) args := c.Args() if len(args) == 0 { return errors.Errorf("image name or ID must be specified") } - config, err := getConfig(c) - if err != nil { - return errors.Wrapf(err, "Could not get config") - } - store, err := getStore(config) - if err != nil { - return err - } - - for _, id := range args { - image, err := images.FindImage(store, id) + for _, arg := range args { + image, err := runtime.GetImage(arg) if err != nil { - return errors.Wrapf(err, "could not get image %q", id) + return errors.Wrapf(err, "could not get image %q", arg) } - if image != nil { - ctrIDs, err := runningContainers(image, store) - if err != nil { - return errors.Wrapf(err, "error getting running containers for image %q", id) - } - if len(ctrIDs) > 0 && len(image.Names) <= 1 { - if force { - removeContainers(ctrIDs, store) - } else { - for ctrID := range ctrIDs { - return fmt.Errorf("Could not remove image %q (must force) - container %q is using its reference image", id, ctrID) - } - } - } - // If the user supplied an ID, we cannot delete the image if it is referred to by multiple tags - if images.MatchesID(image.ID, id) { - if len(image.Names) > 1 && !force { - return fmt.Errorf("unable to delete %s (must force) - image is referred to in multiple tags", image.ID) - } - // If it is forced, we have to untag the image so that it can be deleted - image.Names = image.Names[:0] - } else { - name, err2 := images.UntagImage(store, image, id) - if err2 != nil { - return err - } - fmt.Printf("untagged: %s\n", name) - } - - if len(image.Names) > 1 { - continue - } - id, err := images.RemoveImage(image, store) - if err != nil { - return err - } - fmt.Printf("%s\n", id) - } - } - - return nil -} - -// Returns a list of running containers associated with the given ImageReference -// TODO: replace this with something in libkpod -func runningContainers(image *storage.Image, store storage.Store) ([]string, error) { - ctrIDs := []string{} - containers, err := store.Containers() - if err != nil { - return nil, err - } - for _, ctr := range containers { - if ctr.ImageID == image.ID { - ctrIDs = append(ctrIDs, ctr.ID) - } - } - return ctrIDs, nil -} - -// TODO: replace this with something in libkpod -func removeContainers(ctrIDs []string, store storage.Store) error { - for _, ctrID := range ctrIDs { - if err := store.DeleteContainer(ctrID); err != nil { - return errors.Wrapf(err, "could not remove container %q", ctrID) + id, err := runtime.RemoveImage(image, c.Bool("force")) + if err != nil { + return errors.Wrapf(err, "error removing image %q", id) } + fmt.Printf("%s\n", id) } return nil } diff --git a/cmd/kpod/save.go b/cmd/kpod/save.go index f04ba463..fac73e65 100644 --- a/cmd/kpod/save.go +++ b/cmd/kpod/save.go @@ -5,7 +5,6 @@ import ( "os" "github.com/kubernetes-incubator/cri-o/libpod" - "github.com/kubernetes-incubator/cri-o/libpod/images" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/urfave/cli" @@ -72,12 +71,12 @@ func saveCmd(c *cli.Context) error { var dst string switch c.String("format") { - case images.OCIArchive: - dst = images.OCIArchive + ":" + output - case images.DockerArchive: + case libpod.OCIArchive: + dst = libpod.OCIArchive + ":" + output + case libpod.DockerArchive: fallthrough case "": - dst = images.DockerArchive + ":" + output + dst = libpod.DockerArchive + ":" + output default: return errors.Errorf("unknown format option %q", c.String("format")) } diff --git a/cmd/kpod/stats.go b/cmd/kpod/stats.go index 0bf2e263..ac81212a 100644 --- a/cmd/kpod/stats.go +++ b/cmd/kpod/stats.go @@ -8,9 +8,10 @@ import ( "text/template" "time" + "github.com/docker/go-units" + tm "github.com/buger/goterm" "github.com/kubernetes-incubator/cri-o/libkpod" - "github.com/kubernetes-incubator/cri-o/libpod/images" "github.com/kubernetes-incubator/cri-o/oci" "github.com/pkg/errors" "github.com/urfave/cli" @@ -182,7 +183,7 @@ func outputStatsUsingFormatString(stats *libkpod.ContainerStats) { } func combineHumanValues(a, b uint64) string { - return fmt.Sprintf("%s / %s", images.FormattedSize(float64(a)), images.FormattedSize(float64(b))) + return fmt.Sprintf("%s / %s", units.HumanSize(float64(a)), units.HumanSize(float64(b))) } func floatToPercentString(f float64) string { diff --git a/libpod/images/copy_data.go b/libpod/copy_data.go similarity index 79% rename from libpod/images/copy_data.go rename to libpod/copy_data.go index 92cd3256..60df883e 100644 --- a/libpod/images/copy_data.go +++ b/libpod/copy_data.go @@ -1,4 +1,4 @@ -package images +package libpod import ( "encoding/json" @@ -11,12 +11,14 @@ import ( "github.com/containers/image/docker/reference" is "github.com/containers/image/storage" + "github.com/containers/image/transports" "github.com/containers/image/types" "github.com/containers/storage" "github.com/containers/storage/pkg/archive" "github.com/docker/docker/pkg/ioutils" "github.com/kubernetes-incubator/cri-o/cmd/kpod/docker" "github.com/kubernetes-incubator/cri-o/libpod/common" + "github.com/kubernetes-incubator/cri-o/libpod/driver" digest "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go/v1" ociv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -34,6 +36,28 @@ const ( OCIv1ImageManifest = v1.MediaTypeImageManifest ) +// Data handles the data used when inspecting a container +// nolint +type Data struct { + ID string + Tags []string + Digests []string + ManifestDigest digest.Digest + Comment string + Created *time.Time + Container string + Author string + Config ociv1.ImageConfig + Architecture string + OS string + Annotations map[string]string + CreatedBy string + Size uint + VirtualSize uint + GraphDriver driver.Data + RootFS ociv1.RootFS +} + // CopyData stores the basic data used when copying a container or image type CopyData struct { store storage.Store @@ -360,13 +384,13 @@ func (c *CopyData) Save() error { } // GetContainerCopyData gets the copy data for a container -func GetContainerCopyData(store storage.Store, name string) (*CopyData, error) { +func (r *Runtime) GetContainerCopyData(name string) (*CopyData, error) { var data *CopyData var err error if name != "" { - data, err = openCopyData(store, name) + data, err = openCopyData(r.store, name) if os.IsNotExist(errors.Cause(err)) { - data, err = importCopyData(store, name, "") + data, err = r.importCopyData(r.store, name, "") } } if err != nil { @@ -380,17 +404,17 @@ func GetContainerCopyData(store storage.Store, name string) (*CopyData, error) { } // GetImageCopyData gets the copy data for an image -func GetImageCopyData(store storage.Store, image string) (*CopyData, error) { +func (r *Runtime) GetImageCopyData(image string) (*CopyData, error) { if image == "" { return nil, errors.Errorf("image name must be specified") } - img, err := FindImage(store, image) + img, err := r.GetImage(image) if err != nil { return nil, errors.Wrapf(err, "error locating image %q for importing settings", image) } systemContext := common.GetSystemContext("") - data, err := ImportCopyDataFromImage(store, systemContext, img.ID, "", "") + data, err := r.ImportCopyDataFromImage(systemContext, img.ID, "", "") if err != nil { return nil, errors.Wrapf(err, "error reading image") } @@ -401,7 +425,7 @@ func GetImageCopyData(store storage.Store, image string) (*CopyData, error) { } -func importCopyData(store storage.Store, container, signaturePolicyPath string) (*CopyData, error) { +func (r *Runtime) importCopyData(store storage.Store, container, signaturePolicyPath string) (*CopyData, error) { if container == "" { return nil, errors.Errorf("container name must be specified") } @@ -413,7 +437,7 @@ func importCopyData(store storage.Store, container, signaturePolicyPath string) systemContext := common.GetSystemContext(signaturePolicyPath) - data, err := ImportCopyDataFromImage(store, systemContext, c.ImageID, container, c.ID) + data, err := r.ImportCopyDataFromImage(systemContext, c.ImageID, container, c.ID) if err != nil { return nil, err } @@ -461,13 +485,13 @@ func openCopyData(store storage.Store, container string) (*CopyData, error) { } // ImportCopyDataFromImage creates copy data for an image with the given parameters -func ImportCopyDataFromImage(store storage.Store, systemContext *types.SystemContext, imageID, containerName, containerID string) (*CopyData, error) { +func (r *Runtime) ImportCopyDataFromImage(systemContext *types.SystemContext, imageID, containerName, containerID string) (*CopyData, error) { manifest := []byte{} config := []byte{} imageName := "" if imageID != "" { - ref, err := is.Transport.ParseStoreReference(store, "@"+imageID) + ref, err := is.Transport.ParseStoreReference(r.store, "@"+imageID) if err != nil { return nil, errors.Wrapf(err, "no such image %q", "@"+imageID) } @@ -484,7 +508,7 @@ func ImportCopyDataFromImage(store storage.Store, systemContext *types.SystemCon if err != nil { return nil, errors.Wrapf(err, "error reading image manifest") } - if img, err3 := store.Image(imageID); err3 == nil { + if img, err3 := r.store.Image(imageID); err3 == nil { if len(img.Names) > 0 { imageName = img.Names[0] } @@ -492,7 +516,7 @@ func ImportCopyDataFromImage(store storage.Store, systemContext *types.SystemCon } data := &CopyData{ - store: store, + store: r.store, Type: containerType, FromImage: imageName, FromImageID: imageID, @@ -550,3 +574,91 @@ func (c *CopyData) MakeImageRef(manifestType string, compress archive.Compressio } return ref, nil } + +// GetData gets the Data for a container with the given name in the given store. +func (r *Runtime) GetData(name string) (*Data, error) { + img, err := r.GetImage(name) + if err != nil { + return nil, errors.Wrapf(err, "error reading image %q", name) + } + + imgRef, err := r.GetImageRef("@" + img.ID) + if err != nil { + return nil, errors.Wrapf(err, "error reading image reference %q", img.ID) + } + defer imgRef.Close() + + tags, digests, err := ParseImageNames(img.Names) + if err != nil { + return nil, errors.Wrapf(err, "error parsing image names for %q", name) + } + + driverName, err := driver.GetDriverName(r.store) + if err != nil { + return nil, errors.Wrapf(err, "error reading name of storage driver") + } + + topLayerID := img.TopLayer + + driverMetadata, err := driver.GetDriverMetadata(r.store, topLayerID) + if err != nil { + return nil, errors.Wrapf(err, "error asking storage driver %q for metadata", driverName) + } + + layer, err := r.store.Layer(topLayerID) + if err != nil { + return nil, errors.Wrapf(err, "error reading information about layer %q", topLayerID) + } + size, err := r.store.DiffSize(layer.Parent, layer.ID) + if err != nil { + return nil, errors.Wrapf(err, "error determining size of layer %q", layer.ID) + } + + imgSize, err := imgRef.Size() + if err != nil { + return nil, errors.Wrapf(err, "error determining size of image %q", transports.ImageName(imgRef.Reference())) + } + + manifest, manifestType, err := imgRef.Manifest() + if err != nil { + return nil, errors.Wrapf(err, "error reading manifest for image %q", img.ID) + } + manifestDigest := digest.Digest("") + if len(manifest) > 0 { + manifestDigest = digest.Canonical.FromBytes(manifest) + } + annotations := annotations(manifest, manifestType) + + config, err := imgRef.OCIConfig() + if err != nil { + return nil, errors.Wrapf(err, "error reading image configuration for %q", img.ID) + } + historyComment := "" + historyCreatedBy := "" + if len(config.History) > 0 { + historyComment = config.History[len(config.History)-1].Comment + historyCreatedBy = config.History[len(config.History)-1].CreatedBy + } + + return &Data{ + ID: img.ID, + Tags: tags, + Digests: digests, + ManifestDigest: manifestDigest, + Comment: historyComment, + Created: config.Created, + Author: config.Author, + Config: config.Config, + Architecture: config.Architecture, + OS: config.OS, + Annotations: annotations, + CreatedBy: historyCreatedBy, + Size: uint(size), + VirtualSize: uint(size + imgSize), + GraphDriver: driver.Data{ + Name: driverName, + Data: driverMetadata, + }, + RootFS: config.RootFS, + }, nil +} diff --git a/libpod/images/copy_ref.go b/libpod/copy_ref.go similarity index 99% rename from libpod/images/copy_ref.go rename to libpod/copy_ref.go index a65d0050..5c70817c 100644 --- a/libpod/images/copy_ref.go +++ b/libpod/copy_ref.go @@ -1,4 +1,4 @@ -package images +package libpod import ( "bytes" diff --git a/libkpod/diff.go b/libpod/diff.go similarity index 60% rename from libkpod/diff.go rename to libpod/diff.go index 281592c8..055bb7fe 100644 --- a/libkpod/diff.go +++ b/libpod/diff.go @@ -1,38 +1,37 @@ -package libkpod +package libpod import ( "github.com/containers/storage/pkg/archive" - "github.com/kubernetes-incubator/cri-o/libpod/images" "github.com/kubernetes-incubator/cri-o/libpod/layers" "github.com/pkg/errors" ) // GetDiff returns the differences between the two images, layers, or containers -func (c *ContainerServer) GetDiff(from, to string) ([]archive.Change, error) { - toLayer, err := c.getLayerID(to) +func (r *Runtime) GetDiff(from, to string) ([]archive.Change, error) { + toLayer, err := r.getLayerID(to) if err != nil { return nil, err } fromLayer := "" if from != "" { - fromLayer, err = c.getLayerID(from) + fromLayer, err = r.getLayerID(from) if err != nil { return nil, err } } - return c.Store().Changes(fromLayer, toLayer) + return r.store.Changes(fromLayer, toLayer) } // GetLayerID gets a full layer id given a full or partial id // If the id matches a container or image, the id of the top layer is returned // If the id matches a layer, the top layer id is returned -func (c *ContainerServer) getLayerID(id string) (string, error) { +func (r *Runtime) getLayerID(id string) (string, error) { var toLayer string - toImage, err := images.FindImage(c.store, id) + toImage, err := r.GetImage(id) if err != nil { - toCtr, err := c.store.Container(id) + toCtr, err := r.store.Container(id) if err != nil { - toLayer, err = layers.FullID(c.store, id) + toLayer, err = layers.FullID(r.store, id) if err != nil { return "", errors.Errorf("layer, image, or container %s does not exist", id) } @@ -45,8 +44,8 @@ func (c *ContainerServer) getLayerID(id string) (string, error) { return toLayer, nil } -func (c *ContainerServer) getLayerParent(layerID string) (string, error) { - layer, err := c.store.Layer(layerID) +func (r *Runtime) getLayerParent(layerID string) (string, error) { + layer, err := r.store.Layer(layerID) if err != nil { return "", err } diff --git a/libpod/images/copy.go b/libpod/images/copy.go deleted file mode 100644 index 255c1000..00000000 --- a/libpod/images/copy.go +++ /dev/null @@ -1,208 +0,0 @@ -package images - -import ( - "io" - "os" - "strings" - "syscall" - - cp "github.com/containers/image/copy" - dockerarchive "github.com/containers/image/docker/archive" - "github.com/containers/image/docker/tarfile" - "github.com/containers/image/manifest" - ociarchive "github.com/containers/image/oci/archive" - "github.com/containers/image/signature" - is "github.com/containers/image/storage" - "github.com/containers/image/transports/alltransports" - "github.com/containers/storage" - "github.com/containers/storage/pkg/archive" - "github.com/kubernetes-incubator/cri-o/libpod/common" - "github.com/pkg/errors" -) - -const ( - // DefaultRegistry is a prefix that we apply to an image name - // to check docker hub first for the image - DefaultRegistry = "docker://" -) - -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() -) - -// CopyOptions contains the options given when pushing or pulling images -type CopyOptions struct { - // Compression specifies the type of compression which is applied to - // layer blobs. The default is to not use compression, but - // archive.Gzip is recommended. - Compression archive.Compression - // SignaturePolicyPath specifies an override location for the signature - // policy which should be used for verifying the new image as it is - // being written. Except in specific circumstances, no value should be - // specified, indicating that the shared, system-wide default policy - // should be used. - SignaturePolicyPath string - // ReportWriter is an io.Writer which will be used to log the writing - // of the new image. - ReportWriter io.Writer - // Store is the local storage store which holds the source image. - Store storage.Store - // DockerRegistryOptions encapsulates settings that affect how we - // connect or authenticate to a remote registry to which we want to - // push the image. - common.DockerRegistryOptions - // SigningOptions encapsulates settings that control whether or not we - // strip or add signatures to the image when pushing (uploading) the - // image to a registry. - common.SigningOptions - // Quiet suppresses the output when a push or pull happens - Quiet bool -} - -// PushImage pushes the src image to the destination -func PushImage(srcName, destName string, options CopyOptions) error { - if srcName == "" || destName == "" { - return errors.Wrapf(syscall.EINVAL, "source and destination image names must be specified") - } - - // Get the destination Image Reference - dest, err := alltransports.ParseImageName(destName) - if err != nil { - return errors.Wrapf(err, "error getting destination imageReference for %q", destName) - } - - policyContext, err := common.GetPolicyContext(options.SignaturePolicyPath) - if err != nil { - return errors.Wrapf(err, "Could not get default policy context for signature policy path %q", options.SignaturePolicyPath) - } - defer policyContext.Destroy() - // Look up the image name and its layer, then build the imagePushData from - // the image - img, err := FindImage(options.Store, srcName) - if err != nil { - return errors.Wrapf(err, "error locating image %q for importing settings", srcName) - } - systemContext := common.GetSystemContext(options.SignaturePolicyPath) - cd, err := ImportCopyDataFromImage(options.Store, systemContext, img.ID, "", "") - if err != nil { - return err - } - // Give the image we're producing the same ancestors as its source image - cd.FromImage = cd.Docker.ContainerConfig.Image - cd.FromImageID = string(cd.Docker.Parent) - - // Prep the layers and manifest for export - src, err := cd.MakeImageRef(manifest.GuessMIMEType(cd.Manifest), options.Compression, img.Names, img.TopLayer, nil) - if err != nil { - return errors.Wrapf(err, "error copying layers and metadata") - } - - copyOptions := common.GetCopyOptions(options.ReportWriter, options.SignaturePolicyPath, nil, &options.DockerRegistryOptions, options.SigningOptions) - - // 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 -} - -// PullImage copies the image from the source to the destination -func PullImage(imgName string, allTags bool, options CopyOptions) error { - var ( - images []string - output io.Writer - ) - store := options.Store - sc := common.GetSystemContext(options.SignaturePolicyPath) - - if options.Quiet { - output = nil - } else { - output = os.Stdout - } - - srcRef, err := alltransports.ParseImageName(imgName) - if err != nil { - defaultName := DefaultRegistry + imgName - srcRef2, err2 := alltransports.ParseImageName(defaultName) - if err2 != nil { - return errors.Errorf("error parsing image name %q: %v", defaultName, err2) - } - srcRef = srcRef2 - } - - splitArr := strings.Split(imgName, ":") - archFile := splitArr[len(splitArr)-1] - - // supports pulling from docker-archive, oci, and registries - if srcRef.Transport().Name() == DockerArchive { - tarSource := tarfile.NewSource(archFile) - manifest, err := tarSource.LoadTarManifest() - if err != nil { - return errors.Errorf("error retrieving manifest.json: %v", err) - } - // to pull all the images stored in one tar file - for i := range manifest { - if manifest[i].RepoTags != nil { - images = append(images, manifest[i].RepoTags[0]) - } else { - // create an image object and use the hex value of the digest as the image ID - // for parsing the store reference - newImg, err := srcRef.NewImage(sc) - if err != nil { - return err - } - defer newImg.Close() - digest := newImg.ConfigInfo().Digest - if err := digest.Validate(); err == nil { - images = append(images, "@"+digest.Hex()) - } else { - return errors.Wrapf(err, "error getting config info") - } - } - } - } 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 errors.Wrapf(err, "error loading manifest for %q", srcRef) - } - - if manifest.Annotations == nil || manifest.Annotations["org.opencontainers.image.ref.name"] == "" { - return errors.Errorf("error, archive doesn't have a name annotation. Cannot store image with no name") - } - images = append(images, manifest.Annotations["org.opencontainers.image.ref.name"]) - } else { - images = append(images, imgName) - } - - policy, err := signature.DefaultPolicy(sc) - if err != nil { - return err - } - - policyContext, err := signature.NewPolicyContext(policy) - if err != nil { - return err - } - defer policyContext.Destroy() - - copyOptions := common.GetCopyOptions(output, "", nil, nil, common.SigningOptions{}) - - for _, image := range images { - destRef, err := is.Transport.ParseStoreReference(store, image) - if err != nil { - return errors.Errorf("error parsing dest reference name: %v", err) - } - if err = cp.Image(policyContext, destRef, srcRef, copyOptions); err != nil { - return errors.Errorf("error loading image %q: %v", image, err) - } - } - return nil -} diff --git a/libpod/images/image.go b/libpod/images/image.go deleted file mode 100644 index 191b4392..00000000 --- a/libpod/images/image.go +++ /dev/null @@ -1,288 +0,0 @@ -package images - -import ( - "fmt" - "strings" - "time" - - is "github.com/containers/image/storage" - "github.com/containers/image/transports" - "github.com/containers/image/types" - "github.com/containers/storage" - "github.com/kubernetes-incubator/cri-o/libpod/common" - digest "github.com/opencontainers/go-digest" - "github.com/pkg/errors" -) - -// FilterParams contains the filter options that may be given when outputting images -type FilterParams struct { - dangling string - label string - beforeImage time.Time - sinceImage time.Time - referencePattern string -} - -// ParseFilter takes a set of images and a filter string as input, and returns the -func ParseFilter(store storage.Store, filter string) (*FilterParams, error) { - images, err := store.Images() - if err != nil { - return nil, err - } - params := new(FilterParams) - filterStrings := strings.Split(filter, ",") - for _, param := range filterStrings { - pair := strings.SplitN(param, "=", 2) - switch strings.TrimSpace(pair[0]) { - case "dangling": - if common.IsValidBool(pair[1]) { - params.dangling = pair[1] - } else { - return nil, fmt.Errorf("invalid filter: '%s=[%s]'", pair[0], pair[1]) - } - case "label": - params.label = pair[1] - case "before": - if img, err := findImageInSlice(images, pair[1]); err == nil { - info, err := getImageInspectInfo(store, img) - if err != nil { - return nil, err - } - params.beforeImage = info.Created - } else { - return nil, fmt.Errorf("no such id: %s", pair[0]) - } - case "since": - if img, err := findImageInSlice(images, pair[1]); err == nil { - info, err := getImageInspectInfo(store, img) - if err != nil { - return nil, err - } - params.sinceImage = info.Created - } else { - return nil, fmt.Errorf("no such id: %s``", pair[0]) - } - case "reference": - params.referencePattern = pair[1] - default: - return nil, fmt.Errorf("invalid filter: '%s'", pair[0]) - } - } - return params, nil -} - -func matchesFilter(store storage.Store, image storage.Image, name string, params *FilterParams) bool { - if params == nil { - return true - } - - info, err := getImageInspectInfo(store, image) - if err != nil { - return false - } - if params.dangling != "" && !matchesDangling(name, params.dangling) { - return false - } else if params.label != "" && !matchesLabel(info, store, params.label) { - return false - } else if !params.beforeImage.IsZero() && !matchesBeforeImage(info, name, params) { - return false - } else if !params.sinceImage.IsZero() && !matchesSinceImage(info, name, params) { - return false - } else if params.referencePattern != "" && !MatchesReference(name, params.referencePattern) { - return false - } - return true -} - -func matchesDangling(name string, dangling string) bool { - if common.IsFalse(dangling) && name != "" { - return true - } else if common.IsTrue(dangling) && name == "" { - return true - } - return false -} - -func matchesLabel(info *types.ImageInspectInfo, store storage.Store, label string) bool { - pair := strings.SplitN(label, "=", 2) - for key, value := range info.Labels { - if key == pair[0] { - if len(pair) == 2 { - if value == pair[1] { - return true - } - } else { - return false - } - } - } - return false -} - -// Returns true if the image was created since the filter image. Returns -// false otherwise -func matchesBeforeImage(info *types.ImageInspectInfo, name string, params *FilterParams) bool { - return info.Created.Before(params.beforeImage) -} - -// Returns true if the image was created since the filter image. Returns -// false otherwise -func matchesSinceImage(info *types.ImageInspectInfo, name string, params *FilterParams) bool { - return info.Created.After(params.sinceImage) -} - -// MatchesID returns true if argID is a full or partial match for id -func MatchesID(id, argID string) bool { - return strings.HasPrefix(argID, id) -} - -// MatchesReference returns true if argName is a full or partial match for name -// Partial matches will register only if they match the most specific part of the name available -// For example, take the image docker.io/library/redis:latest -// redis, library,redis, docker.io/library/redis, redis:latest, etc. will match -// But redis:alpine, ry/redis, library, and io/library/redis will not -func MatchesReference(name, argName string) bool { - if argName == "" { - return false - } - splitName := strings.Split(name, ":") - // If the arg contains a tag, we handle it differently than if it does not - if strings.Contains(argName, ":") { - splitArg := strings.Split(argName, ":") - return strings.HasSuffix(splitName[0], splitArg[0]) && (splitName[1] == splitArg[1]) - } - return strings.HasSuffix(splitName[0], argName) -} - -// FormattedSize returns a human-readable formatted size for the image -func FormattedSize(size float64) string { - suffixes := [5]string{"B", "KB", "MB", "GB", "TB"} - - count := 0 - for size >= 1024 && count < 4 { - size /= 1024 - count++ - } - return fmt.Sprintf("%.4g %s", size, suffixes[count]) -} - -// FindImage searches for a *storage.Image with a matching the given name or ID in the given store. -func FindImage(store storage.Store, image string) (*storage.Image, error) { - var img *storage.Image - ref, err := is.Transport.ParseStoreReference(store, image) - if err == nil { - img, err = is.Transport.GetStoreImage(store, ref) - } - if err != nil { - img2, err2 := 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 - } - return img, nil -} - -// FindImageRef searches for and returns a new types.Image matching the given name or ID in the given store. -func FindImageRef(store storage.Store, image string) (types.Image, error) { - img, err := FindImage(store, image) - if err != nil { - return nil, errors.Wrapf(err, "unable to locate image %q", image) - } - ref, err := is.Transport.ParseStoreReference(store, "@"+img.ID) - if err != nil { - return nil, errors.Wrapf(err, "error parsing reference to image %q", img.ID) - } - imgRef, err := ref.NewImage(nil) - if err != nil { - return nil, errors.Wrapf(err, "error reading image %q", img.ID) - } - return imgRef, nil -} - -func findImageInSlice(images []storage.Image, ref string) (storage.Image, error) { - for _, image := range images { - if MatchesID(image.ID, ref) { - return image, nil - } - for _, name := range image.Names { - if MatchesReference(name, ref) { - return image, nil - } - } - } - return storage.Image{}, errors.New("could not find image") -} - -// InfoAndDigestAndSize returns the inspection info and size of the image in the given -// store and the digest of its manifest, if it has one, or "" if it doesn't. -func InfoAndDigestAndSize(store storage.Store, img storage.Image) (*types.ImageInspectInfo, digest.Digest, int64, error) { - imgRef, err := FindImageRef(store, "@"+img.ID) - if err != nil { - return nil, "", -1, errors.Wrapf(err, "error reading image %q", img.ID) - } - defer imgRef.Close() - return infoAndDigestAndSize(imgRef) -} - -func infoAndDigestAndSize(imgRef types.Image) (*types.ImageInspectInfo, digest.Digest, int64, error) { - imgSize, err := imgRef.Size() - if err != nil { - return nil, "", -1, errors.Wrapf(err, "error reading size of image %q", transports.ImageName(imgRef.Reference())) - } - manifest, _, err := imgRef.Manifest() - if err != nil { - return nil, "", -1, errors.Wrapf(err, "error reading manifest for image %q", transports.ImageName(imgRef.Reference())) - } - manifestDigest := digest.Digest("") - if len(manifest) > 0 { - manifestDigest = digest.Canonical.FromBytes(manifest) - } - info, err := imgRef.Inspect() - if err != nil { - return nil, "", -1, errors.Wrapf(err, "error inspecting image %q", transports.ImageName(imgRef.Reference())) - } - return info, manifestDigest, imgSize, nil -} - -// GetImagesMatchingFilter returns a slice of all images in the store that match the provided FilterParams. -// Images with more than one name matching the filter will be in the slice once for each name -func GetImagesMatchingFilter(store storage.Store, filter *FilterParams, argName string) ([]storage.Image, error) { - images, err := store.Images() - filteredImages := []storage.Image{} - if err != nil { - return nil, err - } - for _, image := range images { - names := []string{} - if len(image.Names) > 0 { - names = image.Names - } else { - names = append(names, "") - } - for _, name := range names { - if (filter == nil && argName == "") || (filter != nil && matchesFilter(store, image, name, filter)) || MatchesReference(name, argName) { - newImage := image - newImage.Names = []string{name} - filteredImages = append(filteredImages, newImage) - } - } - } - return filteredImages, nil -} - -func getImageInspectInfo(store storage.Store, image storage.Image) (*types.ImageInspectInfo, error) { - storeRef, err := is.Transport.ParseStoreReference(store, "@"+image.ID) - if err != nil { - return nil, err - } - img, err := storeRef.NewImage(nil) - if err != nil { - return nil, err - } - defer img.Close() - return img.Inspect() -} diff --git a/libpod/images/image_data.go b/libpod/images/image_data.go index f187bc4d..12fb1e77 100644 --- a/libpod/images/image_data.go +++ b/libpod/images/image_data.go @@ -5,7 +5,9 @@ import ( "time" "github.com/containers/image/docker/reference" + is "github.com/containers/image/storage" "github.com/containers/image/transports" + "github.com/containers/image/types" "github.com/containers/storage" "github.com/kubernetes-incubator/cri-o/libpod/driver" digest "github.com/opencontainers/go-digest" @@ -162,3 +164,40 @@ func GetData(store storage.Store, name string) (*Data, error) { RootFS: config.RootFS, }, nil } + +// FindImage searches for a *storage.Image with a matching the given name or ID in the given store. +func FindImage(store storage.Store, image string) (*storage.Image, error) { + var img *storage.Image + ref, err := is.Transport.ParseStoreReference(store, image) + if err == nil { + img, err = is.Transport.GetStoreImage(store, ref) + } + if err != nil { + img2, err2 := 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 + } + return img, nil +} + +// FindImageRef searches for and returns a new types.Image matching the given name or ID in the given store. +func FindImageRef(store storage.Store, image string) (types.Image, error) { + img, err := FindImage(store, image) + if err != nil { + return nil, errors.Wrapf(err, "unable to locate image %q", image) + } + ref, err := is.Transport.ParseStoreReference(store, "@"+img.ID) + if err != nil { + return nil, errors.Wrapf(err, "error parsing reference to image %q", img.ID) + } + imgRef, err := ref.NewImage(nil) + if err != nil { + return nil, errors.Wrapf(err, "error reading image %q", img.ID) + } + return imgRef, nil +} diff --git a/libpod/images/rmi.go b/libpod/images/rmi.go deleted file mode 100644 index 8593a868..00000000 --- a/libpod/images/rmi.go +++ /dev/null @@ -1,35 +0,0 @@ -package images - -import ( - "github.com/containers/storage" - "github.com/pkg/errors" -) - -// UntagImage removes the tag from the given image -func UntagImage(store storage.Store, image *storage.Image, imgArg string) (string, error) { - // Remove name from image.Names and set the new names - newNames := []string{} - removedName := "" - for _, name := range image.Names { - if MatchesReference(name, imgArg) || MatchesID(imgArg, image.ID) { - removedName = name - continue - } - newNames = append(newNames, name) - } - if removedName != "" { - if err := store.SetNames(image.ID, newNames); err != nil { - return "", errors.Wrapf(err, "error removing name %q from image %q", removedName, image.ID) - } - } - return removedName, nil -} - -// RemoveImage removes the given image from storage -func RemoveImage(image *storage.Image, store storage.Store) (string, error) { - _, err := store.DeleteImage(image.ID, true) - if err != nil { - return "", errors.Wrapf(err, "could not remove image %q", image.ID) - } - return image.ID, nil -} diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index cdd32b10..4b506233 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -1,6 +1,7 @@ package libpod import ( + "github.com/containers/storage" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" ) @@ -99,11 +100,11 @@ func (r *Runtime) LookupContainer(idOrName string) (*Container, error) { return r.state.LookupContainer(idOrName) } -// Containers retrieves all containers from the state +// GetContainers retrieves all containers from the state // Filters can be provided which will determine what containers are included in // the output. Multiple filters are handled by ANDing their output, so only // containers matching all filters are returned -func (r *Runtime) Containers(filters ...ContainerFilter) ([]*Container, error) { +func (r *Runtime) GetContainers(filters ...ContainerFilter) ([]*Container, error) { r.lock.RLock() defer r.lock.RUnlock() @@ -131,3 +132,29 @@ func (r *Runtime) Containers(filters ...ContainerFilter) ([]*Container, error) { return ctrsFiltered, nil } + +// getContainersWithImage returns a list of containers referencing imageID +func (r *Runtime) getContainersWithImage(imageID string) ([]storage.Container, error) { + var matchingContainers []storage.Container + containers, err := r.store.Containers() + if err != nil { + return nil, err + } + + for _, ctr := range containers { + if ctr.ImageID == imageID { + matchingContainers = append(matchingContainers, ctr) + } + } + return matchingContainers, nil +} + +// removeMultipleContainers deletes a list of containers from the store +func (r *Runtime) removeMultipleContainers(containers []storage.Container) error { + for _, ctr := range containers { + if err := r.store.DeleteContainer(ctr.ID); err != nil { + return errors.Wrapf(err, "could not remove container %q", ctr) + } + } + return nil +} diff --git a/libpod/image.go b/libpod/runtime_img.go similarity index 51% rename from libpod/image.go rename to libpod/runtime_img.go index 61b56254..2d0e5b9f 100644 --- a/libpod/image.go +++ b/libpod/runtime_img.go @@ -1,24 +1,29 @@ package libpod import ( + "encoding/json" "fmt" "io" "strings" "syscall" + "time" cp "github.com/containers/image/copy" dockerarchive "github.com/containers/image/docker/archive" + "github.com/containers/image/docker/reference" "github.com/containers/image/docker/tarfile" "github.com/containers/image/manifest" ociarchive "github.com/containers/image/oci/archive" "github.com/containers/image/signature" is "github.com/containers/image/storage" + "github.com/containers/image/transports" "github.com/containers/image/transports/alltransports" "github.com/containers/image/types" "github.com/containers/storage" "github.com/containers/storage/pkg/archive" "github.com/kubernetes-incubator/cri-o/libpod/common" - "github.com/kubernetes-incubator/cri-o/libpod/images" + digest "github.com/opencontainers/go-digest" + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) @@ -60,10 +65,21 @@ type CopyOptions struct { // Image API +// ImageFilterParams contains the filter options that may be given when outputting images +type ImageFilterParams struct { + Dangling string + Label string + BeforeImage time.Time + SinceImage time.Time + ReferencePattern string + ImageName string + ImageInput string +} + // ImageFilter is a function to determine whether an image is included in // command output. Images to be outputted are tested using the function. A true // return will include the image, a false return will exclude it. -type ImageFilter func(*storage.Image) bool +type ImageFilter func(*storage.Image, *types.ImageInspectInfo) bool // PullImage pulls an image from configured registries // By default, only the latest tag (or a specific tag if requested) will be @@ -75,7 +91,7 @@ func (r *Runtime) PullImage(imgName string, allTags bool, signaturePolicyPath st defer r.lock.Unlock() if !r.valid { - return fmt.Errorf("runtime is not valid") + return ErrRuntimeStopped } // PullImage copies the image from the source to the destination @@ -178,7 +194,7 @@ func (r *Runtime) PushImage(source string, destination string, options CopyOptio defer r.lock.Unlock() if !r.valid { - return fmt.Errorf("runtime is not valid") + return ErrRuntimeStopped } // PushImage pushes the src image to the destination @@ -209,7 +225,7 @@ func (r *Runtime) PushImage(source string, destination string, options CopyOptio if err != nil { return errors.Wrapf(err, "error locating image %q for importing settings", source) } - cd, err := images.ImportCopyDataFromImage(r.store, r.imageContext, img.ID, "", "") + cd, err := r.ImportCopyDataFromImage(r.imageContext, img.ID, "", "") if err != nil { return err } @@ -239,7 +255,7 @@ func (r *Runtime) TagImage(image *storage.Image, tag string) error { defer r.lock.Unlock() if !r.valid { - return fmt.Errorf("runtime is not valid") + return ErrRuntimeStopped } tags, err := r.store.Names(image.ID) @@ -256,17 +272,17 @@ func (r *Runtime) TagImage(image *storage.Image, tag string) error { } // UntagImage removes a tag from the given image -func (r *Runtime) UntagImage(image *storage.Image, tag string) error { +func (r *Runtime) UntagImage(image *storage.Image, tag string) (string, error) { r.lock.Lock() defer r.lock.Unlock() if !r.valid { - return fmt.Errorf("runtime is not valid") + return "", ErrRuntimeStopped } tags, err := r.store.Names(image.ID) if err != nil { - return err + return "", err } for i, key := range tags { if key == tag { @@ -275,21 +291,49 @@ func (r *Runtime) UntagImage(image *storage.Image, tag string) error { break } } - return r.store.SetNames(image.ID, tags) + if err = r.store.SetNames(image.ID, tags); err != nil { + return "", err + } + return tag, nil } // RemoveImage deletes an image from local storage -// Images being used by running containers cannot be removed -func (r *Runtime) RemoveImage(image *storage.Image) error { +// Images being used by running containers can only be removed if force=true +func (r *Runtime) RemoveImage(image *storage.Image, force bool) (string, error) { r.lock.Lock() defer r.lock.Unlock() if !r.valid { - return fmt.Errorf("runtime is not valid") + return "", ErrRuntimeStopped } - _, err := r.store.DeleteImage(image.ID, false) - return err + containersWithImage, err := r.getContainersWithImage(image.ID) + if err != nil { + return "", errors.Wrapf(err, "error getting containers for image %q", image.ID) + } + if len(containersWithImage) > 0 && len(image.Names) <= 1 { + if force { + if err := r.removeMultipleContainers(containersWithImage); err != nil { + return "", err + } + } else { + for _, ctr := range containersWithImage { + return "", fmt.Errorf("Could not remove image %q (must force) - container %q is using its reference image", image.ID, ctr.ImageID) + } + } + } + + if len(image.Names) > 1 && !force { + return "", fmt.Errorf("unable to delete %s (must force) - image is referred to in multiple tags", image.ID) + } + // If it is forced, we have to untag the image so that it can be deleted + image.Names = image.Names[:0] + + _, err = r.store.DeleteImage(image.ID, true) + if err != nil { + return "", err + } + return image.ID, nil } // GetImage retrieves an image matching the given name or hash from system @@ -300,7 +344,7 @@ func (r *Runtime) GetImage(image string) (*storage.Image, error) { defer r.lock.Unlock() if !r.valid { - return nil, fmt.Errorf("runtime is not valid") + return nil, ErrRuntimeStopped } return r.getImage(image) } @@ -330,9 +374,13 @@ func (r *Runtime) GetImageRef(image string) (types.Image, error) { defer r.lock.Unlock() if !r.valid { - return nil, fmt.Errorf("runtime is not valid") + return nil, ErrRuntimeStopped } + return r.getImageRef(image) +} + +func (r *Runtime) getImageRef(image string) (types.Image, error) { img, err := r.getImage(image) if err != nil { return nil, errors.Wrapf(err, "unable to locate image %q", image) @@ -352,11 +400,280 @@ func (r *Runtime) GetImageRef(image string) (types.Image, error) { // Filters can be provided which will determine which images are included in the // output. Multiple filters are handled by ANDing their output, so only images // matching all filters are included -func (r *Runtime) GetImages(filter ...ImageFilter) ([]*storage.Image, error) { - return nil, ErrNotImplemented +func (r *Runtime) GetImages(params *ImageFilterParams, filters ...ImageFilter) ([]*storage.Image, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, ErrRuntimeStopped + } + + images, err := r.store.Images() + if err != nil { + return nil, err + } + + var imagesFiltered []*storage.Image + + for _, img := range images { + info, err := r.getImageInspectInfo(img) + if err != nil { + return nil, err + } + var names []string + if len(img.Names) > 0 { + names = img.Names + } else { + names = append(names, "") + } + for _, name := range names { + include := true + if params != nil { + params.ImageName = name + } + for _, filter := range filters { + include = include && filter(&img, info) + } + + if include { + newImage := img + newImage.Names = []string{name} + imagesFiltered = append(imagesFiltered, &newImage) + } + } + } + + return imagesFiltered, nil +} + +// GetHistory gets the history of an image and information about its layers +func (r *Runtime) GetHistory(image string) ([]ociv1.History, []types.BlobInfo, string, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, nil, "", ErrRuntimeStopped + } + + img, err := r.getImage(image) + if err != nil { + return nil, nil, "", errors.Wrapf(err, "no such image %q", image) + } + + src, err := r.getImageRef(image) + if err != nil { + return nil, nil, "", errors.Wrapf(err, "error instantiating image %q", image) + } + + oci, err := src.OCIConfig() + if err != nil { + return nil, nil, "", err + } + + return oci.History, src.LayerInfos(), img.ID, nil } // ImportImage imports an OCI format image archive into storage as an image func (r *Runtime) ImportImage(path string) (*storage.Image, error) { return nil, ErrNotImplemented } + +// GetImageInspectInfo returns the inspect information of an image +func (r *Runtime) GetImageInspectInfo(image storage.Image) (*types.ImageInspectInfo, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, ErrRuntimeStopped + } + return r.getImageInspectInfo(image) +} + +func (r *Runtime) getImageInspectInfo(image storage.Image) (*types.ImageInspectInfo, error) { + img, err := r.getImageRef(image.ID) + if err != nil { + return nil, err + } + return img.Inspect() +} + +// ParseImageFilter takes a set of images and a filter string as input, and returns the libpod.ImageFilterParams struct +func (r *Runtime) ParseImageFilter(imageInput, filter string) (*ImageFilterParams, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, ErrRuntimeStopped + } + + if filter == "" && imageInput == "" { + return nil, nil + } + + var params ImageFilterParams + params.ImageInput = imageInput + + if filter == "" && imageInput != "" { + return ¶ms, nil + } + + images, err := r.store.Images() + if err != nil { + return nil, err + } + + filterStrings := strings.Split(filter, ",") + for _, param := range filterStrings { + pair := strings.SplitN(param, "=", 2) + switch strings.TrimSpace(pair[0]) { + case "dangling": + if common.IsValidBool(pair[1]) { + params.Dangling = pair[1] + } else { + return nil, fmt.Errorf("invalid filter: '%s=[%s]'", pair[0], pair[1]) + } + case "label": + params.Label = pair[1] + case "before": + if img, err := findImageInSlice(images, pair[1]); err == nil { + info, err := r.GetImageInspectInfo(img) + if err != nil { + return nil, err + } + params.BeforeImage = info.Created + } else { + return nil, fmt.Errorf("no such id: %s", pair[0]) + } + case "since": + if img, err := findImageInSlice(images, pair[1]); err == nil { + info, err := r.GetImageInspectInfo(img) + if err != nil { + return nil, err + } + params.SinceImage = info.Created + } else { + return nil, fmt.Errorf("no such id: %s``", pair[0]) + } + case "reference": + params.ReferencePattern = pair[1] + default: + return nil, fmt.Errorf("invalid filter: '%s'", pair[0]) + } + } + return ¶ms, nil +} + +// InfoAndDigestAndSize returns the inspection info and size of the image in the given +// store and the digest of its manifest, if it has one, or "" if it doesn't. +func (r *Runtime) InfoAndDigestAndSize(img storage.Image) (*types.ImageInspectInfo, digest.Digest, int64, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, "", -1, ErrRuntimeStopped + } + + imgRef, err := r.getImageRef("@" + img.ID) + if err != nil { + return nil, "", -1, errors.Wrapf(err, "error reading image %q", img.ID) + } + defer imgRef.Close() + return infoAndDigestAndSize(imgRef) +} + +func infoAndDigestAndSize(imgRef types.Image) (*types.ImageInspectInfo, digest.Digest, int64, error) { + imgSize, err := imgRef.Size() + if err != nil { + return nil, "", -1, errors.Wrapf(err, "error reading size of image %q", transports.ImageName(imgRef.Reference())) + } + manifest, _, err := imgRef.Manifest() + if err != nil { + return nil, "", -1, errors.Wrapf(err, "error reading manifest for image %q", transports.ImageName(imgRef.Reference())) + } + manifestDigest := digest.Digest("") + if len(manifest) > 0 { + manifestDigest = digest.Canonical.FromBytes(manifest) + } + info, err := imgRef.Inspect() + if err != nil { + return nil, "", -1, errors.Wrapf(err, "error inspecting image %q", transports.ImageName(imgRef.Reference())) + } + return info, manifestDigest, imgSize, nil +} + +// MatchesID returns true if argID is a full or partial match for id +func MatchesID(id, argID string) bool { + return strings.HasPrefix(argID, id) +} + +// MatchesReference returns true if argName is a full or partial match for name +// Partial matches will register only if they match the most specific part of the name available +// For example, take the image docker.io/library/redis:latest +// redis, library/redis, docker.io/library/redis, redis:latest, etc. will match +// But redis:alpine, ry/redis, library, and io/library/redis will not +func MatchesReference(name, argName string) bool { + if argName == "" { + return false + } + splitName := strings.Split(name, ":") + // If the arg contains a tag, we handle it differently than if it does not + if strings.Contains(argName, ":") { + splitArg := strings.Split(argName, ":") + return strings.HasSuffix(splitName[0], splitArg[0]) && (splitName[1] == splitArg[1]) + } + return strings.HasSuffix(splitName[0], argName) +} + +// ParseImageNames parses the names we've stored with an image into a list of +// tagged references and a list of references which contain digests. +func ParseImageNames(names []string) (tags, digests []string, err error) { + for _, name := range names { + if named, err := reference.ParseNamed(name); err == nil { + if digested, ok := named.(reference.Digested); ok { + canonical, err := reference.WithDigest(named, digested.Digest()) + if err == nil { + digests = append(digests, canonical.String()) + } + } else { + if reference.IsNameOnly(named) { + named = reference.TagNameOnly(named) + } + if tagged, ok := named.(reference.Tagged); ok { + namedTagged, err := reference.WithTag(named, tagged.Tag()) + if err == nil { + tags = append(tags, namedTagged.String()) + } + } + } + } + } + return tags, digests, nil +} + +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 +} + +func findImageInSlice(images []storage.Image, ref string) (storage.Image, error) { + for _, image := range images { + if MatchesID(image.ID, ref) { + return image, nil + } + for _, name := range image.Names { + if MatchesReference(name, ref) { + return image, nil + } + } + } + return storage.Image{}, errors.New("could not find image") +} diff --git a/test/kpod_inspect.bats b/test/kpod_inspect.bats index e587735e..ca4b7c8e 100644 --- a/test/kpod_inspect.bats +++ b/test/kpod_inspect.bats @@ -35,7 +35,7 @@ function teardown() { echo "$output" [ "$status" -eq 0 ] inspectOutput="$output" - run ${KPOD_BINARY} $KPOD_OPTIONS images --quiet ${IMAGE} + run ${KPOD_BINARY} $KPOD_OPTIONS images --no-trunc --quiet ${IMAGE} echo "$output" [ "$status" -eq 0 ] [ "$output" = "$inspectOutput" ] diff --git a/test/kpod_tag.bats b/test/kpod_tag.bats index 6c00456b..93109db5 100644 --- a/test/kpod_tag.bats +++ b/test/kpod_tag.bats @@ -2,7 +2,7 @@ load helpers -IMAGE="docker.io/library/alpine:latest" +IMAGE="alpine:latest" function teardown() { cleanup_test @@ -17,7 +17,7 @@ function teardown() { run ${KPOD_BINARY} ${KPOD_OPTIONS} inspect foobar:latest echo "$output" [ "$status" -eq 0 ] - run ${KPOD_BINARY} ${KPOD_OPTIONS} rmi foobar:latest + run ${KPOD_BINARY} ${KPOD_OPTIONS} rmi --force foobar:latest [ "$status" -eq 0 ] } @@ -26,10 +26,12 @@ function teardown() { echo "$output" [ "$status" -eq 0 ] run ${KPOD_BINARY} ${KPOD_OPTIONS} tag $IMAGE foobar + echo "$output" + [ "$status" -eq 0 ] run ${KPOD_BINARY} ${KPOD_OPTIONS} inspect foobar:latest echo "$output" [ "$status" -eq 0 ] - run ${KPOD_BINARY} ${KPOD_OPTIONS} rmi foobar:latest + run ${KPOD_BINARY} ${KPOD_OPTIONS} rmi --force foobar:latest [ "$status" -eq 0 ] } @@ -38,9 +40,11 @@ function teardown() { echo "$output" [ "$status" -eq 0 ] run ${KPOD_BINARY} ${KPOD_OPTIONS} tag $IMAGE foobar:v + echo "$output" + [ "$status" -eq 0 ] run ${KPOD_BINARY} ${KPOD_OPTIONS} inspect foobar:v echo "$output" [ "$status" -eq 0 ] - run ${KPOD_BINARY} ${KPOD_OPTIONS} rmi foobar:v + run ${KPOD_BINARY} ${KPOD_OPTIONS} rmi --force foobar:v [ "$status" -eq 0 ] }