Add 'kpod images' and 'kpod rmi' commands
'kpod images' lists all images on a system. 'kpod rmi' removes one or more images from a system. The images will not be removed if they are associated with a running container, unless the -f option is used Signed-off-by: Ryan Cole <rcyoalne@gmail.com>
This commit is contained in:
parent
7fb772b7d1
commit
a040f20a76
11 changed files with 1708 additions and 8 deletions
|
@ -1,7 +1,11 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cp "github.com/containers/image/copy"
|
||||
is "github.com/containers/image/storage"
|
||||
|
@ -11,10 +15,21 @@ import (
|
|||
"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"`
|
||||
}
|
||||
|
||||
func getStore(c *cli.Context) (storage.Store, error) {
|
||||
options := storage.DefaultStoreOptions
|
||||
if c.GlobalIsSet("root") || c.GlobalIsSet("runroot") {
|
||||
if c.GlobalIsSet("root") {
|
||||
options.GraphRoot = c.GlobalString("root")
|
||||
}
|
||||
if c.GlobalIsSet("runroot") {
|
||||
options.RunRoot = c.GlobalString("runroot")
|
||||
}
|
||||
|
||||
|
@ -67,3 +82,34 @@ func getSystemContext(signaturePolicyPath string) *types.SystemContext {
|
|||
}
|
||||
return sc
|
||||
}
|
||||
|
||||
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 getSize(image storage.Image, store storage.Store) (int64, error) {
|
||||
|
||||
is.Transport.SetStore(store)
|
||||
storeRef, err := is.Transport.ParseStoreReference(store, "@"+image.ID)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return -1, err
|
||||
}
|
||||
img, err := storeRef.NewImage(nil)
|
||||
if err != nil {
|
||||
fmt.Println("Error with NewImage")
|
||||
return -1, err
|
||||
}
|
||||
imgSize, err := img.Size()
|
||||
if err != nil {
|
||||
fmt.Println("Error getting size")
|
||||
return -1, err
|
||||
}
|
||||
return imgSize, nil
|
||||
}
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"testing"
|
||||
|
||||
"flag"
|
||||
|
||||
is "github.com/containers/image/storage"
|
||||
"github.com/containers/storage"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func TestGetStore(t *testing.T) {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
t.Log("Could not determine user. Running as root may cause tests to fail")
|
||||
} else if u.Uid != "0" {
|
||||
t.Fatal("tests will fail unless run as root")
|
||||
}
|
||||
// Make sure the tests are running as root
|
||||
failTestIfNotRoot(t)
|
||||
|
||||
set := flag.NewFlagSet("test", 0)
|
||||
globalSet := flag.NewFlagSet("test", 0)
|
||||
|
@ -25,8 +24,72 @@ func TestGetStore(t *testing.T) {
|
|||
c := cli.NewContext(nil, set, globalCtx)
|
||||
c.Command = command
|
||||
|
||||
_, err = getStore(c)
|
||||
_, err := getStore(c)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
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 = getSize(images[0], store)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func failTestIfNotRoot(t *testing.T) {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
t.Log("Could not determine user. Running without root may cause tests to fail")
|
||||
} else if u.Uid != "0" {
|
||||
t.Fatal("tests will fail unless run as root")
|
||||
}
|
||||
}
|
||||
|
||||
func pullTestImage(name string) error {
|
||||
cmd := exec.Command("crioctl", "image", "pull", name)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
378
cmd/kpod/images.go
Normal file
378
cmd/kpod/images.go
Normal file
|
@ -0,0 +1,378 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
is "github.com/containers/image/storage"
|
||||
"github.com/containers/storage"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type imageOutputParams struct {
|
||||
ID string
|
||||
Name string
|
||||
Digest string
|
||||
CreatedAt string
|
||||
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{
|
||||
Name: "quiet, q",
|
||||
Usage: "display only image IDs",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "noheading, n",
|
||||
Usage: "do not print column headings",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-trunc, notruncate",
|
||||
Usage: "do not truncate output",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "digests",
|
||||
Usage: "show digests",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "format",
|
||||
Usage: "pretty-print images using a Go template. will override --quiet",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "filter, f",
|
||||
Usage: "filter output based on conditions provided (default [])",
|
||||
},
|
||||
}
|
||||
|
||||
imagesDescription = "lists locally stored images."
|
||||
imagesCommand = cli.Command{
|
||||
Name: "images",
|
||||
Usage: "list images in local storage",
|
||||
Description: imagesDescription,
|
||||
Flags: imagesFlags,
|
||||
Action: imagesCmd,
|
||||
ArgsUsage: "",
|
||||
}
|
||||
)
|
||||
|
||||
func imagesCmd(c *cli.Context) error {
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
quiet := false
|
||||
if c.IsSet("quiet") {
|
||||
quiet = c.Bool("quiet")
|
||||
}
|
||||
noheading := false
|
||||
if c.IsSet("noheading") {
|
||||
noheading = c.Bool("noheading")
|
||||
}
|
||||
truncate := true
|
||||
if c.IsSet("no-trunc") {
|
||||
truncate = !c.Bool("no-trunc")
|
||||
}
|
||||
digests := false
|
||||
if c.IsSet("digests") {
|
||||
digests = c.Bool("digests")
|
||||
}
|
||||
formatString := ""
|
||||
hasTemplate := false
|
||||
if c.IsSet("format") {
|
||||
formatString = c.String("format")
|
||||
hasTemplate = true
|
||||
}
|
||||
|
||||
name := ""
|
||||
if len(c.Args()) == 1 {
|
||||
name = c.Args().Get(0)
|
||||
} else if len(c.Args()) > 1 {
|
||||
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
|
||||
if c.IsSet("filter") {
|
||||
params, err = parseFilter(images, c.String("filter"))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing filter")
|
||||
}
|
||||
} else {
|
||||
params = nil
|
||||
}
|
||||
|
||||
if len(images) > 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 pair[1] == "true" || pair[1] == "false" {
|
||||
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
|
||||
}
|
||||
|
||||
func outputHeader(truncate, digests bool) {
|
||||
if truncate {
|
||||
fmt.Printf("%-20s %-56s ", "IMAGE ID", "IMAGE NAME")
|
||||
} else {
|
||||
fmt.Printf("%-64s %-56s ", "IMAGE ID", "IMAGE NAME")
|
||||
}
|
||||
|
||||
if digests {
|
||||
fmt.Printf("%-64s ", "DIGEST")
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
createdTime := imageMetadata.CreatedTime.Format("Jan 2, 2006 15:04")
|
||||
digest := ""
|
||||
if len(imageMetadata.Blobs) > 0 {
|
||||
digest = string(imageMetadata.Blobs[0].Digest)
|
||||
}
|
||||
size, _ := getSize(image, store)
|
||||
|
||||
names := []string{""}
|
||||
if len(image.Names) > 0 {
|
||||
names = image.Names
|
||||
} else {
|
||||
// images without names should be printed with "<none>" as the image name
|
||||
names = append(names, "<none>")
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 dangling == "false" && name != "<none>" {
|
||||
return true
|
||||
} else if dangling == "true" && name == "<none>" {
|
||||
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 {
|
||||
return errors.Wrapf(err, "Template parsing error")
|
||||
}
|
||||
|
||||
err = tmpl.Execute(os.Stdout, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println()
|
||||
return nil
|
||||
}
|
||||
|
||||
func outputUsingFormatString(truncate, digests bool, params imageOutputParams) {
|
||||
if truncate {
|
||||
fmt.Printf("%-20.12s %-56s", params.ID, params.Name)
|
||||
} else {
|
||||
fmt.Printf("%-64s %-56s", params.ID, params.Name)
|
||||
}
|
||||
|
||||
if digests {
|
||||
fmt.Printf(" %-64s", params.Digest)
|
||||
}
|
||||
fmt.Printf(" %-22s %s\n", params.CreatedAt, params.Size)
|
||||
}
|
678
cmd/kpod/images_test.go
Normal file
678
cmd/kpod/images_test.go
Normal file
|
@ -0,0 +1,678 @@
|
|||
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("<none>", "true") {
|
||||
t.Error("matchesDangling() should return true with dangling=true and name=<none>")
|
||||
}
|
||||
|
||||
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("<none>", "false") {
|
||||
t.Error("matchesDangling() should return false with dangling=false and name=<none>")
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
|
@ -22,7 +22,9 @@ func main() {
|
|||
app.Version = Version
|
||||
|
||||
app.Commands = []cli.Command{
|
||||
imagesCommand,
|
||||
launchCommand,
|
||||
rmiCommand,
|
||||
tagCommand,
|
||||
versionCommand,
|
||||
pullCommand,
|
||||
|
@ -45,6 +47,24 @@ func main() {
|
|||
Usage: "used to pass an option to the storage driver",
|
||||
},
|
||||
}
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "root",
|
||||
Usage: "path to the root directory in which data, including images, is stored",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "runroot",
|
||||
Usage: "path to the 'run directory' where all state information is stored",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "storage-driver, s",
|
||||
Usage: "select which storage driver is used to manage storage of images and containers (default is overlay2)",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "storage-opt",
|
||||
Usage: "used to pass an option to the storage driver",
|
||||
},
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
logrus.Fatal(err)
|
||||
|
|
238
cmd/kpod/rmi.go
Normal file
238
cmd/kpod/rmi.go
Normal file
|
@ -0,0 +1,238 @@
|
|||
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"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
rmiDescription = "removes one or more locally stored images."
|
||||
rmiFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "force, f",
|
||||
Usage: "force removal of the image",
|
||||
},
|
||||
}
|
||||
rmiCommand = cli.Command{
|
||||
Name: "rmi",
|
||||
Usage: "removes one or more images from local storage",
|
||||
Description: rmiDescription,
|
||||
Action: rmiCmd,
|
||||
ArgsUsage: "IMAGE-NAME-OR-ID [...]",
|
||||
Flags: rmiFlags,
|
||||
}
|
||||
)
|
||||
|
||||
func rmiCmd(c *cli.Context) error {
|
||||
|
||||
force := false
|
||||
if c.IsSet("force") {
|
||||
force = c.Bool("force")
|
||||
}
|
||||
|
||||
args := c.Args()
|
||||
if len(args) == 0 {
|
||||
return errors.Errorf("image name or ID must be specified")
|
||||
}
|
||||
|
||||
store, err := getStore(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range args {
|
||||
image, err := getImage(id, store)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not get image %q", id)
|
||||
}
|
||||
if image != nil {
|
||||
ctrIDs, err := runningContainers(image, store)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error getting running containers for image %q", id)
|
||||
}
|
||||
if len(ctrIDs) > 0 && len(image.Names) <= 1 {
|
||||
if force {
|
||||
removeContainers(ctrIDs, store)
|
||||
} else {
|
||||
for ctrID := range ctrIDs {
|
||||
return fmt.Errorf("Could not remove image %q (must force) - container %q is using its reference image", id, ctrID)
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the user supplied an ID, we cannot delete the image if it is referred to by multiple tags
|
||||
if 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)
|
||||
if err2 != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("untagged: %s", name)
|
||||
}
|
||||
|
||||
if len(image.Names) > 0 {
|
||||
continue
|
||||
}
|
||||
id, err := removeImage(image, store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s\n", id)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
func runningContainers(image *storage.Image, store storage.Store) ([]string, error) {
|
||||
ctrIDs := []string{}
|
||||
ctrStore, err := store.ContainerStore()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containers, err := ctrStore.Containers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ctr := range containers {
|
||||
if ctr.ImageID == image.ID {
|
||||
ctrIDs = append(ctrIDs, ctr.ID)
|
||||
}
|
||||
}
|
||||
return ctrIDs, nil
|
||||
}
|
||||
|
||||
func removeContainers(ctrIDs []string, store storage.Store) error {
|
||||
ctrStore, err := store.ContainerStore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ctrID := range ctrIDs {
|
||||
if err = ctrStore.Delete(ctrID); err != nil {
|
||||
return errors.Wrapf(err, "could not remove container %q", ctrID)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
145
cmd/kpod/rmi_test.go
Normal file
145
cmd/kpod/rmi_test.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
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")
|
||||
}
|
||||
}
|
|
@ -2,6 +2,37 @@
|
|||
|
||||
: ${PROG:=$(basename ${BASH_SOURCE})}
|
||||
|
||||
__kpod_list_images() {
|
||||
COMPREPLY=($(compgen -W "$(kpod images -q)" -- $cur))
|
||||
}
|
||||
|
||||
_kpod_images() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
--quiet
|
||||
-q
|
||||
--noheading
|
||||
-n
|
||||
--no-trunc
|
||||
--digests
|
||||
--format
|
||||
--filter
|
||||
-f
|
||||
"
|
||||
|
||||
local options_with_args="
|
||||
"
|
||||
|
||||
local all_options="$options_with_args $boolean_options"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_complete_() {
|
||||
local options_with_args=$1
|
||||
local boolean_options="$2 -h --help"
|
||||
|
@ -19,6 +50,24 @@ _complete_() {
|
|||
esac
|
||||
}
|
||||
|
||||
_kpod_rmi() {
|
||||
local boolean_options="
|
||||
--help
|
||||
-h
|
||||
--force
|
||||
-f
|
||||
"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
|
||||
;;
|
||||
*)
|
||||
__kpod_list_images
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_kpod_launch() {
|
||||
local options_with_args="
|
||||
"
|
||||
|
@ -60,7 +109,9 @@ _kpod_kpod() {
|
|||
--help -h
|
||||
"
|
||||
commands="
|
||||
images
|
||||
launch
|
||||
rmi
|
||||
tag
|
||||
version
|
||||
pull
|
||||
|
|
48
docs/kpod-images.1.md
Normal file
48
docs/kpod-images.1.md
Normal file
|
@ -0,0 +1,48 @@
|
|||
## kpod-images "1" "March 2017" "kpod"
|
||||
|
||||
## NAME
|
||||
kpod images - List images in local storage.
|
||||
|
||||
## SYNOPSIS
|
||||
**kpod** **images** [*options* [...]]
|
||||
|
||||
## DESCRIPTION
|
||||
Displays locally stored images, their names, and their IDs.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**--digests**
|
||||
|
||||
Show image digests
|
||||
|
||||
**--filter, -f=[]**
|
||||
|
||||
Filter output based on conditions provided (default [])
|
||||
|
||||
**--format="TEMPLATE"**
|
||||
|
||||
Pretty-print images using a Go template. Will override --quiet
|
||||
|
||||
**--noheading, -n**
|
||||
|
||||
Omit the table headings from the listing of images.
|
||||
|
||||
**--no-trunc, --notruncate**
|
||||
|
||||
Do not truncate output.
|
||||
|
||||
**--quiet, -q**
|
||||
|
||||
Lists only the image IDs.
|
||||
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
kpod images
|
||||
|
||||
kpod images --quiet
|
||||
|
||||
kpod images -q --noheading --notruncate
|
||||
|
||||
## SEE ALSO
|
||||
kpod(1)
|
27
docs/kpod-rmi.1.md
Normal file
27
docs/kpod-rmi.1.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
## kpod-rmi "1" "March 2017" "kpod"
|
||||
|
||||
## NAME
|
||||
kpod rmi - Removes one or more images.
|
||||
|
||||
## SYNOPSIS
|
||||
**kpod** **rmi** **imageID [...]**
|
||||
|
||||
## DESCRIPTION
|
||||
Removes one or more locally stored images.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**--force, -f**
|
||||
|
||||
Executing this command will stop all containers that are using the image and remove them from the system
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
kpod rmi imageID
|
||||
|
||||
kpod rmi --force imageID
|
||||
|
||||
kpod rmi imageID1 imageID2 imageID3
|
||||
|
||||
## SEE ALSO
|
||||
kpod(1)
|
|
@ -29,9 +29,15 @@ has the capability to debug pods/images created by crio.
|
|||
|
||||
# COMMANDS
|
||||
|
||||
## images
|
||||
List images in local storage
|
||||
|
||||
## launch
|
||||
Launch a pod
|
||||
|
||||
## rmi
|
||||
Removes one or more locally stored images
|
||||
|
||||
## tag
|
||||
Add one or more additional names to locally-stored image
|
||||
|
||||
|
|
Loading…
Reference in a new issue