diff --git a/cmd/buildah/images.go b/cmd/buildah/images.go index 361d7a9d16c..7c894fa26cc 100644 --- a/cmd/buildah/images.go +++ b/cmd/buildah/images.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "regexp" + "sort" "strings" "text/template" "time" @@ -13,7 +14,9 @@ import ( "github.com/containers/buildah/imagebuildah" buildahcli "github.com/containers/buildah/pkg/cli" is "github.com/containers/image/storage" + "github.com/containers/libpod/cmd/podman/formats" "github.com/containers/storage" + units "github.com/docker/go-units" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -59,6 +62,14 @@ type imageResults struct { filter string } +var imagesHeader = map[string]string{ + "Name": "REPOSITORY", + "Tag": "TAG", + "ID": "IMAGE ID", + "CreatedAt": "CREATED", + "Size": "SIZE", +} + func init() { var ( opts imageResults @@ -143,10 +154,6 @@ func imagesCmd(c *cobra.Command, args []string, iopts *imageResults) error { } } - if len(images) > 0 && !opts.noHeading && !opts.quiet && opts.format == "" && !opts.json { - outputHeader(opts.truncate, opts.digests) - } - return outputImages(ctx, images, store, params, name, opts) } @@ -213,26 +220,33 @@ func setFilterDate(ctx context.Context, store storage.Store, images []storage.Im return time.Time{}, fmt.Errorf("Could not locate image %q", imgName) } -func outputHeader(truncate, digests bool) { - if truncate { - fmt.Printf("%-56s %-20s %-20s ", "IMAGE NAME", "IMAGE TAG", "IMAGE ID") - } else { - fmt.Printf("%-56s %-20s %-64s ", "IMAGE NAME", "IMAGE TAG", "IMAGE ID") +func outputHeader(opts imageOptions) string { + if opts.format != "" { + return strings.Replace(opts.format, `\t`, "\t", -1) } - - if digests { - fmt.Printf("%-71s ", "DIGEST") + if opts.quiet { + return formats.IDString + } + format := "table {{.Name}}\t{{.Tag}}\t" + if opts.noHeading { + format = "{{.Name}}\t{{.Tag}}\t" } - fmt.Printf("%-22s %s\n", "CREATED AT", "SIZE") + if opts.digests { + format += "{{.Digest}}\t" + } + format += "{{.ID}}\t{{.CreatedAt}}\t{{.Size}}" + return format } +type imagesSorted []imageOutputParams + func outputImages(ctx context.Context, images []storage.Image, store storage.Store, filters *filterParams, argName string, opts imageOptions) error { found := false + var imagesParams imagesSorted jsonImages := []jsonImage{} for _, image := range images { createdTime := image.Created - inspectedTime, digest, size, _ := getDateAndDigestAndSize(ctx, image, store) if !inspectedTime.IsZero() { if createdTime != inspectedTime { @@ -255,6 +269,11 @@ func outputImages(ctx context.Context, images []storage.Image, store storage.Sto } } + imageID := "sha256:" + image.ID + if opts.truncate { + imageID = shortID(image.ID) + } + names := []string{} if len(image.Names) > 0 { names = image.Names @@ -274,11 +293,6 @@ func outputImages(ctx context.Context, images []storage.Image, store storage.Sto if !matchesFilter(ctx, image, store, name+":"+tag, filters) { continue } - if opts.quiet { - fmt.Printf("%-64s\n", image.ID) - // We only want to print each id once - break outer - } if opts.json { jsonImages = append(jsonImages, jsonImage{ID: image.ID, Names: image.Names}) // We only want to print each id once @@ -286,20 +300,18 @@ func outputImages(ctx context.Context, images []storage.Image, store storage.Sto } params := imageOutputParams{ Tag: tag, - ID: image.ID, + ID: imageID, Name: name, Digest: digest, - CreatedAt: createdTime.Format("Jan 2, 2006 15:04"), - Size: formattedSize(size), CreatedAtRaw: createdTime, + CreatedAt: units.HumanDuration(time.Since((createdTime))) + " ago", + Size: formattedSize(size), } - if opts.format != "" { - if err := outputUsingTemplate(opts.format, params); err != nil { - return err - } - continue + imagesParams = append(imagesParams, params) + if opts.quiet { + // We only want to print each id once + break outer } - outputUsingFormatString(opts.truncate, opts.digests, params) } } } @@ -313,11 +325,42 @@ func outputImages(ctx context.Context, images []storage.Image, store storage.Sto return err } fmt.Printf("%s\n", data) + return nil } - + imagesParams = sortImagesOutput(imagesParams) + out := formats.StdoutTemplateArray{Output: imagesToGeneric(imagesParams), Template: outputHeader(opts), Fields: imagesHeader} + formats.Writer(out).Out() return nil } +func shortID(id string) string { + idTruncLength := 12 + if len(id) > idTruncLength { + return id[:idTruncLength] + } + return id +} + +func sortImagesOutput(imagesOutput imagesSorted) imagesSorted { + sort.Sort(imagesOutput) + return imagesOutput +} + +func (a imagesSorted) Less(i, j int) bool { + return a[i].CreatedAtRaw.After(a[j].CreatedAtRaw) +} +func (a imagesSorted) Len() int { return len(a) } +func (a imagesSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +func imagesToGeneric(templParams []imageOutputParams) (genericParams []interface{}) { + if len(templParams) > 0 { + for _, v := range templParams { + genericParams = append(genericParams, interface{}(v)) + } + } + return genericParams +} + func matchesFilter(ctx context.Context, image storage.Image, store storage.Store, name string, params *filterParams) bool { if params == nil { return true diff --git a/cmd/buildah/images_test.go b/cmd/buildah/images_test.go index 0fc76d7c95e..a1d0c1a81ea 100644 --- a/cmd/buildah/images_test.go +++ b/cmd/buildah/images_test.go @@ -113,40 +113,6 @@ func TestSizeFormatting(t *testing.T) { } } -func TestOutputHeader(t *testing.T) { - output := captureOutput(func() { - outputHeader(true, false) - }) - expectedOutput := fmt.Sprintf("%-56s %-20s %-20s %-22s %s\n", "IMAGE NAME", "IMAGE TAG", "IMAGE ID", "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("%-56s %-20s %-20s %-71s %-22s %s\n", "IMAGE NAME", "IMAGE TAG", "IMAGE ID", "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("%-56s %-20s %-64s %-22s %s\n", "IMAGE NAME", "IMAGE TAG", "IMAGE ID", "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, true) - }) - expectedOutput = fmt.Sprintf("%-56s %-20s %-64s %-71s %-22s %s\n", "IMAGE NAME", "IMAGE TAG", "IMAGE ID", "DIGEST", "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 { @@ -195,45 +161,6 @@ func TestNoMatchesReferenceWithoutTag(t *testing.T) { } } -func TestOutputImagesQuietTruncated(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - opts := imageOptions{ - truncate: true, - quiet: true, - } - - store, err := storage.GetStore(storeOptions) - 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 that we know we have at least one - _, err = pullTestImage(t, "busybox:latest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - - // Tests quiet and truncated output - output, err := captureOutputWithError(func() error { - return outputImages(getContext(), images[:1], store, nil, "", opts) - }) - 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) @@ -263,7 +190,7 @@ func TestOutputImagesQuietNotTruncated(t *testing.T) { output, err := captureOutputWithError(func() error { return outputImages(getContext(), images[:1], store, nil, "", opts) }) - expectedOutput := fmt.Sprintf("%-64s\n", images[0].ID) + expectedOutput := fmt.Sprintf("sha256:%s\n", images[0].ID) if err != nil { t.Error("quiet/non-truncated output produces error") } else if strings.TrimSpace(output) != strings.TrimSpace(expectedOutput) { @@ -304,48 +231,11 @@ func TestOutputImagesFormatString(t *testing.T) { expectedOutput := images[0].ID if err != nil { t.Error("format string output produces error") - } else if strings.TrimSpace(output) != strings.TrimSpace(expectedOutput) { + } else if !strings.Contains(expectedOutput, strings.TrimSpace(output)) { 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) - - opts := imageOptions{ - quiet: true, - } - store, err := storage.GetStore(storeOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - - // Pull an image so that we know we have at least one - _, err = pullTestImage(t, "busybox:latest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - - 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(getContext(), images[:1], store, nil, "", opts) - }) - 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) @@ -382,48 +272,6 @@ func TestOutputImagesArgNoMatch(t *testing.T) { } } -func TestOutputMultipleImages(t *testing.T) { - // Make sure the tests are running as root - failTestIfNotRoot(t) - - opts := imageOptions{ - quiet: true, - truncate: true, - } - store, err := storage.GetStore(storeOptions) - if err != nil { - t.Fatal(err) - } else if store != nil { - is.Transport.SetStore(store) - } - - // Pull two images so that we know we have at least two - _, err = pullTestImage(t, "busybox:latest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - _, err = pullTestImage(t, "alpine:latest") - if err != nil { - t.Fatalf("could not pull image to remove: %v", err) - } - - 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(getContext(), images[:2], store, nil, "", opts) - }) - 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) diff --git a/cmd/buildah/rmi_test.go b/cmd/buildah/rmi_test.go index cbf023a064c..ec6f2f2a817 100644 --- a/cmd/buildah/rmi_test.go +++ b/cmd/buildah/rmi_test.go @@ -106,13 +106,13 @@ func TestStorageImageIDTrue(t *testing.T) { if err != nil { t.Fatalf("Error reading images: %v", err) } - id, err := captureOutputWithError(func() error { - return outputImages(getContext(), images, store, nil, "busybox:latest", opts) - }) - if err != nil { - t.Fatalf("Error getting id of image: %v", err) + var id string + if len(images) > 0 { + id = strings.TrimSpace(images[0].ID) + } + if id == "" { + t.Fatalf("Error getting image id") } - id = strings.TrimSpace(id) imgRef, err := storageImageID(getContext(), store, id) if err != nil { diff --git a/tests/images.bats b/tests/images.bats index e1271cd17b8..7c867b7667a 100755 --- a/tests/images.bats +++ b/tests/images.bats @@ -12,7 +12,7 @@ load helpers run buildah images img1 --filter="service=redis" img2 check_options_flag_err "--filter=service=redis" - run buildah images img1 img2 img3 -q + run buildah images img1 img2 img3 -q check_options_flag_err "-q" } @@ -83,13 +83,25 @@ load helpers buildah rmi -a -f } +@test "images no-trunc test" { + cid1=$(buildah from --pull --signature-policy ${TESTSDIR}/policy.json alpine) + cid2=$(buildah from --pull --signature-policy ${TESTSDIR}/policy.json busybox) + run buildah --debug=false images -q --no-trunc + [ $(wc -l <<< "$output") -eq 2 ] + echo $output + [[ ${lines[0]} =~ "sha256" ]] + [ "${status}" -eq 0 ] + buildah rm -a + buildah rmi -a -f +} + @test "images json test" { cid1=$(buildah from --pull --signature-policy ${TESTSDIR}/policy.json alpine) cid2=$(buildah from --pull --signature-policy ${TESTSDIR}/policy.json busybox) run buildah --debug=false images --json [ $(wc -l <<< "$output") -eq 14 ] [ "${status}" -eq 0 ] - + run buildah --debug=false images --json alpine [ $(wc -l <<< "$output") -eq 8 ] [ "${status}" -eq 0 ] @@ -145,7 +157,7 @@ load helpers cid=$(buildah from --pull --signature-policy ${TESTSDIR}/policy.json scratch) buildah commit --signature-policy ${TESTSDIR}/policy.json $cid test buildah commit --signature-policy ${TESTSDIR}/policy.json $cid test - run buildah --debug=false images + run buildah --debug=false images [ $(wc -l <<< "$output") -eq 3 ] [ "${status}" -eq 0 ] run buildah --debug=false images --filter dangling=true diff --git a/vendor/github.com/containers/libpod/cmd/podman/README.md b/vendor/github.com/containers/libpod/cmd/podman/README.md new file mode 100644 index 00000000000..0fee7eafa4b --- /dev/null +++ b/vendor/github.com/containers/libpod/cmd/podman/README.md @@ -0,0 +1,15 @@ +# podman - Simple debugging tool for pods and images +podman is a daemonless container runtime for managing containers, pods, and container images. +It is intended as a counterpart to CRI-O, to provide low-level debugging not available through the CRI interface used by Kubernetes. +It can also act as a container runtime independent of CRI-O, creating and managing its own set of containers. + +## Use cases +1. Create containers +2. Start, stop, signal, attach to, and inspect existing containers +3. Run new commands in existing containers +4. Push and pull images +5. List and inspect existing images +6. Create new images by committing changes within a container +7. Create pods +8. Start, stop, signal, and inspect existing pods +9. Populate pods with containers diff --git a/vendor/github.com/containers/libpod/cmd/podman/formats/formats.go b/vendor/github.com/containers/libpod/cmd/podman/formats/formats.go new file mode 100644 index 00000000000..c454c39bdb8 --- /dev/null +++ b/vendor/github.com/containers/libpod/cmd/podman/formats/formats.go @@ -0,0 +1,174 @@ +package formats + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + "text/template" + + "github.com/ghodss/yaml" + "github.com/pkg/errors" + "golang.org/x/crypto/ssh/terminal" +) + +const ( + // JSONString const to save on duplicate variable names + JSONString = "json" + // IDString const to save on duplicates for Go templates + IDString = "{{.ID}}" + + parsingErrorStr = "Template parsing error" +) + +// Writer interface for outputs +type Writer interface { + Out() error +} + +// JSONStructArray for JSON output +type JSONStructArray struct { + Output []interface{} +} + +// StdoutTemplateArray for Go template output +type StdoutTemplateArray struct { + Output []interface{} + Template string + Fields map[string]string +} + +// JSONStruct for JSON output +type JSONStruct struct { + Output interface{} +} + +// StdoutTemplate for Go template output +type StdoutTemplate struct { + Output interface{} + Template string + Fields map[string]string +} + +// YAMLStruct for YAML output +type YAMLStruct struct { + Output interface{} +} + +func setJSONFormatEncoder(isTerminal bool, w io.Writer) *json.Encoder { + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + if isTerminal { + enc.SetEscapeHTML(false) + } + return enc +} + +// Out method for JSON Arrays +func (j JSONStructArray) Out() error { + buf := bytes.NewBuffer(nil) + enc := setJSONFormatEncoder(terminal.IsTerminal(int(os.Stdout.Fd())), buf) + if err := enc.Encode(j.Output); err != nil { + return err + } + data := buf.Bytes() + + // JSON returns a byte array with a literal null [110 117 108 108] in it + // if it is passed empty data. We used bytes.Compare to see if that is + // the case. + if diff := bytes.Compare(data, []byte("null")); diff == 0 { + data = []byte("[]") + } + + // If the we did get NULL back, we should spit out {} which is + // at least valid JSON for the consumer. + fmt.Printf("%s", data) + humanNewLine() + return nil +} + +// Out method for Go templates +func (t StdoutTemplateArray) Out() error { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + if strings.HasPrefix(t.Template, "table") { + // replace any spaces with tabs in template so that tabwriter can align it + t.Template = strings.Replace(strings.TrimSpace(t.Template[5:]), " ", "\t", -1) + headerTmpl, err := template.New("header").Funcs(headerFunctions).Parse(t.Template) + if err != nil { + return errors.Wrapf(err, parsingErrorStr) + } + err = headerTmpl.Execute(w, t.Fields) + if err != nil { + return err + } + fmt.Fprintln(w, "") + } + t.Template = strings.Replace(t.Template, " ", "\t", -1) + tmpl, err := template.New("image").Funcs(basicFunctions).Parse(t.Template) + if err != nil { + return errors.Wrapf(err, parsingErrorStr) + } + for i, raw := range t.Output { + basicTmpl := tmpl.Funcs(basicFunctions) + if err := basicTmpl.Execute(w, raw); err != nil { + return errors.Wrapf(err, parsingErrorStr) + } + if i != len(t.Output)-1 { + fmt.Fprintln(w, "") + continue + } + // Only print new line at the end of the output if stdout is the terminal + if terminal.IsTerminal(int(os.Stdout.Fd())) { + fmt.Fprintln(w, "") + } + } + return w.Flush() +} + +// Out method for JSON struct +func (j JSONStruct) Out() error { + data, err := json.MarshalIndent(j.Output, "", " ") + if err != nil { + return err + } + fmt.Printf("%s", data) + humanNewLine() + return nil +} + +//Out method for Go templates +func (t StdoutTemplate) Out() error { + tmpl, err := template.New("image").Parse(t.Template) + if err != nil { + return errors.Wrapf(err, "template parsing error") + } + err = tmpl.Execute(os.Stdout, t.Output) + if err != nil { + return err + } + humanNewLine() + return nil +} + +// Out method for YAML +func (y YAMLStruct) Out() error { + var buf []byte + var err error + buf, err = yaml.Marshal(y.Output) + if err != nil { + return err + } + fmt.Printf("%s", string(buf)) + humanNewLine() + return nil +} + +// humanNewLine prints a new line at the end of the output only if stdout is the terminal +func humanNewLine() { + if terminal.IsTerminal(int(os.Stdout.Fd())) { + fmt.Println() + } +} diff --git a/vendor/github.com/containers/libpod/cmd/podman/formats/templates.go b/vendor/github.com/containers/libpod/cmd/podman/formats/templates.go new file mode 100644 index 00000000000..c2582552a3f --- /dev/null +++ b/vendor/github.com/containers/libpod/cmd/podman/formats/templates.go @@ -0,0 +1,78 @@ +package formats + +import ( + "bytes" + "encoding/json" + "strings" + "text/template" +) + +// basicFunctions are the set of initial +// functions provided to every template. +var basicFunctions = template.FuncMap{ + "json": func(v interface{}) string { + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + _ = enc.Encode(v) + // Remove the trailing new line added by the encoder + return strings.TrimSpace(buf.String()) + }, + "split": strings.Split, + "join": strings.Join, + "title": strings.Title, + "lower": strings.ToLower, + "upper": strings.ToUpper, + "pad": padWithSpace, + "truncate": truncateWithLength, +} + +// HeaderFunctions are used to created headers of a table. +// This is a replacement of basicFunctions for header generation +// because we want the header to remain intact. +// Some functions like `split` are irrelevant so not added. +var headerFunctions = template.FuncMap{ + "json": func(v string) string { + return v + }, + "title": func(v string) string { + return v + }, + "lower": func(v string) string { + return v + }, + "upper": func(v string) string { + return v + }, + "truncate": func(v string, l int) string { + return v + }, +} + +// Parse creates a new anonymous template with the basic functions +// and parses the given format. +func Parse(format string) (*template.Template, error) { + return NewParse("", format) +} + +// NewParse creates a new tagged template with the basic functions +// and parses the given format. +func NewParse(tag, format string) (*template.Template, error) { + return template.New(tag).Funcs(basicFunctions).Parse(format) +} + +// padWithSpace adds whitespace to the input if the input is non-empty +func padWithSpace(source string, prefix, suffix int) string { + if source == "" { + return source + } + return strings.Repeat(" ", prefix) + source + strings.Repeat(" ", suffix) +} + +// truncateWithLength truncates the source string up to the length provided by the input +func truncateWithLength(source string, length int) string { + if len(source) < length { + return source + } + return source[:length] +}