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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
cp "github.com/containers/image/copy"
|
cp "github.com/containers/image/copy"
|
||||||
is "github.com/containers/image/storage"
|
is "github.com/containers/image/storage"
|
||||||
|
@ -11,10 +15,21 @@ import (
|
||||||
"github.com/urfave/cli"
|
"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) {
|
func getStore(c *cli.Context) (storage.Store, error) {
|
||||||
options := storage.DefaultStoreOptions
|
options := storage.DefaultStoreOptions
|
||||||
if c.GlobalIsSet("root") || c.GlobalIsSet("runroot") {
|
if c.GlobalIsSet("root") {
|
||||||
options.GraphRoot = c.GlobalString("root")
|
options.GraphRoot = c.GlobalString("root")
|
||||||
|
}
|
||||||
|
if c.GlobalIsSet("runroot") {
|
||||||
options.RunRoot = c.GlobalString("runroot")
|
options.RunRoot = c.GlobalString("runroot")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,3 +82,34 @@ func getSystemContext(signaturePolicyPath string) *types.SystemContext {
|
||||||
}
|
}
|
||||||
return sc
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"flag"
|
"flag"
|
||||||
|
|
||||||
|
is "github.com/containers/image/storage"
|
||||||
|
"github.com/containers/storage"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetStore(t *testing.T) {
|
func TestGetStore(t *testing.T) {
|
||||||
u, err := user.Current()
|
// Make sure the tests are running as root
|
||||||
if err != nil {
|
failTestIfNotRoot(t)
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
set := flag.NewFlagSet("test", 0)
|
set := flag.NewFlagSet("test", 0)
|
||||||
globalSet := 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 := cli.NewContext(nil, set, globalCtx)
|
||||||
c.Command = command
|
c.Command = command
|
||||||
|
|
||||||
_, err = getStore(c)
|
_, err := getStore(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
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.Version = Version
|
||||||
|
|
||||||
app.Commands = []cli.Command{
|
app.Commands = []cli.Command{
|
||||||
|
imagesCommand,
|
||||||
launchCommand,
|
launchCommand,
|
||||||
|
rmiCommand,
|
||||||
tagCommand,
|
tagCommand,
|
||||||
versionCommand,
|
versionCommand,
|
||||||
pullCommand,
|
pullCommand,
|
||||||
|
@ -45,6 +47,24 @@ func main() {
|
||||||
Usage: "used to pass an option to the storage driver",
|
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 {
|
if err := app.Run(os.Args); err != nil {
|
||||||
logrus.Fatal(err)
|
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})}
|
: ${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_() {
|
_complete_() {
|
||||||
local options_with_args=$1
|
local options_with_args=$1
|
||||||
local boolean_options="$2 -h --help"
|
local boolean_options="$2 -h --help"
|
||||||
|
@ -19,6 +50,24 @@ _complete_() {
|
||||||
esac
|
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() {
|
_kpod_launch() {
|
||||||
local options_with_args="
|
local options_with_args="
|
||||||
"
|
"
|
||||||
|
@ -60,7 +109,9 @@ _kpod_kpod() {
|
||||||
--help -h
|
--help -h
|
||||||
"
|
"
|
||||||
commands="
|
commands="
|
||||||
|
images
|
||||||
launch
|
launch
|
||||||
|
rmi
|
||||||
tag
|
tag
|
||||||
version
|
version
|
||||||
pull
|
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
|
# COMMANDS
|
||||||
|
|
||||||
|
## images
|
||||||
|
List images in local storage
|
||||||
|
|
||||||
## launch
|
## launch
|
||||||
Launch a pod
|
Launch a pod
|
||||||
|
|
||||||
|
## rmi
|
||||||
|
Removes one or more locally stored images
|
||||||
|
|
||||||
## tag
|
## tag
|
||||||
Add one or more additional names to locally-stored image
|
Add one or more additional names to locally-stored image
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue