diff --git a/cmd/kpod/common.go b/cmd/kpod/common.go index 75eebc18..894c78fd 100644 --- a/cmd/kpod/common.go +++ b/cmd/kpod/common.go @@ -1,56 +1,11 @@ package main import ( - "encoding/json" - "io" - "strings" - "time" - - cp "github.com/containers/image/copy" - "github.com/containers/image/signature" is "github.com/containers/image/storage" - "github.com/containers/image/types" "github.com/containers/storage" - "github.com/pkg/errors" "github.com/urfave/cli" ) -type imageMetadata struct { - Tag string `json:"tag"` - CreatedTime time.Time `json:"created-time"` - ID string `json:"id"` - Blobs []types.BlobInfo `json:"blob-list"` - Layers map[string][]string `json:"layers"` - SignatureSizes []string `json:"signature-sizes"` -} - -// DockerRegistryOptions encapsulates settings that affect how we connect or -// authenticate to a remote registry. -type dockerRegistryOptions struct { - // DockerRegistryCreds is the user name and password to supply in case - // we need to pull an image from a registry, and it requires us to - // authenticate. - DockerRegistryCreds *types.DockerAuthConfig - // DockerCertPath is the location of a directory containing CA - // certificates which will be used to verify the registry's certificate - // (all files with names ending in ".crt"), and possibly client - // certificates and private keys (pairs of files with the same name, - // except for ".cert" and ".key" suffixes). - DockerCertPath string - // DockerInsecureSkipTLSVerify turns off verification of TLS - // certificates and allows connecting to registries without encryption. - DockerInsecureSkipTLSVerify bool -} - -// SigningOptions encapsulates settings that control whether or not we strip or -// add signatures to images when writing them. -type signingOptions struct { - // RemoveSignatures directs us to remove any signatures which are already present. - RemoveSignatures bool - // SignBy is a key identifier of some kind, indicating that a signature should be generated using the specified private key and stored with the image. - SignBy string -} - func getStore(c *cli.Context) (storage.Store, error) { options := storage.DefaultStoreOptions if c.GlobalIsSet("root") { @@ -76,241 +31,3 @@ func getStore(c *cli.Context) (storage.Store, error) { is.Transport.SetStore(store) return store, nil } - -func parseMetadata(image storage.Image) (imageMetadata, error) { - var im imageMetadata - - dec := json.NewDecoder(strings.NewReader(image.Metadata)) - if err := dec.Decode(&im); err != nil { - return imageMetadata{}, err - } - return im, nil -} - -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 -} - -func getCopyOptions(reportWriter io.Writer, signaturePolicyPath string, srcDockerRegistry, destDockerRegistry *dockerRegistryOptions, signing signingOptions) *cp.Options { - if srcDockerRegistry == nil { - srcDockerRegistry = &dockerRegistryOptions{} - } - if destDockerRegistry == nil { - destDockerRegistry = &dockerRegistryOptions{} - } - srcContext := srcDockerRegistry.getSystemContext(signaturePolicyPath) - destContext := destDockerRegistry.getSystemContext(signaturePolicyPath) - return &cp.Options{ - RemoveSignatures: signing.RemoveSignatures, - SignBy: signing.SignBy, - ReportWriter: reportWriter, - SourceCtx: srcContext, - DestinationCtx: destContext, - } -} - -func findContainer(store storage.Store, container string) (*storage.Container, error) { - ctrStore, err := store.ContainerStore() - if err != nil { - return nil, err - } - return ctrStore.Get(container) -} - -func getContainerTopLayerID(store storage.Store, containerID string) (string, error) { - ctr, err := findContainer(store, containerID) - if err != nil { - return "", err - } - return ctr.LayerID, nil -} - -func getSystemContext(signaturePolicyPath string) *types.SystemContext { - sc := &types.SystemContext{} - if signaturePolicyPath != "" { - sc.SignaturePolicyPath = signaturePolicyPath - } - return sc -} - -func copyStringStringMap(m map[string]string) map[string]string { - n := map[string]string{} - for k, v := range m { - n[k] = v - } - return n -} - -// A container FS is split into two parts. The first is the top layer, a -// mutable layer, and the rest is the RootFS: the set of immutable layers -// that make up the image on which the container is based -func getRootFsSize(store storage.Store, containerID string) (int64, error) { - ctrStore, err := store.ContainerStore() - if err != nil { - return 0, err - } - container, err := ctrStore.Get(containerID) - if err != nil { - return 0, err - } - lstore, err := store.LayerStore() - if err != nil { - return 0, err - } - - // Ignore the size of the top layer. The top layer is a mutable RW layer - // and is not considered a part of the rootfs - rwLayer, err := lstore.Get(container.LayerID) - if err != nil { - return 0, err - } - layer, err := lstore.Get(rwLayer.Parent) - if err != nil { - return 0, err - } - - size := int64(0) - for layer.Parent != "" { - layerSize, err := lstore.DiffSize(layer.Parent, layer.ID) - if err != nil { - return size, errors.Wrapf(err, "getting diffsize of layer %q and its parent %q", layer.ID, layer.Parent) - } - size += layerSize - layer, err = lstore.Get(layer.Parent) - if err != nil { - return 0, err - } - } - // Get the size of the last layer. Has to be outside of the loop - // because the parent of the last layer is "", andlstore.Get("") - // will return an error - layerSize, err := lstore.DiffSize(layer.Parent, layer.ID) - return size + layerSize, err -} - -func getContainerRwSize(store storage.Store, containerID string) (int64, error) { - ctrStore, err := store.ContainerStore() - if err != nil { - return 0, err - } - container, err := ctrStore.Get(containerID) - if err != nil { - return 0, err - } - lstore, err := store.LayerStore() - if err != nil { - return 0, err - } - - // Get the size of the top layer by calculating the size of the diff - // between the layer and its parent. The top layer of a container is - // the only RW layer, all others are immutable - layer, err := lstore.Get(container.LayerID) - if err != nil { - return 0, err - } - return lstore.DiffSize(layer.Parent, layer.ID) -} - -func isTrue(str string) bool { - return str == "true" -} - -func isFalse(str string) bool { - return str == "false" -} - -func isValidBool(str string) bool { - return isTrue(str) || isFalse(str) -} - -func getDriverName(store storage.Store) (string, error) { - driver, err := store.GraphDriver() - if err != nil { - return "", err - } - return driver.String(), nil -} - -func getDriverMetadata(store storage.Store, layerID string) (map[string]string, error) { - driver, err := store.GraphDriver() - if err != nil { - return nil, err - } - return driver.Metadata(layerID) -} - -func getImageSize(image storage.Image, store storage.Store) (int64, error) { - is.Transport.SetStore(store) - storeRef, err := is.Transport.ParseStoreReference(store, "@"+image.ID) - if err != nil { - return -1, err - } - img, err := storeRef.NewImage(nil) - if err != nil { - return -1, err - } - imgSize, err := img.Size() - if err != nil { - return -1, err - } - return imgSize, nil -} - -func getImageTopLayer(image storage.Image) (string, error) { - metadata, err := parseMetadata(image) - if err != nil { - return "", err - } - // Get the digest of the first blob - digest := string(metadata.Blobs[0].Digest) - // Return the first layer associated with the given digest - return metadata.Layers[digest][0], nil -} - -func getPolicyContext(path string) (*signature.PolicyContext, error) { - policy, err := signature.DefaultPolicy(&types.SystemContext{SignaturePolicyPath: path}) - if err != nil { - return nil, err - } - return signature.NewPolicyContext(policy) -} -func parseRegistryCreds(creds string) (*types.DockerAuthConfig, error) { - if creds == "" { - return nil, errors.New("no credentials supplied") - } - if strings.Index(creds, ":") < 0 { - return nil, errors.New("user name supplied, but no password supplied") - } - v := strings.SplitN(creds, ":", 2) - cfg := &types.DockerAuthConfig{ - Username: v[0], - Password: v[1], - } - return cfg, nil -} - -func (o dockerRegistryOptions) getSystemContext(signaturePolicyPath string) *types.SystemContext { - sc := &types.SystemContext{ - SignaturePolicyPath: signaturePolicyPath, - DockerAuthConfig: o.DockerRegistryCreds, - DockerCertPath: o.DockerCertPath, - DockerInsecureSkipTLSVerify: o.DockerInsecureSkipTLSVerify, - } - return sc -} diff --git a/cmd/kpod/common_test.go b/cmd/kpod/common_test.go index 3ec33658..711c8c3e 100644 --- a/cmd/kpod/common_test.go +++ b/cmd/kpod/common_test.go @@ -7,7 +7,6 @@ import ( "flag" - is "github.com/containers/image/storage" "github.com/containers/storage" "github.com/urfave/cli" ) @@ -30,52 +29,6 @@ func TestGetStore(t *testing.T) { } } -func TestParseMetadata(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } else if len(images) == 0 { - t.Fatalf("no images with metadata to parse") - } - - _, err = parseMetadata(images[0]) - if err != nil { - t.Error(err) - } -} - -func TestGetSize(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - - _, err = getImageSize(images[0], store) - if err != nil { - t.Error(err) - } -} - func failTestIfNotRoot(t *testing.T) { u, err := user.Current() if err != nil { diff --git a/cmd/kpod/history.go b/cmd/kpod/history.go index aaa6df7d..ae0a5d3f 100644 --- a/cmd/kpod/history.go +++ b/cmd/kpod/history.go @@ -15,6 +15,7 @@ import ( is "github.com/containers/image/storage" "github.com/containers/storage" units "github.com/docker/go-units" + "github.com/kubernetes-incubator/cri-o/libkpod/common" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -240,7 +241,7 @@ func createJSON(store storage.Store, opts historyOptions) ([]byte, error) { return nil, errors.Errorf("no such image %q: %v", opts.image, err) } - systemContext := getSystemContext("") + systemContext := common.GetSystemContext("") src, err := ref.NewImage(systemContext) if err != nil { diff --git a/cmd/kpod/images.go b/cmd/kpod/images.go index d2a8dc48..22943043 100644 --- a/cmd/kpod/images.go +++ b/cmd/kpod/images.go @@ -3,11 +3,11 @@ package main import ( "fmt" "os" - "strings" "text/template" - is "github.com/containers/image/storage" "github.com/containers/storage" + "github.com/kubernetes-incubator/cri-o/libkpod/image" + libkpodimage "github.com/kubernetes-incubator/cri-o/libkpod/image" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -20,15 +20,6 @@ type imageOutputParams struct { Size string } -type filterParams struct { - dangling string - label string - beforeImage string // Images are sorted by date, so we can just output until we see the image - sinceImage string // Images are sorted by date, so we can just output until we don't see the image - seenImage bool // Hence this boolean - referencePattern string -} - var ( imagesFlags = []cli.Flag{ cli.BoolFlag{ @@ -104,14 +95,9 @@ func imagesCmd(c *cli.Context) error { return errors.New("'buildah images' requires at most 1 argument") } - images, err := store.Images() - if err != nil { - return errors.Wrapf(err, "error reading images") - } - - var params *filterParams + var params *libkpodimage.FilterParams if c.IsSet("filter") { - params, err = parseFilter(images, c.String("filter")) + params, err = libkpodimage.ParseFilter(store, c.String("filter")) if err != nil { return errors.Wrapf(err, "error parsing filter") } @@ -119,60 +105,15 @@ func imagesCmd(c *cli.Context) error { params = nil } - if len(images) > 0 && !noheading && !quiet && !hasTemplate { + imageList, err := libkpodimage.GetImagesMatchingFilter(store, params, name) + if err != nil { + return errors.Wrapf(err, "could not get list of images matching filter") + } + if len(imageList) > 0 && !noheading && !quiet && !hasTemplate { outputHeader(truncate, digests) } - return outputImages(images, formatString, store, params, name, hasTemplate, truncate, digests, quiet) -} - -func parseFilter(images []storage.Image, filter string) (*filterParams, error) { - params := new(filterParams) - filterStrings := strings.Split(filter, ",") - for _, param := range filterStrings { - pair := strings.SplitN(param, "=", 2) - switch strings.TrimSpace(pair[0]) { - case "dangling": - if 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 imageExists(images, pair[1]) { - params.beforeImage = pair[1] - } else { - return nil, fmt.Errorf("no such id: %s", pair[0]) - } - case "since": - if imageExists(images, pair[1]) { - params.sinceImage = pair[1] - } 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 imageExists(images []storage.Image, ref string) bool { - for _, image := range images { - if matchesID(image.ID, ref) { - return true - } - for _, name := range image.Names { - if matchesReference(name, ref) { - return true - } - } - } - return false + return outputImages(store, imageList, formatString, hasTemplate, truncate, digests, quiet) } func outputHeader(truncate, digests bool) { @@ -189,9 +130,9 @@ func outputHeader(truncate, digests bool) { fmt.Printf("%-22s %s\n", "CREATED AT", "SIZE") } -func outputImages(images []storage.Image, format string, store storage.Store, filters *filterParams, argName string, hasTemplate, truncate, digests, quiet bool) error { - for _, image := range images { - imageMetadata, err := parseMetadata(image) +func outputImages(store storage.Store, images []storage.Image, format string, hasTemplate, truncate, digests, quiet bool) error { + for _, img := range images { + imageMetadata, err := image.ParseMetadata(img) if err != nil { fmt.Println(err) } @@ -200,156 +141,33 @@ func outputImages(images []storage.Image, format string, store storage.Store, fi if len(imageMetadata.Blobs) > 0 { digest = string(imageMetadata.Blobs[0].Digest) } - size, _ := getImageSize(image, store) + size, _ := libkpodimage.Size(store, img) - names := []string{""} - if len(image.Names) > 0 { - names = image.Names - } else { - // images without names should be printed with "" as the image name - names = append(names, "") + if quiet { + fmt.Printf("%-64s\n", img.ID) + // We only want to print each id once + break } - for _, name := range names { - if !matchesFilter(image, store, name, filters) || !matchesReference(name, argName) { - continue - } - if quiet { - fmt.Printf("%-64s\n", image.ID) - // We only want to print each id once - break - } - params := imageOutputParams{ - ID: image.ID, - Name: name, - Digest: digest, - CreatedAt: createdTime, - Size: formattedSize(size), - } - if hasTemplate { - err = outputUsingTemplate(format, params) - if err != nil { - return err - } - continue - } - - outputUsingFormatString(truncate, digests, params) + params := imageOutputParams{ + ID: img.ID, + Name: img.Names[0], + Digest: digest, + CreatedAt: createdTime, + Size: libkpodimage.FormattedSize(size), } + if hasTemplate { + err = outputUsingTemplate(format, params) + if err != nil { + return err + } + continue + } + outputUsingFormatString(truncate, digests, params) } return nil } -func matchesFilter(image storage.Image, store storage.Store, name string, params *filterParams) bool { - if params == nil { - return true - } - if params.dangling != "" && !matchesDangling(name, params.dangling) { - return false - } else if params.label != "" && !matchesLabel(image, store, params.label) { - return false - } else if params.beforeImage != "" && !matchesBeforeImage(image, name, params) { - return false - } else if params.sinceImage != "" && !matchesSinceImage(image, name, params) { - return false - } else if params.referencePattern != "" && !matchesReference(name, params.referencePattern) { - return false - } - return true -} - -func matchesDangling(name string, dangling string) bool { - if isFalse(dangling) && name != "" { - return true - } else if isTrue(dangling) && name == "" { - return true - } - return false -} - -func matchesLabel(image storage.Image, store storage.Store, label string) bool { - storeRef, err := is.Transport.ParseStoreReference(store, "@"+image.ID) - if err != nil { - - } - img, err := storeRef.NewImage(nil) - if err != nil { - return false - } - info, err := img.Inspect() - if err != nil { - return false - } - - 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(image storage.Image, name string, params *filterParams) bool { - if params.seenImage { - return false - } - if matchesReference(name, params.beforeImage) || matchesID(image.ID, params.beforeImage) { - params.seenImage = true - return false - } - return true -} - -// Returns true if the image was created since the filter image. Returns -// false otherwise -func matchesSinceImage(image storage.Image, name string, params *filterParams) bool { - if params.seenImage { - return true - } - if matchesReference(name, params.sinceImage) || matchesID(image.ID, params.sinceImage) { - params.seenImage = true - } - return false -} - -func matchesID(id, argID string) bool { - return strings.HasPrefix(argID, id) -} - -func matchesReference(name, argName string) bool { - if argName == "" { - return true - } - 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) -} - -func formattedSize(size int64) string { - suffixes := [5]string{"B", "KB", "MB", "GB", "TB"} - - count := 0 - formattedSize := float64(size) - for formattedSize >= 1024 && count < 4 { - formattedSize /= 1024 - count++ - } - return fmt.Sprintf("%.4g %s", formattedSize, suffixes[count]) -} - func outputUsingTemplate(format string, params imageOutputParams) error { tmpl, err := template.New("image").Parse(format) if err != nil { diff --git a/cmd/kpod/images_test.go b/cmd/kpod/images_test.go deleted file mode 100644 index e714166d..00000000 --- a/cmd/kpod/images_test.go +++ /dev/null @@ -1,678 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "io" - "os" - "strings" - "testing" - - is "github.com/containers/image/storage" - "github.com/containers/storage" -) - -func TestTemplateOutputBlankTemplate(t *testing.T) { - params := imageOutputParams{ - ID: "0123456789abcdef", - Name: "test/image:latest", - Digest: "sha256:012345789abcdef012345789abcdef012345789abcdef012345789abcdef", - CreatedAt: "Jan 01 2016 10:45", - Size: "97 KB", - } - - err := outputUsingTemplate("", params) - //Output: Words - if err != nil { - t.Error(err) - } -} - -func TestTemplateOutputValidTemplate(t *testing.T) { - params := imageOutputParams{ - ID: "0123456789abcdef", - Name: "test/image:latest", - Digest: "sha256:012345789abcdef012345789abcdef012345789abcdef012345789abcdef", - CreatedAt: "Jan 01 2016 10:45", - Size: "97 KB", - } - - templateString := "{{.ID}}" - - output, err := captureOutputWithError(func() error { - return outputUsingTemplate(templateString, params) - }) - if err != nil { - t.Error(err) - } else if strings.TrimSpace(output) != strings.TrimSpace(params.ID) { - t.Errorf("Error with template output:\nExpected: %s\nReceived: %s\n", params.ID, output) - } -} - -func TestFormatStringOutput(t *testing.T) { - params := imageOutputParams{ - ID: "012345789abcdef", - Name: "test/image:latest", - Digest: "sha256:012345789abcdef012345789abcdef012345789abcdef012345789abcdef", - CreatedAt: "Jan 01 2016 10:45", - Size: "97 KB", - } - - output := captureOutput(func() { - outputUsingFormatString(true, true, params) - }) - expectedOutput := fmt.Sprintf("%-12.12s %-40s %-64s %-22s %s\n", params.ID, params.Name, params.Digest, params.CreatedAt, params.Size) - if output != expectedOutput { - t.Errorf("Error outputting using format string:\n\texpected: %s\n\treceived: %s\n", expectedOutput, output) - } -} - -func TestSizeFormatting(t *testing.T) { - size := formattedSize(0) - if size != "0 B" { - t.Errorf("Error formatting size: expected '%s' got '%s'", "0 B", size) - } - - size = formattedSize(1024) - if size != "1 KB" { - t.Errorf("Error formatting size: expected '%s' got '%s'", "1 KB", size) - } - - size = formattedSize(1024 * 1024 * 1024 * 1024 * 1024) - if size != "1024 TB" { - t.Errorf("Error formatting size: expected '%s' got '%s'", "1024 TB", size) - } -} - -func TestOutputHeader(t *testing.T) { - output := captureOutput(func() { - outputHeader(true, false) - }) - expectedOutput := fmt.Sprintf("%-12s %-40s %-22s %s\n", "IMAGE ID", "IMAGE NAME", "CREATED AT", "SIZE") - if output != expectedOutput { - t.Errorf("Error outputting header:\n\texpected: %s\n\treceived: %s\n", expectedOutput, output) - } - - output = captureOutput(func() { - outputHeader(true, true) - }) - expectedOutput = fmt.Sprintf("%-12s %-40s %-64s %-22s %s\n", "IMAGE ID", "IMAGE NAME", "DIGEST", "CREATED AT", "SIZE") - if output != expectedOutput { - t.Errorf("Error outputting header:\n\texpected: %s\n\treceived: %s\n", expectedOutput, output) - } - - output = captureOutput(func() { - outputHeader(false, false) - }) - expectedOutput = fmt.Sprintf("%-64s %-40s %-22s %s\n", "IMAGE ID", "IMAGE NAME", "CREATED AT", "SIZE") - if output != expectedOutput { - t.Errorf("Error outputting header:\n\texpected: %s\n\treceived: %s\n", expectedOutput, output) - } -} - -func TestMatchWithTag(t *testing.T) { - isMatch := matchesReference("docker.io/kubernetes/pause:latest", "pause:latest") - if !isMatch { - t.Error("expected match, got not match") - } - - isMatch = matchesReference("docker.io/kubernetes/pause:latest", "kubernetes/pause:latest") - if !isMatch { - t.Error("expected match, got no match") - } -} - -func TestNoMatchesReferenceWithTag(t *testing.T) { - isMatch := matchesReference("docker.io/kubernetes/pause:latest", "redis:latest") - if isMatch { - t.Error("expected no match, got match") - } - - isMatch = matchesReference("docker.io/kubernetes/pause:latest", "kubernetes/redis:latest") - if isMatch { - t.Error("expected no match, got match") - } -} - -func TestMatchesReferenceWithoutTag(t *testing.T) { - isMatch := matchesReference("docker.io/kubernetes/pause:latest", "pause") - if !isMatch { - t.Error("expected match, got not match") - } - - isMatch = matchesReference("docker.io/kubernetes/pause:latest", "kubernetes/pause") - if !isMatch { - t.Error("expected match, got no match") - } -} - -func TestNoMatchesReferenceWithoutTag(t *testing.T) { - isMatch := matchesReference("docker.io/kubernetes/pause:latest", "redis") - if isMatch { - t.Error("expected no match, got match") - } - - isMatch = matchesReference("docker.io/kubernetes/pause:latest", "kubernetes/redis") - if isMatch { - t.Error("expected no match, got match") - } -} - -func TestOutputImagesQuietTruncated(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - - // Tests quiet and truncated output - output, err := captureOutputWithError(func() error { - return outputImages(images[:1], "", store, nil, "", false, true, false, true) - }) - expectedOutput := fmt.Sprintf("%-64s\n", images[0].ID) - if err != nil { - t.Error("quiet/truncated output produces error") - } else if strings.TrimSpace(output) != strings.TrimSpace(expectedOutput) { - t.Errorf("quiet/truncated output does not match expected value\nExpected: %s\nReceived: %s\n", expectedOutput, output) - } -} - -func TestOutputImagesQuietNotTruncated(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - - // Tests quiet and non-truncated output - output, err := captureOutputWithError(func() error { - return outputImages(images[:1], "", store, nil, "", false, false, false, true) - }) - expectedOutput := fmt.Sprintf("%-64s\n", images[0].ID) - if err != nil { - t.Error("quiet/non-truncated output produces error") - } else if strings.TrimSpace(output) != strings.TrimSpace(expectedOutput) { - t.Errorf("quiet/non-truncated output does not match expected value\nExpected: %s\nReceived: %s\n", expectedOutput, output) - } -} - -func TestOutputImagesFormatString(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - - // Tests output with format template - output, err := captureOutputWithError(func() error { - return outputImages(images[:1], "{{.ID}}", store, nil, "", true, true, false, false) - }) - expectedOutput := fmt.Sprintf("%s", images[0].ID) - if err != nil { - t.Error("format string output produces error") - } else if strings.TrimSpace(output) != strings.TrimSpace(expectedOutput) { - t.Errorf("format string output does not match expected value\nExpected: %s\nReceived: %s\n", expectedOutput, output) - } -} - -func TestOutputImagesFormatTemplate(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - - // Tests quiet and non-truncated output - output, err := captureOutputWithError(func() error { - return outputImages(images[:1], "", store, nil, "", false, false, false, true) - }) - expectedOutput := fmt.Sprintf("%-64s\n", images[0].ID) - if err != nil { - t.Error("format template output produces error") - } else if strings.TrimSpace(output) != strings.TrimSpace(expectedOutput) { - t.Errorf("format template output does not match expected value\nExpected: %s\nReceived: %s\n", expectedOutput, output) - } -} - -func TestOutputImagesArgNoMatch(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - - // Tests output with an arg name that does not match. Args ending in ":" cannot match - // because all images in the repository must have a tag, and here the tag is an - // empty string - output, err := captureOutputWithError(func() error { - return outputImages(images[:1], "", store, nil, "foo:", false, true, false, false) - }) - expectedOutput := fmt.Sprintf("") - if err != nil { - t.Error("arg no match output produces error") - } else if strings.TrimSpace(output) != strings.TrimSpace(expectedOutput) { - t.Error("arg no match output should be empty") - } -} - -func TestOutputMultipleImages(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - - // Tests quiet and truncated output - output, err := captureOutputWithError(func() error { - return outputImages(images[:2], "", store, nil, "", false, true, false, true) - }) - expectedOutput := fmt.Sprintf("%-64s\n%-64s\n", images[0].ID, images[1].ID) - if err != nil { - t.Error("multi-image output produces error") - } else if strings.TrimSpace(output) != strings.TrimSpace(expectedOutput) { - t.Errorf("multi-image output does not match expected value\nExpected: %s\nReceived: %s\n", expectedOutput, output) - } -} - -func TestParseFilterAllParams(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - // Pull an image so we know we have it - err = pullTestImage("busybox:latest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - - label := "dangling=true,label=a=b,before=busybox:latest,since=busybox:latest,reference=abcdef" - params, err := parseFilter(images, label) - if err != nil { - t.Fatalf("error parsing filter") - } - - expectedParams := &filterParams{dangling: "true", label: "a=b", beforeImage: "busybox:latest", sinceImage: "busybox:latest", referencePattern: "abcdef"} - if *params != *expectedParams { - t.Errorf("filter did not return expected result\n\tExpected: %v\n\tReceived: %v", expectedParams, params) - } -} - -func TestParseFilterInvalidDangling(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - // Pull an image so we know we have it - err = pullTestImage("busybox:latest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - - label := "dangling=NO,label=a=b,before=busybox:latest,since=busybox:latest,reference=abcdef" - _, err = parseFilter(images, label) - if err == nil || err.Error() != "invalid filter: 'dangling=[NO]'" { - t.Fatalf("expected error parsing filter") - } -} - -func TestParseFilterInvalidBefore(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - // Pull an image so we know we have it - err = pullTestImage("busybox:latest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - - label := "dangling=false,label=a=b,before=:,since=busybox:latest,reference=abcdef" - _, err = parseFilter(images, label) - if err == nil || !strings.Contains(err.Error(), "no such id") { - t.Fatalf("expected error parsing filter") - } -} - -func TestParseFilterInvalidSince(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - // Pull an image so we know we have it - err = pullTestImage("busybox:latest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - - label := "dangling=false,label=a=b,before=busybox:latest,since=:,reference=abcdef" - _, err = parseFilter(images, label) - if err == nil || !strings.Contains(err.Error(), "no such id") { - t.Fatalf("expected error parsing filter") - } -} - -func TestParseFilterInvalidFilter(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - // Pull an image so we know we have it - err = pullTestImage("busybox:latest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - - label := "foo=bar" - _, err = parseFilter(images, label) - if err == nil || err.Error() != "invalid filter: 'foo'" { - t.Fatalf("expected error parsing filter") - } -} - -func TestImageExistsTrue(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - // Pull an image so we know we have it - err = pullTestImage("busybox:katest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - - if !imageExists(images, "busybox:latest") { - t.Errorf("expected image %s to exist", "busybox:latest") - } -} - -func TestImageExistsFalse(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - - if imageExists(images, ":") { - t.Errorf("image %s should not exist", ":") - } -} - -func TestMatchesDangingTrue(t *testing.T) { - if !matchesDangling("", "true") { - t.Error("matchesDangling() should return true with dangling=true and name=") - } - - if !matchesDangling("hello", "false") { - t.Error("matchesDangling() should return true with dangling=false and name='hello'") - } -} - -func TestMatchesDangingFalse(t *testing.T) { - if matchesDangling("hello", "true") { - t.Error("matchesDangling() should return false with dangling=true and name=hello") - } - - if matchesDangling("", "false") { - t.Error("matchesDangling() should return false with dangling=false and name=") - } -} - -func TestMatchesLabelTrue(t *testing.T) { - //TODO: How do I implement this? -} - -func TestMatchesLabelFalse(t *testing.T) { - // TODO: How do I implement this? -} - -func TestMatchesBeforeImageTrue(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - - // by default, params.seenImage is false - params := new(filterParams) - params.seenImage = false - params.beforeImage = "foo:bar" - if !matchesBeforeImage(images[0], ":", params) { - t.Error("should have matched beforeImage") - } -} - -func TestMatchesBeforeImageFalse(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - - // by default, params.seenImage is false - params := new(filterParams) - params.seenImage = true - params.beforeImage = "foo:bar" - // Should return false because the image has been seen - if matchesBeforeImage(images[0], ":", params) { - t.Error("should not have matched beforeImage") - } - - params.seenImage = false - if matchesBeforeImage(images[0], "foo:bar", params) { - t.Error("image should have been filtered out") - } -} - -func TestMatchesSinceeImageTrue(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - - // by default, params.seenImage is false - params := new(filterParams) - params.seenImage = true - params.sinceImage = "foo:bar" - if !matchesSinceImage(images[0], ":", params) { - t.Error("should have matched SinceImage") - } -} - -func TestMatchesSinceImageFalse(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - store, err := storage.GetStore(storage.DefaultStoreOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - - // by default, params.seenImage is false - params := new(filterParams) - params.seenImage = false - params.sinceImage = "foo:bar" - // Should return false because the image has been seen - if matchesSinceImage(images[0], ":", params) { - t.Error("should not have matched sinceImage") - } - - if matchesSinceImage(images[0], "foo:bar", params) { - t.Error("image should have been filtered out") - } -} - -func captureOutputWithError(f func() error) (string, error) { - old := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - err := f() - - w.Close() - os.Stdout = old - var buf bytes.Buffer - io.Copy(&buf, r) - return buf.String(), err -} - -// Captures output so that it can be compared to expected values -func captureOutput(f func()) string { - old := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - f() - - w.Close() - os.Stdout = old - var buf bytes.Buffer - io.Copy(&buf, r) - return buf.String() -} diff --git a/cmd/kpod/inspect.go b/cmd/kpod/inspect.go index b6f61e39..63d5c231 100644 --- a/cmd/kpod/inspect.go +++ b/cmd/kpod/inspect.go @@ -6,6 +6,8 @@ import ( "os" "text/template" + "github.com/kubernetes-incubator/cri-o/libkpod" + libkpodimage "github.com/kubernetes-incubator/cri-o/libkpod/image" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -83,19 +85,19 @@ func inspectCmd(c *cli.Context) error { var data interface{} switch itemType { case inspectTypeContainer: - data, err = getContainerData(store, name, size) + data, err = libkpod.GetContainerData(store, name, size) if err != nil { return errors.Wrapf(err, "error parsing container data") } case inspectTypeImage: - data, err = getImageData(store, name) + data, err = libkpodimage.GetImageData(store, name) if err != nil { return errors.Wrapf(err, "error parsing image data") } case inspectAll: - ctrData, err := getContainerData(store, name, size) + ctrData, err := libkpod.GetContainerData(store, name, size) if err != nil { - imgData, err := getImageData(store, name) + imgData, err := libkpodimage.GetImageData(store, name) if err != nil { return errors.Wrapf(err, "error parsing image data") } diff --git a/cmd/kpod/pull.go b/cmd/kpod/pull.go index a39555d4..2c0af7e5 100644 --- a/cmd/kpod/pull.go +++ b/cmd/kpod/pull.go @@ -1,27 +1,13 @@ package main import ( - "fmt" - "os" - "github.com/Sirupsen/logrus" - cp "github.com/containers/image/copy" - "github.com/containers/image/docker/reference" - "github.com/containers/image/signature" - is "github.com/containers/image/storage" - "github.com/containers/image/transports/alltransports" - "github.com/containers/image/types" - "github.com/containers/storage" + "github.com/kubernetes-incubator/cri-o/libkpod/common" + libkpodimage "github.com/kubernetes-incubator/cri-o/libkpod/image" "github.com/pkg/errors" "github.com/urfave/cli" ) -const ( - // DefaultRegistry is a prefix that we apply to an image name - // to check docker hub first for the image - DefaultRegistry = "docker://" -) - var ( pullFlags = []cli.Flag{ cli.BoolFlag{ @@ -69,59 +55,11 @@ func pullCmd(c *cli.Context) error { allTags = c.Bool("all-tags") } - systemContext := getSystemContext("") + systemContext := common.GetSystemContext("") - err = pullImage(store, image, allTags, systemContext) + err = libkpodimage.PullImage(store, image, allTags, systemContext) if err != nil { return errors.Errorf("error pulling image from %q: %v", image, err) } return nil } - -// pullImage copies the image from the source to the destination -func pullImage(store storage.Store, imgName string, allTags bool, sc *types.SystemContext) error { - defaultName := DefaultRegistry + imgName - var fromName string - var tag string - - srcRef, err := alltransports.ParseImageName(defaultName) - if err != nil { - srcRef2, err2 := alltransports.ParseImageName(imgName) - if err2 != nil { - return errors.Wrapf(err2, "error parsing image name %q", imgName) - } - srcRef = srcRef2 - } - - ref := srcRef.DockerReference() - if ref != nil { - imgName = srcRef.DockerReference().Name() - fromName = imgName - tagged, ok := srcRef.DockerReference().(reference.NamedTagged) - if ok { - imgName = imgName + ":" + tagged.Tag() - tag = tagged.Tag() - } - } - - destRef, err := is.Transport.ParseStoreReference(store, imgName) - if err != nil { - return errors.Wrapf(err, "error parsing full image name %q", 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 := getCopyOptions(os.Stdout, "", nil, nil, signingOptions{}) - - fmt.Println(tag + ": pulling from " + fromName) - return cp.Image(policyContext, destRef, srcRef, copyOptions) -} diff --git a/cmd/kpod/push.go b/cmd/kpod/push.go index 33eecc4a..32b7cd43 100644 --- a/cmd/kpod/push.go +++ b/cmd/kpod/push.go @@ -2,16 +2,12 @@ package main import ( "fmt" - "io" "os" - "syscall" - cp "github.com/containers/image/copy" - "github.com/containers/image/manifest" - "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/libkpod/common" + libkpodimage "github.com/kubernetes-incubator/cri-o/libkpod/image" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -68,32 +64,6 @@ var ( } ) -type pushOptions 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. - 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. - signingOptions -} - func pushCmd(c *cli.Context) error { var registryCreds *types.DockerAuthConfig @@ -116,7 +86,7 @@ func pushCmd(c *cli.Context) error { signBy := c.String("sign-by") if registryCredsString != "" { - creds, err := parseRegistryCreds(registryCredsString) + creds, err := common.ParseRegistryCreds(registryCredsString) if err != nil { return err } @@ -128,16 +98,16 @@ func pushCmd(c *cli.Context) error { return err } - options := pushOptions{ + options := libkpodimage.CopyOptions{ Compression: compress, SignaturePolicyPath: signaturePolicy, Store: store, - dockerRegistryOptions: dockerRegistryOptions{ + DockerRegistryOptions: common.DockerRegistryOptions{ DockerRegistryCreds: registryCreds, DockerCertPath: certPath, DockerInsecureSkipTLSVerify: skipVerify, }, - signingOptions: signingOptions{ + SigningOptions: common.SigningOptions{ RemoveSignatures: removeSignatures, SignBy: signBy, }, @@ -145,52 +115,5 @@ func pushCmd(c *cli.Context) error { if !c.Bool("quiet") { options.ReportWriter = os.Stderr } - return pushImage(srcName, destName, options) -} - -func pushImage(srcName, destName string, options pushOptions) 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 := 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 := getSystemContext(options.SignaturePolicyPath) - cid, err := importContainerImageDataFromImage(options.Store, systemContext, img.ID, "", "") - if err != nil { - return err - } - // Give the image we're producing the same ancestors as its source image - cid.FromImage = cid.Docker.ContainerConfig.Image - cid.FromImageID = string(cid.Docker.Parent) - - // Prep the layers and manifest for export - src, err := cid.makeImageRef(manifest.GuessMIMEType(cid.Manifest), options.Compression, img.Names, img.TopLayer, nil) - if err != nil { - return errors.Wrapf(err, "error copying layers and metadata") - } - - copyOptions := 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 + return libkpodimage.PushImage(srcName, destName, options) } diff --git a/cmd/kpod/rmi.go b/cmd/kpod/rmi.go index e1128da8..7b857a1a 100644 --- a/cmd/kpod/rmi.go +++ b/cmd/kpod/rmi.go @@ -3,11 +3,8 @@ package main import ( "fmt" - 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" + libkpodimage "github.com/kubernetes-incubator/cri-o/libkpod/image" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -48,7 +45,7 @@ func rmiCmd(c *cli.Context) error { } for _, id := range args { - image, err := getImage(id, store) + image, err := libkpodimage.FindImage(store, id) if err != nil { return errors.Wrapf(err, "could not get image %q", id) } @@ -67,14 +64,14 @@ func rmiCmd(c *cli.Context) error { } } // If the user supplied an ID, we cannot delete the image if it is referred to by multiple tags - if matchesID(image.ID, id) { + if libkpodimage.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 := untagImage(id, image, store) + name, err2 := libkpodimage.UntagImage(store, image, id) if err2 != nil { return err } @@ -84,7 +81,7 @@ func rmiCmd(c *cli.Context) error { if len(image.Names) > 0 { continue } - id, err := removeImage(image, store) + id, err := libkpodimage.RemoveImage(image, store) if err != nil { return err } @@ -95,69 +92,8 @@ func rmiCmd(c *cli.Context) error { return nil } -func getImage(id string, store storage.Store) (*storage.Image, error) { - var ref types.ImageReference - ref, err := properImageRef(id) - if err != nil { - //logrus.Debug(err) - } - if ref == nil { - if ref, err = storageImageRef(store, id); err != nil { - //logrus.Debug(err) - } - } - if ref == nil { - if ref, err = storageImageID(store, id); err != nil { - //logrus.Debug(err) - } - } - if ref != nil { - image, err2 := is.Transport.GetStoreImage(store, ref) - if err2 != nil { - return nil, err2 - } - return image, nil - } - return nil, err -} - -func untagImage(imgArg string, image *storage.Image, store storage.Store) (string, error) { - // Remove name from image.Names and set the new name in the ImageStore - imgStore, err := store.ImageStore() - if err != nil { - return "", errors.Wrap(err, "could not untag image") - } - newNames := []string{} - removedName := "" - for _, name := range image.Names { - if matchesReference(name, imgArg) { - removedName = name - continue - } - newNames = append(newNames, name) - } - imgStore.SetNames(image.ID, newNames) - err = imgStore.Save() - return removedName, err -} - -func removeImage(image *storage.Image, store storage.Store) (string, error) { - imgStore, err := store.ImageStore() - if err != nil { - return "", errors.Wrapf(err, "could not open image store") - } - err = imgStore.Delete(image.ID) - if err != nil { - return "", errors.Wrapf(err, "could not remove image") - } - err = imgStore.Save() - if err != nil { - return "", errors.Wrapf(err, "could not save image store") - } - return image.ID, 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{} ctrStore, err := store.ContainerStore() @@ -177,6 +113,7 @@ func runningContainers(image *storage.Image, store storage.Store) ([]string, err return ctrIDs, nil } +// TODO: replace this with something in libkpod func removeContainers(ctrIDs []string, store storage.Store) error { ctrStore, err := store.ContainerStore() if err != nil { @@ -189,50 +126,3 @@ func removeContainers(ctrIDs []string, store storage.Store) error { } return nil } - -// If it's looks like a proper image reference, parse it and check if it -// corresponds to an image that actually exists. -func properImageRef(id string) (types.ImageReference, error) { - var ref types.ImageReference - var err error - if ref, err = alltransports.ParseImageName(id); err == nil { - if img, err2 := ref.NewImage(nil); err2 == nil { - img.Close() - return ref, nil - } - return nil, fmt.Errorf("error confirming presence of image reference %q: %v", transports.ImageName(ref), err) - } - return nil, fmt.Errorf("error parsing %q as an image reference: %v", id, err) -} - -// If it's looks like an image reference that's relative to our storage, parse -// it and check if it corresponds to an image that actually exists. -func storageImageRef(store storage.Store, id string) (types.ImageReference, error) { - var ref types.ImageReference - var err error - if ref, err = is.Transport.ParseStoreReference(store, id); err == nil { - if img, err2 := ref.NewImage(nil); err2 == nil { - img.Close() - return ref, nil - } - return nil, fmt.Errorf("error confirming presence of storage image reference %q: %v", transports.ImageName(ref), err) - } - return nil, fmt.Errorf("error parsing %q as a storage image reference: %v", id, err) -} - -// If it might be an ID that's relative to our storage, parse it and check if it -// corresponds to an image that actually exists. This _should_ be redundant, -// since we already tried deleting the image using the ID directly above, but it -// can't hurt either. -func storageImageID(store storage.Store, id string) (types.ImageReference, error) { - var ref types.ImageReference - var err error - if ref, err = is.Transport.ParseStoreReference(store, "@"+id); err == nil { - if img, err2 := ref.NewImage(nil); err2 == nil { - img.Close() - return ref, nil - } - return nil, fmt.Errorf("error confirming presence of storage image reference %q: %v", transports.ImageName(ref), err) - } - return nil, fmt.Errorf("error parsing %q as a storage image reference: %v", "@"+id, err) -} diff --git a/cmd/kpod/rmi_test.go b/cmd/kpod/rmi_test.go deleted file mode 100644 index c10c8eeb..00000000 --- a/cmd/kpod/rmi_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package main - -import ( - "strings" - "testing" - - is "github.com/containers/image/storage" - "github.com/containers/storage" -) - -func TestProperImageRefTrue(t *testing.T) { - // Pull an image so we know we have it - err := pullTestImage("busybox:latest") - if err != nil { - t.Fatalf("could not pull image to remove") - } - // This should match a url path - imgRef, err := properImageRef("docker://busybox:latest") - if err != nil { - t.Errorf("could not match image: %v", err) - } else if imgRef == nil { - t.Error("Returned nil Image Reference") - } -} - -func TestProperImageRefFalse(t *testing.T) { - // Pull an image so we know we have it - err := pullTestImage("busybox:latest") - if err != nil { - t.Fatal("could not pull image to remove") - } - // This should match a url path - imgRef, _ := properImageRef("docker://:") - if imgRef != nil { - t.Error("should not have found an Image Reference") - } -} - -func TestStorageImageRefTrue(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - options := storage.DefaultStoreOptions - store, err := storage.GetStore(options) - if store != nil { - is.Transport.SetStore(store) - } - if err != nil { - t.Fatalf("could not get store: %v", err) - } - // Pull an image so we know we have it - err = pullTestImage("busybox:latest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - imgRef, err := storageImageRef(store, "busybox") - if err != nil { - t.Errorf("could not match image: %v", err) - } else if imgRef == nil { - t.Error("Returned nil Image Reference") - } -} - -func TestStorageImageRefFalse(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - options := storage.DefaultStoreOptions - store, err := storage.GetStore(options) - if store != nil { - is.Transport.SetStore(store) - } - if err != nil { - t.Fatalf("could not get store: %v", err) - } - // Pull an image so we know we have it - err = pullTestImage("busybox:latest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - imgRef, _ := storageImageRef(store, "") - if imgRef != nil { - t.Error("should not have found an Image Reference") - } -} - -func TestStorageImageIDTrue(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - options := storage.DefaultStoreOptions - store, err := storage.GetStore(options) - if store != nil { - is.Transport.SetStore(store) - } - if err != nil { - t.Fatalf("could not get store: %v", err) - } - // Pull an image so we know we have it - err = pullTestImage("busybox:latest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - //Somehow I have to get the id of the image I just pulled - images, err := store.Images() - if err != nil { - t.Fatalf("Error reading images: %v", err) - } - id, err := captureOutputWithError(func() error { - return outputImages(images, "", store, nil, "busybox:latest", false, false, false, true) - }) - if err != nil { - t.Fatalf("Error getting id of image: %v", err) - } - id = strings.TrimSpace(id) - - imgRef, err := storageImageID(store, id) - if err != nil { - t.Errorf("could not match image: %v", err) - } else if imgRef == nil { - t.Error("Returned nil Image Reference") - } -} - -func TestStorageImageIDFalse(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - options := storage.DefaultStoreOptions - store, err := storage.GetStore(options) - if store != nil { - is.Transport.SetStore(store) - } - if err != nil { - t.Fatalf("could not get store: %v", err) - } - // Pull an image so we know we have it - - id := "" - - imgRef, _ := storageImageID(store, id) - if imgRef != nil { - t.Error("should not have returned Image Reference") - } -} diff --git a/cmd/kpod/tag.go b/cmd/kpod/tag.go index 575290d3..435f5072 100644 --- a/cmd/kpod/tag.go +++ b/cmd/kpod/tag.go @@ -3,6 +3,7 @@ package main import ( "github.com/containers/image/docker/reference" "github.com/containers/storage" + libkpodimage "github.com/kubernetes-incubator/cri-o/libkpod/image" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -27,7 +28,7 @@ func tagCmd(c *cli.Context) error { if err != nil { return err } - img, err := findImage(store, args[0]) + img, err := libkpodimage.FindImage(store, args[0]) if err != nil { return err } diff --git a/libkpod/common/common.go b/libkpod/common/common.go new file mode 100644 index 00000000..4d7a079a --- /dev/null +++ b/libkpod/common/common.go @@ -0,0 +1,89 @@ +package common + +import ( + "errors" + "io" + "strings" + + cp "github.com/containers/image/copy" + "github.com/containers/image/signature" + "github.com/containers/image/types" +) + +// GetCopyOptions constructs a new containers/image/copy.Options{} struct from the given parameters +func GetCopyOptions(reportWriter io.Writer, signaturePolicyPath string, srcDockerRegistry, destDockerRegistry *DockerRegistryOptions, signing SigningOptions) *cp.Options { + if srcDockerRegistry == nil { + srcDockerRegistry = &DockerRegistryOptions{} + } + if destDockerRegistry == nil { + destDockerRegistry = &DockerRegistryOptions{} + } + srcContext := srcDockerRegistry.GetSystemContext(signaturePolicyPath) + destContext := destDockerRegistry.GetSystemContext(signaturePolicyPath) + return &cp.Options{ + RemoveSignatures: signing.RemoveSignatures, + SignBy: signing.SignBy, + ReportWriter: reportWriter, + SourceCtx: srcContext, + DestinationCtx: destContext, + } +} + +// GetSystemContext Constructs a new containers/image/types.SystemContext{} struct from the given signaturePolicy path +func GetSystemContext(signaturePolicyPath string) *types.SystemContext { + sc := &types.SystemContext{} + if signaturePolicyPath != "" { + sc.SignaturePolicyPath = signaturePolicyPath + } + return sc +} + +// CopyStringStringMap deep copies a map[string]string and returns the result +func CopyStringStringMap(m map[string]string) map[string]string { + n := map[string]string{} + for k, v := range m { + n[k] = v + } + return n +} + +// IsTrue determines whether the given string equals "true" +func IsTrue(str string) bool { + return str == "true" +} + +// IsFalse determines whether the given string equals "false" +func IsFalse(str string) bool { + return str == "false" +} + +// IsValidBool determines whether the given string equals "true" or "false" +func IsValidBool(str string) bool { + return IsTrue(str) || IsFalse(str) +} + +// GetPolicyContext creates a signature policy context for the given signature policy path +func GetPolicyContext(path string) (*signature.PolicyContext, error) { + policy, err := signature.DefaultPolicy(&types.SystemContext{SignaturePolicyPath: path}) + if err != nil { + return nil, err + } + return signature.NewPolicyContext(policy) +} + +// ParseRegistryCreds takes a credentials string in the form USERNAME:PASSWORD +// and returns a DockerAuthConfig +func ParseRegistryCreds(creds string) (*types.DockerAuthConfig, error) { + if creds == "" { + return nil, errors.New("no credentials supplied") + } + if !strings.Contains(creds, ":") { + return nil, errors.New("user name supplied, but no password supplied") + } + v := strings.SplitN(creds, ":", 2) + cfg := &types.DockerAuthConfig{ + Username: v[0], + Password: v[1], + } + return cfg, nil +} diff --git a/libkpod/common/dockerRegistryOptions.go b/libkpod/common/dockerRegistryOptions.go new file mode 100644 index 00000000..fdbaa059 --- /dev/null +++ b/libkpod/common/dockerRegistryOptions.go @@ -0,0 +1,33 @@ +package common + +import "github.com/containers/image/types" + +// DockerRegistryOptions encapsulates settings that affect how we connect or +// authenticate to a remote registry. +type DockerRegistryOptions struct { + // DockerRegistryCreds is the user name and password to supply in case + // we need to pull an image from a registry, and it requires us to + // authenticate. + DockerRegistryCreds *types.DockerAuthConfig + // DockerCertPath is the location of a directory containing CA + // certificates which will be used to verify the registry's certificate + // (all files with names ending in ".crt"), and possibly client + // certificates and private keys (pairs of files with the same name, + // except for ".cert" and ".key" suffixes). + DockerCertPath string + // DockerInsecureSkipTLSVerify turns off verification of TLS + // certificates and allows connecting to registries without encryption. + DockerInsecureSkipTLSVerify bool +} + +// GetSystemContext constructs a new system context from the given signaturePolicy path and the +// values in the DockerRegistryOptions +func (o DockerRegistryOptions) GetSystemContext(signaturePolicyPath string) *types.SystemContext { + sc := &types.SystemContext{ + SignaturePolicyPath: signaturePolicyPath, + DockerAuthConfig: o.DockerRegistryCreds, + DockerCertPath: o.DockerCertPath, + DockerInsecureSkipTLSVerify: o.DockerInsecureSkipTLSVerify, + } + return sc +} diff --git a/libkpod/common/signingOptions.go b/libkpod/common/signingOptions.go new file mode 100644 index 00000000..b7e14be8 --- /dev/null +++ b/libkpod/common/signingOptions.go @@ -0,0 +1,10 @@ +package common + +// SigningOptions encapsulates settings that control whether or not we strip or +// add signatures to images when writing them. +type SigningOptions struct { + // RemoveSignatures directs us to remove any signatures which are already present. + RemoveSignatures bool + // SignBy is a key identifier of some kind, indicating that a signature should be generated using the specified private key and stored with the image. + SignBy string +} diff --git a/libkpod/container.go b/libkpod/container.go new file mode 100644 index 00000000..9873271c --- /dev/null +++ b/libkpod/container.go @@ -0,0 +1,97 @@ +package libkpod + +import ( + cstorage "github.com/containers/storage" + "github.com/pkg/errors" +) + +// FindContainer searches for a container with the given name or ID in the given store +func FindContainer(store cstorage.Store, container string) (*cstorage.Container, error) { + ctrStore, err := store.ContainerStore() + if err != nil { + return nil, err + } + return ctrStore.Get(container) +} + +// GetContainerTopLayerID gets the ID of the top layer of the given container +func GetContainerTopLayerID(store cstorage.Store, containerID string) (string, error) { + ctr, err := FindContainer(store, containerID) + if err != nil { + return "", err + } + return ctr.LayerID, nil +} + +// GetContainerRwSize Gets the size of the mutable top layer of the container +func GetContainerRwSize(store cstorage.Store, containerID string) (int64, error) { + ctrStore, err := store.ContainerStore() + if err != nil { + return 0, err + } + container, err := ctrStore.Get(containerID) + if err != nil { + return 0, err + } + lstore, err := store.LayerStore() + if err != nil { + return 0, err + } + + // Get the size of the top layer by calculating the size of the diff + // between the layer and its parent. The top layer of a container is + // the only RW layer, all others are immutable + layer, err := lstore.Get(container.LayerID) + if err != nil { + return 0, err + } + return lstore.DiffSize(layer.Parent, layer.ID) +} + +// GetContainerRootFsSize gets the size of the container's root filesystem +// A container FS is split into two parts. The first is the top layer, a +// mutable layer, and the rest is the RootFS: the set of immutable layers +// that make up the image on which the container is based +func GetContainerRootFsSize(store cstorage.Store, containerID string) (int64, error) { + ctrStore, err := store.ContainerStore() + if err != nil { + return 0, err + } + container, err := ctrStore.Get(containerID) + if err != nil { + return 0, err + } + lstore, err := store.LayerStore() + if err != nil { + return 0, err + } + + // Ignore the size of the top layer. The top layer is a mutable RW layer + // and is not considered a part of the rootfs + rwLayer, err := lstore.Get(container.LayerID) + if err != nil { + return 0, err + } + layer, err := lstore.Get(rwLayer.Parent) + if err != nil { + return 0, err + } + + size := int64(0) + for layer.Parent != "" { + layerSize, err := lstore.DiffSize(layer.Parent, layer.ID) + if err != nil { + return size, errors.Wrapf(err, "getting diffsize of layer %q and its parent %q", layer.ID, layer.Parent) + } + size += layerSize + layer, err = lstore.Get(layer.Parent) + if err != nil { + return 0, err + } + } + // Get the size of the last layer. Has to be outside of the loop + // because the parent of the last layer is "", andlstore.Get("") + // will return an error + layerSize, err := lstore.DiffSize(layer.Parent, layer.ID) + return size + layerSize, err +} diff --git a/cmd/kpod/containerData.go b/libkpod/containerData.go similarity index 80% rename from cmd/kpod/containerData.go rename to libkpod/containerData.go index 2ecc00e9..8be75902 100644 --- a/cmd/kpod/containerData.go +++ b/libkpod/containerData.go @@ -1,4 +1,4 @@ -package main +package libkpod import ( "encoding/json" @@ -9,15 +9,18 @@ import ( "github.com/containers/storage" "github.com/kubernetes-incubator/cri-o/cmd/kpod/docker" + "github.com/kubernetes-incubator/cri-o/libkpod/common" + "github.com/kubernetes-incubator/cri-o/libkpod/driver" + libkpodimage "github.com/kubernetes-incubator/cri-o/libkpod/image" "github.com/kubernetes-incubator/cri-o/oci" "github.com/kubernetes-incubator/cri-o/pkg/annotations" - "github.com/kubernetes-incubator/cri-o/server" "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" ) -type containerData struct { +// ContainerData handles the data used when inspecting a container +type ContainerData struct { ID string Name string LogPath string @@ -52,12 +55,14 @@ type driverData struct { Data map[string]string } -func getContainerData(store storage.Store, name string, size bool) (*containerData, error) { +// GetContainerData gets the ContainerData for a container with the given name in the given store. +// If size is set to true, it will also determine the size of the container +func GetContainerData(store storage.Store, name string, size bool) (*ContainerData, error) { ctr, err := inspectContainer(store, name) if err != nil { return nil, errors.Wrapf(err, "error reading build container %q", name) } - cid, err := openContainer(store, name) + cid, err := libkpodimage.GetContainerCopyData(store, name) if err != nil { return nil, errors.Wrapf(err, "error reading container image data") } @@ -71,19 +76,19 @@ func getContainerData(store storage.Store, name string, size bool) (*containerDa return nil, err } - driverName, err := getDriverName(store) + driverName, err := driver.GetDriverName(store) if err != nil { return nil, err } - topLayer, err := getContainerTopLayerID(store, ctr.ID()) + topLayer, err := GetContainerTopLayerID(store, ctr.ID()) if err != nil { return nil, err } - driverMetadata, err := getDriverMetadata(store, topLayer) + driverMetadata, err := driver.GetDriverMetadata(store, topLayer) if err != nil { return nil, err } - data := &containerData{ + data := &ContainerData{ ID: ctr.ID(), Name: ctr.Name(), LogPath: ctr.LogPath(), @@ -115,13 +120,13 @@ func getContainerData(store storage.Store, name string, size bool) (*containerDa } if size { - sizeRootFs, err := getRootFsSize(store, data.ID) + sizeRootFs, err := GetContainerRootFsSize(store, data.ID) if err != nil { return nil, errors.Wrapf(err, "error reading size for container %q", name) } data.SizeRootFs = uint(sizeRootFs) - sizeRw, err := getContainerRwSize(store, data.ID) + sizeRw, err := GetContainerRwSize(store, data.ID) if err != nil { return nil, errors.Wrapf(err, "error reading RWSize for container %q", name) } @@ -152,7 +157,7 @@ func inspectContainer(store storage.Store, container string) (*oci.Container, er // get an oci.Container instance for a given container ID func getOCIContainer(store storage.Store, container string) (*oci.Container, error) { - ctr, err := findContainer(store, container) + ctr, err := FindContainer(store, container) if err != nil { return nil, err } @@ -179,9 +184,9 @@ func getOCIContainer(store storage.Store, container string) (*oci.Container, err return nil, err } - tty := isTrue(m.Annotations[annotations.TTY]) - stdin := isTrue(m.Annotations[annotations.Stdin]) - stdinOnce := isTrue(m.Annotations[annotations.StdinOnce]) + tty := common.IsTrue(m.Annotations[annotations.TTY]) + stdin := common.IsTrue(m.Annotations[annotations.Stdin]) + stdinOnce := common.IsTrue(m.Annotations[annotations.StdinOnce]) containerPath, err := store.ContainerRunDirectory(ctr.ID) if err != nil { @@ -214,6 +219,6 @@ func getOCIContainer(store storage.Store, container string) (*oci.Container, err } func getOCIRuntime(store storage.Store, container string) (*oci.Runtime, error) { - config := server.DefaultConfig() - return oci.New(config.Runtime, config.RuntimeUntrustedWorkload, config.DefaultWorkloadTrust, config.Conmon, config.ConmonEnv, config.CgroupManager) + // TODO: Move server default config out of server so that it can be used instead of this + return oci.New("/usr/bin/runc", "", "runtime", "/usr/local/libexec/crio/conmon", []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, "cgroupfs") } diff --git a/libkpod/driver/driver.go b/libkpod/driver/driver.go new file mode 100644 index 00000000..4db55852 --- /dev/null +++ b/libkpod/driver/driver.go @@ -0,0 +1,27 @@ +package driver + +import cstorage "github.com/containers/storage" + +// Data handles the data for a storage driver +type Data struct { + Name string + Data map[string]string +} + +// GetDriverName returns the name of the driver for the given store +func GetDriverName(store cstorage.Store) (string, error) { + driver, err := store.GraphDriver() + if err != nil { + return "", err + } + return driver.String(), nil +} + +// GetDriverMetadata returns the metadata regarding the driver for the layer in the given store +func GetDriverMetadata(store cstorage.Store, layerID string) (map[string]string, error) { + driver, err := store.GraphDriver() + if err != nil { + return nil, err + } + return driver.Metadata(layerID) +} diff --git a/cmd/kpod/containerImageRef.go b/libkpod/image/CopyRef.go similarity index 79% rename from cmd/kpod/containerImageRef.go rename to libkpod/image/CopyRef.go index f43a76ad..d36c44fd 100644 --- a/cmd/kpod/containerImageRef.go +++ b/libkpod/image/CopyRef.go @@ -1,4 +1,4 @@ -package main +package image import ( "bytes" @@ -24,7 +24,8 @@ import ( "github.com/pkg/errors" ) -type containerImageRef struct { +// CopyRef handles image references used for copying images to/from remotes +type CopyRef struct { store storage.Store compression archive.Compression name reference.Named @@ -40,9 +41,9 @@ type containerImageRef struct { exporting bool } -type containerImageSource struct { +type copySource struct { path string - ref *containerImageRef + ref *CopyRef store storage.Store layerID string names []string @@ -55,8 +56,9 @@ type containerImageSource struct { exporting bool } -func (i *containerImageRef) NewImage(sc *types.SystemContext) (types.Image, error) { - src, err := i.NewImageSource(sc, nil) +// NewImage creates a new image from the given system context +func (c *CopyRef) NewImage(sc *types.SystemContext) (types.Image, error) { + src, err := c.NewImageSource(sc, nil) if err != nil { return nil, err } @@ -78,10 +80,11 @@ func selectManifestType(preferred string, acceptable, supported []string) string return selected } -func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestTypes []string) (src types.ImageSource, err error) { +// NewImageSource creates a new image source from the given system context and manifest +func (c *CopyRef) NewImageSource(sc *types.SystemContext, manifestTypes []string) (src types.ImageSource, err error) { // Decide which type of manifest and configuration output we're going to provide. supportedManifestTypes := []string{v1.MediaTypeImageManifest, docker.V2S2MediaTypeManifest} - manifestType := selectManifestType(i.preferredManifestType, manifestTypes, supportedManifestTypes) + manifestType := selectManifestType(c.preferredManifestType, manifestTypes, supportedManifestTypes) // If it's not a format we support, return an error. if manifestType != v1.MediaTypeImageManifest && manifestType != docker.V2S2MediaTypeManifest { return nil, errors.Errorf("no supported manifest types (attempted to use %q, only know %q and %q)", @@ -89,8 +92,8 @@ func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestType } // Start building the list of layers using the read-write layer. layers := []string{} - layerID := i.layerID - layer, err := i.store.Layer(layerID) + layerID := c.layerID + layer, err := c.store.Layer(layerID) if err != nil { return nil, errors.Wrapf(err, "unable to read layer %q", layerID) } @@ -102,7 +105,7 @@ func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestType err = nil break } - layer, err = i.store.Layer(layerID) + layer, err = c.store.Layer(layerID) if err != nil { return nil, errors.Wrapf(err, "unable to read layer %q", layerID) } @@ -127,12 +130,12 @@ func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestType // Build fresh copies of the configurations so that we don't mess with the values in the Builder // object itself. oimage := v1.Image{} - err = json.Unmarshal(i.oconfig, &oimage) + err = json.Unmarshal(c.oconfig, &oimage) if err != nil { return nil, err } dimage := docker.V2Image{} - err = json.Unmarshal(i.dconfig, &dimage) + err = json.Unmarshal(c.dconfig, &dimage) if err != nil { return nil, err } @@ -146,7 +149,7 @@ func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestType MediaType: v1.MediaTypeImageConfig, }, Layers: []v1.Descriptor{}, - Annotations: i.annotations, + Annotations: c.annotations, } dmanifest := docker.V2S2Manifest{ V2Versioned: docker.V2Versioned{ @@ -170,8 +173,8 @@ func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestType omediaType := v1.MediaTypeImageLayer dmediaType := docker.V2S2MediaTypeUncompressedLayer // Figure out which media type we want to call this. Assume no compression. - if i.compression != archive.Uncompressed { - switch i.compression { + if c.compression != archive.Uncompressed { + switch c.compression { case archive.Gzip: omediaType = v1.MediaTypeImageLayerGzip dmediaType = docker.V2S2MediaTypeLayer @@ -185,7 +188,7 @@ func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestType } } // If we're not re-exporting the data, just fake up layer and diff IDs for the manifest. - if !i.exporting { + if !c.exporting { fakeLayerDigest := digest.NewDigestFromHex(digest.Canonical.String(), layerID) // Add a note in the manifest about the layer. The blobs should be identified by their // possibly-compressed blob digests, but just use the layer IDs here. @@ -208,7 +211,7 @@ func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestType continue } // Start reading the layer. - rc, err := i.store.Diff("", layerID, nil) + rc, err := c.store.Diff("", layerID, nil) if err != nil { return nil, errors.Wrapf(err, "error extracting layer %q", layerID) } @@ -233,7 +236,7 @@ func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestType counter := ioutils.NewWriteCounter(layerFile) multiWriter := io.MultiWriter(counter, destHasher.Hash()) // Compress the layer, if we're compressing it. - writer, err := archive.CompressStream(multiWriter, i.compression) + writer, err := archive.CompressStream(multiWriter, c.compression) if err != nil { return nil, errors.Wrapf(err, "error compressing layer %q", layerID) } @@ -243,7 +246,7 @@ func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestType } writer.Close() layerFile.Close() - if i.compression == archive.Uncompressed { + if c.compression == archive.Uncompressed { if size != counter.Count { return nil, errors.Errorf("error storing layer %q to file: inconsistent layer size (copied %d, wrote %d)", layerID, size, counter.Count) } @@ -275,18 +278,18 @@ func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestType dimage.RootFS.DiffIDs = append(dimage.RootFS.DiffIDs, srcHasher.Digest()) } - if i.addHistory { + if c.addHistory { // Build history notes in the image configurations. onews := v1.History{ - Created: &i.created, - CreatedBy: i.createdBy, + Created: &c.created, + CreatedBy: c.createdBy, Author: oimage.Author, EmptyLayer: false, } oimage.History = append(oimage.History, onews) dnews := docker.V2S2History{ - Created: i.created, - CreatedBy: i.createdBy, + Created: c.created, + CreatedBy: c.createdBy, Author: dimage.Author, EmptyLayer: false, } @@ -344,90 +347,97 @@ func (i *containerImageRef) NewImageSource(sc *types.SystemContext, manifestType default: panic("unreachable code: unsupported manifest type") } - src = &containerImageSource{ + src = ©Source{ path: path, - ref: i, - store: i.store, - layerID: i.layerID, - names: i.names, - addHistory: i.addHistory, - compression: i.compression, + ref: c, + store: c.store, + layerID: c.layerID, + names: c.names, + addHistory: c.addHistory, + compression: c.compression, config: config, configDigest: digest.Canonical.FromBytes(config), manifest: manifest, manifestType: manifestType, - exporting: i.exporting, + exporting: c.exporting, } return src, nil } -func (i *containerImageRef) NewImageDestination(sc *types.SystemContext) (types.ImageDestination, error) { +// NewImageDestination creates a new image destination from the given system context +func (c *CopyRef) NewImageDestination(sc *types.SystemContext) (types.ImageDestination, error) { return nil, errors.Errorf("can't write to a container") } -func (i *containerImageRef) DockerReference() reference.Named { - return i.name +// DockerReference gets the docker reference for the given CopyRef +func (c *CopyRef) DockerReference() reference.Named { + return c.name } -func (i *containerImageRef) StringWithinTransport() string { - if len(i.names) > 0 { - return i.names[0] +// StringWithinTransport returns the first name of the copyRef +func (c *CopyRef) StringWithinTransport() string { + if len(c.names) > 0 { + return c.names[0] } return "" } -func (i *containerImageRef) DeleteImage(*types.SystemContext) error { +// DeleteImage deletes an image in the CopyRef +func (c *CopyRef) DeleteImage(*types.SystemContext) error { // we were never here return nil } -func (i *containerImageRef) PolicyConfigurationIdentity() string { +// PolicyConfigurationIdentity returns the policy configuration for the CopyRef +func (c *CopyRef) PolicyConfigurationIdentity() string { return "" } -func (i *containerImageRef) PolicyConfigurationNamespaces() []string { +// PolicyConfigurationNamespaces returns the policy configuration namespace for the CopyRef +func (c *CopyRef) PolicyConfigurationNamespaces() []string { return nil } -func (i *containerImageRef) Transport() types.ImageTransport { +// Transport returns an ImageTransport for the given CopyRef +func (c *CopyRef) Transport() types.ImageTransport { return is.Transport } -func (i *containerImageSource) Close() error { - err := os.RemoveAll(i.path) +func (cs *copySource) Close() error { + err := os.RemoveAll(cs.path) if err != nil { - logrus.Errorf("error removing %q: %v", i.path, err) + logrus.Errorf("error removing %q: %v", cs.path, err) } return err } -func (i *containerImageSource) Reference() types.ImageReference { - return i.ref +func (cs *copySource) Reference() types.ImageReference { + return cs.ref } -func (i *containerImageSource) GetSignatures() ([][]byte, error) { +func (cs *copySource) GetSignatures() ([][]byte, error) { return nil, nil } -func (i *containerImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string, error) { +func (cs *copySource) GetTargetManifest(digest digest.Digest) ([]byte, string, error) { return []byte{}, "", errors.Errorf("TODO") } -func (i *containerImageSource) GetManifest() ([]byte, string, error) { - return i.manifest, i.manifestType, nil +func (cs *copySource) GetManifest() ([]byte, string, error) { + return cs.manifest, cs.manifestType, nil } -func (i *containerImageSource) GetBlob(blob types.BlobInfo) (reader io.ReadCloser, size int64, err error) { - if blob.Digest == i.configDigest { +func (cs *copySource) GetBlob(blob types.BlobInfo) (reader io.ReadCloser, size int64, err error) { + if blob.Digest == cs.configDigest { logrus.Debugf("start reading config") - reader := bytes.NewReader(i.config) + reader := bytes.NewReader(cs.config) closer := func() error { logrus.Debugf("finished reading config") return nil } return ioutils.NewReadCloserWrapper(reader, closer), reader.Size(), nil } - layerFile, err := os.OpenFile(filepath.Join(i.path, blob.Digest.String()), os.O_RDONLY, 0600) + layerFile, err := os.OpenFile(filepath.Join(cs.path, blob.Digest.String()), os.O_RDONLY, 0600) if err != nil { logrus.Debugf("error reading layer %q: %v", blob.Digest.String(), err) return nil, -1, err diff --git a/libkpod/image/copy.go b/libkpod/image/copy.go new file mode 100644 index 00000000..64be749e --- /dev/null +++ b/libkpod/image/copy.go @@ -0,0 +1,149 @@ +package image + +import ( + "fmt" + "io" + "os" + "syscall" + + cp "github.com/containers/image/copy" + "github.com/containers/image/docker/reference" + "github.com/containers/image/manifest" + "github.com/containers/image/signature" + is "github.com/containers/image/storage" + "github.com/containers/image/transports/alltransports" + "github.com/containers/image/types" + "github.com/containers/storage" + "github.com/containers/storage/pkg/archive" + "github.com/kubernetes-incubator/cri-o/libkpod/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://" +) + +// 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 +} + +// 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(store storage.Store, imgName string, allTags bool, sc *types.SystemContext) error { + defaultName := DefaultRegistry + imgName + var fromName string + var tag string + + srcRef, err := alltransports.ParseImageName(defaultName) + if err != nil { + srcRef2, err2 := alltransports.ParseImageName(imgName) + if err2 != nil { + return errors.Wrapf(err2, "error parsing image name %q", imgName) + } + srcRef = srcRef2 + } + + ref := srcRef.DockerReference() + if ref != nil { + imgName = srcRef.DockerReference().Name() + fromName = imgName + tagged, ok := srcRef.DockerReference().(reference.NamedTagged) + if ok { + imgName = imgName + ":" + tagged.Tag() + tag = tagged.Tag() + } + } + + destRef, err := is.Transport.ParseStoreReference(store, imgName) + if err != nil { + return errors.Wrapf(err, "error parsing full image name %q", 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(os.Stdout, "", nil, nil, common.SigningOptions{}) + + fmt.Println(tag + ": pulling from " + fromName) + return cp.Image(policyContext, destRef, srcRef, copyOptions) +} diff --git a/cmd/kpod/containerImageData.go b/libkpod/image/copyData.go similarity index 87% rename from cmd/kpod/containerImageData.go rename to libkpod/image/copyData.go index 768044a0..2dedbe56 100644 --- a/cmd/kpod/containerImageData.go +++ b/libkpod/image/copyData.go @@ -1,4 +1,4 @@ -package main +package image import ( "encoding/json" @@ -16,6 +16,7 @@ import ( "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/libkpod/common" digest "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go/v1" ociv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -33,7 +34,8 @@ const ( OCIv1ImageManifest = v1.MediaTypeImageManifest ) -type containerImageData struct { +// CopyData stores the basic data used when copying a container or image +type CopyData struct { store storage.Store // Type is used to help identify a build container's metadata. It @@ -70,7 +72,7 @@ type containerImageData struct { Docker docker.V2Image `json:"docker,omitempty"` } -func (c *containerImageData) initConfig() { +func (c *CopyData) initConfig() { image := ociv1.Image{} dimage := docker.V2Image{} if len(c.Config) > 0 { @@ -114,7 +116,7 @@ func (c *containerImageData) initConfig() { c.fixupConfig() } -func (c *containerImageData) fixupConfig() { +func (c *CopyData) fixupConfig() { if c.Docker.Config != nil { // Prefer image-level settings over those from the container it was built from c.Docker.ContainerConfig = *c.Docker.Config @@ -141,39 +143,39 @@ func (c *containerImageData) fixupConfig() { // OS returns a name of the OS on which a container built using this image //is intended to be run. -func (c *containerImageData) OS() string { +func (c *CopyData) OS() string { return c.OCIv1.OS } // SetOS sets the name of the OS on which a container built using this image // is intended to be run. -func (c *containerImageData) SetOS(os string) { +func (c *CopyData) SetOS(os string) { c.OCIv1.OS = os c.Docker.OS = os } // Architecture returns a name of the architecture on which a container built // using this image is intended to be run. -func (c *containerImageData) Architecture() string { +func (c *CopyData) Architecture() string { return c.OCIv1.Architecture } // SetArchitecture sets the name of the architecture on which ta container built // using this image is intended to be run. -func (c *containerImageData) SetArchitecture(arch string) { +func (c *CopyData) SetArchitecture(arch string) { c.OCIv1.Architecture = arch c.Docker.Architecture = arch } // WorkDir returns the default working directory for running commands in a container // built using this image. -func (c *containerImageData) WorkDir() string { +func (c *CopyData) WorkDir() string { return c.OCIv1.Config.WorkingDir } // SetWorkDir sets the location of the default working directory for running commands // in a container built using this image. -func (c *containerImageData) SetWorkDir(there string) { +func (c *CopyData) SetWorkDir(there string) { c.OCIv1.Config.WorkingDir = there c.Docker.Config.WorkingDir = there } @@ -338,11 +340,13 @@ func makeDockerV2S1Image(manifest docker.V2S1Manifest) (docker.V2Image, error) { return dimage, nil } -func (c *containerImageData) Annotations() map[string]string { - return copyStringStringMap(c.ImageAnnotations) +// Annotations gets the anotations of the container or image +func (c *CopyData) Annotations() map[string]string { + return common.CopyStringStringMap(c.ImageAnnotations) } -func (c *containerImageData) Save() error { +// Save the CopyData to disk +func (c *CopyData) Save() error { buildstate, err := json.Marshal(c) if err != nil { return err @@ -355,13 +359,14 @@ func (c *containerImageData) Save() error { } -func openContainer(store storage.Store, name string) (*containerImageData, error) { - var data *containerImageData +// GetContainerCopyData gets the copy data for a container +func GetContainerCopyData(store storage.Store, name string) (*CopyData, error) { + var data *CopyData var err error if name != "" { - data, err = openContainerImageData(store, name) + data, err = openCopyData(store, name) if os.IsNotExist(err) { - data, err = importContainerImageData(store, name, "") + data, err = importCopyData(store, name, "") } } if err != nil { @@ -374,17 +379,18 @@ func openContainer(store storage.Store, name string) (*containerImageData, error } -func openImage(store storage.Store, image string) (*containerImageData, error) { +// GetImageCopyData gets the copy data for an image +func GetImageCopyData(store storage.Store, image string) (*CopyData, error) { if image == "" { return nil, errors.Errorf("image name must be specified") } - img, err := findImage(store, image) + img, err := FindImage(store, image) if err != nil { return nil, errors.Wrapf(err, "error locating image %q for importing settings", image) } - systemContext := getSystemContext("") - data, err := importContainerImageDataFromImage(store, systemContext, img.ID, "", "") + systemContext := common.GetSystemContext("") + data, err := ImportCopyDataFromImage(store, systemContext, img.ID, "", "") if err != nil { return nil, errors.Wrapf(err, "error reading image") } @@ -395,7 +401,7 @@ func openImage(store storage.Store, image string) (*containerImageData, error) { } -func importContainerImageData(store storage.Store, container, signaturePolicyPath string) (*containerImageData, error) { +func importCopyData(store storage.Store, container, signaturePolicyPath string) (*CopyData, error) { if container == "" { return nil, errors.Errorf("container name must be specified") } @@ -405,9 +411,9 @@ func importContainerImageData(store storage.Store, container, signaturePolicyPat return nil, err } - systemContext := getSystemContext(signaturePolicyPath) + systemContext := common.GetSystemContext(signaturePolicyPath) - data, err := importContainerImageDataFromImage(store, systemContext, c.ImageID, container, c.ID) + data, err := ImportCopyDataFromImage(store, systemContext, c.ImageID, container, c.ID) if err != nil { return nil, err } @@ -425,13 +431,13 @@ func importContainerImageData(store storage.Store, container, signaturePolicyPat err = data.Save() if err != nil { - return nil, errors.Wrapf(err, "error saving containerImageData state") + return nil, errors.Wrapf(err, "error saving CopyData state") } return data, nil } -func openContainerImageData(store storage.Store, container string) (*containerImageData, error) { +func openCopyData(store storage.Store, container string) (*CopyData, error) { cdir, err := store.ContainerDirectory(container) if err != nil { return nil, err @@ -440,7 +446,7 @@ func openContainerImageData(store storage.Store, container string) (*containerIm if err != nil { return nil, err } - c := &containerImageData{} + c := &CopyData{} err = json.Unmarshal(buildstate, &c) if err != nil { return nil, err @@ -454,7 +460,8 @@ func openContainerImageData(store storage.Store, container string) (*containerIm } -func importContainerImageDataFromImage(store storage.Store, systemContext *types.SystemContext, imageID, containerName, containerID string) (*containerImageData, 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) { manifest := []byte{} config := []byte{} imageName := "" @@ -484,7 +491,7 @@ func importContainerImageDataFromImage(store storage.Store, systemContext *types } } - data := &containerImageData{ + data := &CopyData{ store: store, Type: containerType, FromImage: imageName, @@ -503,7 +510,8 @@ func importContainerImageDataFromImage(store storage.Store, systemContext *types } -func (c *containerImageData) makeImageRef(manifestType string, compress archive.Compression, names []string, layerID string, historyTimestamp *time.Time) (types.ImageReference, error) { +// MakeImageRef converts a CopyData struct into a types.ImageReference +func (c *CopyData) MakeImageRef(manifestType string, compress archive.Compression, names []string, layerID string, historyTimestamp *time.Time) (types.ImageReference, error) { var name reference.Named if len(names) > 0 { if parsed, err := reference.ParseNamed(names[0]); err == nil { @@ -525,7 +533,7 @@ func (c *containerImageData) makeImageRef(manifestType string, compress archive. if historyTimestamp != nil { created = historyTimestamp.UTC() } - ref := &containerImageRef{ + ref := &CopyRef{ store: c.store, compression: compress, name: name, diff --git a/libkpod/image/image.go b/libkpod/image/image.go new file mode 100644 index 00000000..f2896d17 --- /dev/null +++ b/libkpod/image/image.go @@ -0,0 +1,283 @@ +package image + +import ( + "fmt" + "strings" + "time" + + is "github.com/containers/image/storage" + "github.com/containers/storage" + "github.com/kubernetes-incubator/cri-o/libkpod/common" + "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 { + params.beforeImage, _ = getCreatedTime(img) + } else { + return nil, fmt.Errorf("no such id: %s", pair[0]) + } + case "since": + if img, err := findImageInSlice(images, pair[1]); err == nil { + params.sinceImage, _ = getCreatedTime(img) + } 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 + } + if params.dangling != "" && !matchesDangling(name, params.dangling) { + return false + } else if params.label != "" && !matchesLabel(image, store, params.label) { + return false + } else if !params.beforeImage.IsZero() && !matchesBeforeImage(image, name, params) { + return false + } else if !params.sinceImage.IsZero() && !matchesSinceImage(image, 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(image storage.Image, store storage.Store, label string) bool { + storeRef, err := is.Transport.ParseStoreReference(store, "@"+image.ID) + if err != nil { + + } + img, err := storeRef.NewImage(nil) + if err != nil { + return false + } + info, err := img.Inspect() + if err != nil { + return false + } + + 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(image storage.Image, name string, params *FilterParams) bool { + if params.beforeImage.IsZero() { + return true + } + createdTime, err := getCreatedTime(image) + if err != nil { + return false + } + if createdTime.Before(params.beforeImage) { + return true + } + return false +} + +// Returns true if the image was created since the filter image. Returns +// false otherwise +func matchesSinceImage(image storage.Image, name string, params *FilterParams) bool { + if params.sinceImage.IsZero() { + return true + } + createdTime, err := getCreatedTime(image) + if err != nil { + return false + } + if createdTime.After(params.beforeImage) { + return true + } + return false +} + +// 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 true + } + 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 int64) string { + suffixes := [5]string{"B", "KB", "MB", "GB", "TB"} + + count := 0 + formattedSize := float64(size) + for formattedSize >= 1024 && count < 4 { + formattedSize /= 1024 + count++ + } + return fmt.Sprintf("%.4g %s", formattedSize, suffixes[count]) +} + +// FindImage searches for an 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 +} + +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") +} + +// Size returns the size of the image in the given store +func Size(store storage.Store, img storage.Image) (int64, error) { + is.Transport.SetStore(store) + storeRef, err := is.Transport.ParseStoreReference(store, "@"+img.ID) + if err != nil { + return -1, err + } + imgRef, err := storeRef.NewImage(nil) + if err != nil { + return -1, err + } + imgSize, err := imgRef.Size() + if err != nil { + return -1, err + } + return imgSize, nil +} + +// GetTopLayerID returns the ID of the top layer of the image +func GetTopLayerID(img storage.Image) (string, error) { + metadata, err := ParseMetadata(img) + if err != nil { + return "", err + } + // Get the digest of the first blob + digest := string(metadata.Blobs[0].Digest) + // Return the first layer associated with the given digest + return metadata.Layers[digest][0], nil +} + +func getCreatedTime(image storage.Image) (time.Time, error) { + metadata, err := ParseMetadata(image) + if err != nil { + return time.Time{}, err + } + return metadata.CreatedTime, 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 || (matchesFilter(store, image, name, filter) || MatchesReference(name, argName)) { + newImage := image + newImage.Names = []string{name} + filteredImages = append(filteredImages, newImage) + } + } + } + return filteredImages, nil +} diff --git a/cmd/kpod/imageData.go b/libkpod/image/imageData.go similarity index 81% rename from cmd/kpod/imageData.go rename to libkpod/image/imageData.go index 1419ecd9..eff4c484 100644 --- a/cmd/kpod/imageData.go +++ b/libkpod/image/imageData.go @@ -1,16 +1,19 @@ -package main +package image import ( "encoding/json" "time" "github.com/containers/storage" + "github.com/kubernetes-incubator/cri-o/libkpod/driver" digest "github.com/opencontainers/go-digest" ociv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) -type imageData struct { +// ImageData handles the data used when inspecting a container +// nolint +type ImageData struct { ID string Names []string Digests []digest.Digest @@ -25,7 +28,7 @@ type imageData struct { OS string Size uint VirtualSize uint - GraphDriver driverData + GraphDriver driver.Data RootFS ociv1.RootFS } @@ -55,13 +58,14 @@ type rootFS struct { Layers []string } -func getImageData(store storage.Store, name string) (*imageData, error) { - img, err := findImage(store, name) +// GetImageData gets the ImageData for a container with the given name in the given store. +func GetImageData(store storage.Store, name string) (*ImageData, error) { + img, err := FindImage(store, name) if err != nil { return nil, errors.Wrapf(err, "error reading image %q", name) } - cid, err := openImage(store, name) + cid, err := GetImageCopyData(store, name) if err != nil { return nil, errors.Wrapf(err, "error reading image %q", name) } @@ -94,16 +98,16 @@ func getImageData(store storage.Store, name string) (*imageData, error) { } } - driverName, err := getDriverName(store) + driverName, err := driver.GetDriverName(store) if err != nil { return nil, err } - topLayerID, err := getImageTopLayer(*img) + topLayerID, err := GetTopLayerID(*img) if err != nil { return nil, err } - driverMetadata, err := getDriverMetadata(store, topLayerID) + driverMetadata, err := driver.GetDriverMetadata(store, topLayerID) if err != nil { return nil, err } @@ -121,12 +125,12 @@ func getImageData(store storage.Store, name string) (*imageData, error) { return nil, err } - virtualSize, err := getImageSize(*img, store) + virtualSize, err := Size(store, *img) if err != nil { return nil, err } - return &imageData{ + return &ImageData{ ID: img.ID, Names: img.Names, Digests: digests, @@ -141,7 +145,7 @@ func getImageData(store storage.Store, name string) (*imageData, error) { OS: cid.OCIv1.OS, Size: uint(size), VirtualSize: uint(virtualSize), - GraphDriver: driverData{ + GraphDriver: driver.Data{ Name: driverName, Data: driverMetadata, }, @@ -150,7 +154,7 @@ func getImageData(store storage.Store, name string) (*imageData, error) { } func getDigests(img storage.Image) ([]digest.Digest, error) { - metadata, err := parseMetadata(img) + metadata, err := ParseMetadata(img) if err != nil { return nil, err } diff --git a/libkpod/image/metadata.go b/libkpod/image/metadata.go new file mode 100644 index 00000000..53a1cc47 --- /dev/null +++ b/libkpod/image/metadata.go @@ -0,0 +1,32 @@ +package image + +import ( + "encoding/json" + "strings" + "time" + + "github.com/containers/image/types" + "github.com/containers/storage" +) + +// Metadata stores all of the metadata for an image +type Metadata struct { + Tag string `json:"tag"` + CreatedTime time.Time `json:"created-time"` + ID string `json:"id"` + Blobs []types.BlobInfo `json:"blob-list"` + Layers map[string][]string `json:"layers"` + SignatureSizes []string `json:"signature-sizes"` +} + +// ParseMetadata takes an image, parses the json stored in it's metadata +// field, and converts it to a Metadata struct +func ParseMetadata(image storage.Image) (Metadata, error) { + var m Metadata + + dec := json.NewDecoder(strings.NewReader(image.Metadata)) + if err := dec.Decode(&m); err != nil { + return Metadata{}, err + } + return m, nil +} diff --git a/libkpod/image/rmi.go b/libkpod/image/rmi.go new file mode 100644 index 00000000..db17088d --- /dev/null +++ b/libkpod/image/rmi.go @@ -0,0 +1,44 @@ +package image + +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 name in the ImageStore + imgStore, err := store.ImageStore() + if err != nil { + return "", errors.Wrap(err, "could not untag image") + } + newNames := []string{} + removedName := "" + for _, name := range image.Names { + if MatchesReference(name, imgArg) { + removedName = name + continue + } + newNames = append(newNames, name) + } + imgStore.SetNames(image.ID, newNames) + err = imgStore.Save() + return removedName, err +} + +// RemoveImage removes the given image from storage +func RemoveImage(image *storage.Image, store storage.Store) (string, error) { + imgStore, err := store.ImageStore() + if err != nil { + return "", errors.Wrapf(err, "could not open image store") + } + err = imgStore.Delete(image.ID) + if err != nil { + return "", errors.Wrapf(err, "could not remove image") + } + err = imgStore.Save() + if err != nil { + return "", errors.Wrapf(err, "could not save image store") + } + return image.ID, nil +}