From 6606e99a805264792797e04a96e1a39d79623088 Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Wed, 10 Jul 2024 14:27:37 +0200 Subject: [PATCH] podman-bootc images Add a new `images` command that lists bootc-enabled images in the local store. This is a first step toward working with local images rather than always pulling them. Signed-off-by: Valentin Rothberg --- cmd/images.go | 225 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 cmd/images.go diff --git a/cmd/images.go b/cmd/images.go new file mode 100644 index 00000000..9d304f9d --- /dev/null +++ b/cmd/images.go @@ -0,0 +1,225 @@ +package cmd + +import ( + "fmt" + "os" + "sort" + "strings" + "time" + "unicode" + + "github.com/containers/common/pkg/report" + "github.com/containers/podman-bootc/pkg/utils" + "github.com/containers/podman/v5/pkg/bindings/images" + "github.com/containers/podman/v5/pkg/domain/entities" + "github.com/distribution/reference" + "github.com/docker/go-units" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + imagesCmd = &cobra.Command{ + Use: "images", + Short: "List bootc images in the local containers store", + Long: "List bootc images in the local container store", + RunE: doImages, + } +) + +func init() { + RootCmd.AddCommand(imagesCmd) +} + +func doImages(flags *cobra.Command, args []string) error { + machine, err := utils.GetMachineContext() + if err != nil { + println(utils.PodmanMachineErrorMessage) + logrus.Errorf("failed to connect to podman machine. Is podman machine running?\n%s", err) + return err + } + + filters := map[string][]string{"label": []string{"containers.bootc=1"}} + imageList, err := images.List(machine.Ctx, new(images.ListOptions).WithFilters(filters)) + if err != nil { + return err + } + + imageReports, err := sortImages(imageList) + if err != nil { + return err + } + + return writeImagesTemplate(imageReports) +} + +func writeImagesTemplate(imgs []imageReporter) error { + hdrs := report.Headers(imageReporter{}, map[string]string{ + "ID": "IMAGE ID", + "ReadOnly": "R/O", + }) + + rpt := report.New(os.Stdout, "images") + defer rpt.Flush() + + rpt, err := rpt.Parse(report.OriginPodman, lsFormatFromFlags()) + if err != nil { + return err + } + + if err := rpt.Execute(hdrs); err != nil { + return err + } + + return rpt.Execute(imgs) +} + +func sortImages(imageS []*entities.ImageSummary) ([]imageReporter, error) { + imgs := make([]imageReporter, 0, len(imageS)) + var err error + for _, e := range imageS { + var h imageReporter + if len(e.RepoTags) > 0 { + tagged := []imageReporter{} + untagged := []imageReporter{} + for _, tag := range e.RepoTags { + h.ImageSummary = *e + h.Repository, h.Tag, err = tokenRepoTag(tag) + if err != nil { + return nil, fmt.Errorf("parsing repository tag: %q: %w", tag, err) + } + if h.Tag == "" { + untagged = append(untagged, h) + } else { + tagged = append(tagged, h) + } + } + // Note: we only want to display "" if we + // couldn't find any tagged name in RepoTags. + if len(tagged) > 0 { + imgs = append(imgs, tagged...) + } else { + imgs = append(imgs, untagged[0]) + } + } else { + h.ImageSummary = *e + h.Repository = "" + h.Tag = "" + imgs = append(imgs, h) + } + } + + sort.Slice(imgs, sortFunc("created", imgs)) + return imgs, err +} + +func tokenRepoTag(ref string) (string, string, error) { + if ref == ":" { + return "", "", nil + } + + repo, err := reference.Parse(ref) + if err != nil { + return "", "", err + } + + named, ok := repo.(reference.Named) + if !ok { + return ref, "", nil + } + name := named.Name() + if name == "" { + name = "" + } + + tagged, ok := repo.(reference.Tagged) + if !ok { + return name, "", nil + } + tag := tagged.Tag() + if tag == "" { + tag = "" + } + + return name, tag, nil +} + +func sortFunc(key string, data []imageReporter) func(i, j int) bool { + switch key { + case "id": + return func(i, j int) bool { + return data[i].ID() < data[j].ID() + } + case "repository": + return func(i, j int) bool { + return data[i].Repository < data[j].Repository + } + case "size": + return func(i, j int) bool { + return data[i].size() < data[j].size() + } + case "tag": + return func(i, j int) bool { + return data[i].Tag < data[j].Tag + } + default: + // case "created": + return func(i, j int) bool { + return data[i].created().After(data[j].created()) + } + } +} + +func lsFormatFromFlags() string { + row := []string{ + "{{if .Repository}}{{.Repository}}{{else}}{{end}}", + "{{if .Tag}}{{.Tag}}{{else}}{{end}}", + "{{.ID}}", "{{.Created}}", "{{.Size}}", + } + return "{{range . }}" + strings.Join(row, "\t") + "\n{{end -}}" +} + +type imageReporter struct { + Repository string `json:"repository,omitempty"` + Tag string `json:"tag,omitempty"` + entities.ImageSummary +} + +func (i imageReporter) ID() string { + return i.ImageSummary.ID[0:12] +} + +func (i imageReporter) Created() string { + return units.HumanDuration(time.Since(i.created())) + " ago" +} + +func (i imageReporter) created() time.Time { + return time.Unix(i.ImageSummary.Created, 0).UTC() +} + +func (i imageReporter) Size() string { + s := units.HumanSizeWithPrecision(float64(i.ImageSummary.Size), 3) + j := strings.LastIndexFunc(s, unicode.IsNumber) + return s[:j+1] + " " + s[j+1:] +} + +func (i imageReporter) History() string { + return strings.Join(i.ImageSummary.History, ", ") +} + +func (i imageReporter) CreatedAt() string { + return i.created().String() +} + +func (i imageReporter) CreatedSince() string { + return i.Created() +} + +func (i imageReporter) CreatedTime() string { + return i.CreatedAt() +} + +func (i imageReporter) size() int64 { + return i.ImageSummary.Size +}