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