Skip to content

Commit 6606e99

Browse files
committed
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 <[email protected]>
1 parent 79c9266 commit 6606e99

File tree

1 file changed

+225
-0
lines changed

1 file changed

+225
-0
lines changed

cmd/images.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"sort"
7+
"strings"
8+
"time"
9+
"unicode"
10+
11+
"github.com/containers/common/pkg/report"
12+
"github.com/containers/podman-bootc/pkg/utils"
13+
"github.com/containers/podman/v5/pkg/bindings/images"
14+
"github.com/containers/podman/v5/pkg/domain/entities"
15+
"github.com/distribution/reference"
16+
"github.com/docker/go-units"
17+
18+
"github.com/sirupsen/logrus"
19+
"github.com/spf13/cobra"
20+
)
21+
22+
var (
23+
imagesCmd = &cobra.Command{
24+
Use: "images",
25+
Short: "List bootc images in the local containers store",
26+
Long: "List bootc images in the local container store",
27+
RunE: doImages,
28+
}
29+
)
30+
31+
func init() {
32+
RootCmd.AddCommand(imagesCmd)
33+
}
34+
35+
func doImages(flags *cobra.Command, args []string) error {
36+
machine, err := utils.GetMachineContext()
37+
if err != nil {
38+
println(utils.PodmanMachineErrorMessage)
39+
logrus.Errorf("failed to connect to podman machine. Is podman machine running?\n%s", err)
40+
return err
41+
}
42+
43+
filters := map[string][]string{"label": []string{"containers.bootc=1"}}
44+
imageList, err := images.List(machine.Ctx, new(images.ListOptions).WithFilters(filters))
45+
if err != nil {
46+
return err
47+
}
48+
49+
imageReports, err := sortImages(imageList)
50+
if err != nil {
51+
return err
52+
}
53+
54+
return writeImagesTemplate(imageReports)
55+
}
56+
57+
func writeImagesTemplate(imgs []imageReporter) error {
58+
hdrs := report.Headers(imageReporter{}, map[string]string{
59+
"ID": "IMAGE ID",
60+
"ReadOnly": "R/O",
61+
})
62+
63+
rpt := report.New(os.Stdout, "images")
64+
defer rpt.Flush()
65+
66+
rpt, err := rpt.Parse(report.OriginPodman, lsFormatFromFlags())
67+
if err != nil {
68+
return err
69+
}
70+
71+
if err := rpt.Execute(hdrs); err != nil {
72+
return err
73+
}
74+
75+
return rpt.Execute(imgs)
76+
}
77+
78+
func sortImages(imageS []*entities.ImageSummary) ([]imageReporter, error) {
79+
imgs := make([]imageReporter, 0, len(imageS))
80+
var err error
81+
for _, e := range imageS {
82+
var h imageReporter
83+
if len(e.RepoTags) > 0 {
84+
tagged := []imageReporter{}
85+
untagged := []imageReporter{}
86+
for _, tag := range e.RepoTags {
87+
h.ImageSummary = *e
88+
h.Repository, h.Tag, err = tokenRepoTag(tag)
89+
if err != nil {
90+
return nil, fmt.Errorf("parsing repository tag: %q: %w", tag, err)
91+
}
92+
if h.Tag == "<none>" {
93+
untagged = append(untagged, h)
94+
} else {
95+
tagged = append(tagged, h)
96+
}
97+
}
98+
// Note: we only want to display "<none>" if we
99+
// couldn't find any tagged name in RepoTags.
100+
if len(tagged) > 0 {
101+
imgs = append(imgs, tagged...)
102+
} else {
103+
imgs = append(imgs, untagged[0])
104+
}
105+
} else {
106+
h.ImageSummary = *e
107+
h.Repository = "<none>"
108+
h.Tag = "<none>"
109+
imgs = append(imgs, h)
110+
}
111+
}
112+
113+
sort.Slice(imgs, sortFunc("created", imgs))
114+
return imgs, err
115+
}
116+
117+
func tokenRepoTag(ref string) (string, string, error) {
118+
if ref == "<none>:<none>" {
119+
return "<none>", "<none>", nil
120+
}
121+
122+
repo, err := reference.Parse(ref)
123+
if err != nil {
124+
return "<none>", "<none>", err
125+
}
126+
127+
named, ok := repo.(reference.Named)
128+
if !ok {
129+
return ref, "<none>", nil
130+
}
131+
name := named.Name()
132+
if name == "" {
133+
name = "<none>"
134+
}
135+
136+
tagged, ok := repo.(reference.Tagged)
137+
if !ok {
138+
return name, "<none>", nil
139+
}
140+
tag := tagged.Tag()
141+
if tag == "" {
142+
tag = "<none>"
143+
}
144+
145+
return name, tag, nil
146+
}
147+
148+
func sortFunc(key string, data []imageReporter) func(i, j int) bool {
149+
switch key {
150+
case "id":
151+
return func(i, j int) bool {
152+
return data[i].ID() < data[j].ID()
153+
}
154+
case "repository":
155+
return func(i, j int) bool {
156+
return data[i].Repository < data[j].Repository
157+
}
158+
case "size":
159+
return func(i, j int) bool {
160+
return data[i].size() < data[j].size()
161+
}
162+
case "tag":
163+
return func(i, j int) bool {
164+
return data[i].Tag < data[j].Tag
165+
}
166+
default:
167+
// case "created":
168+
return func(i, j int) bool {
169+
return data[i].created().After(data[j].created())
170+
}
171+
}
172+
}
173+
174+
func lsFormatFromFlags() string {
175+
row := []string{
176+
"{{if .Repository}}{{.Repository}}{{else}}<none>{{end}}",
177+
"{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}",
178+
"{{.ID}}", "{{.Created}}", "{{.Size}}",
179+
}
180+
return "{{range . }}" + strings.Join(row, "\t") + "\n{{end -}}"
181+
}
182+
183+
type imageReporter struct {
184+
Repository string `json:"repository,omitempty"`
185+
Tag string `json:"tag,omitempty"`
186+
entities.ImageSummary
187+
}
188+
189+
func (i imageReporter) ID() string {
190+
return i.ImageSummary.ID[0:12]
191+
}
192+
193+
func (i imageReporter) Created() string {
194+
return units.HumanDuration(time.Since(i.created())) + " ago"
195+
}
196+
197+
func (i imageReporter) created() time.Time {
198+
return time.Unix(i.ImageSummary.Created, 0).UTC()
199+
}
200+
201+
func (i imageReporter) Size() string {
202+
s := units.HumanSizeWithPrecision(float64(i.ImageSummary.Size), 3)
203+
j := strings.LastIndexFunc(s, unicode.IsNumber)
204+
return s[:j+1] + " " + s[j+1:]
205+
}
206+
207+
func (i imageReporter) History() string {
208+
return strings.Join(i.ImageSummary.History, ", ")
209+
}
210+
211+
func (i imageReporter) CreatedAt() string {
212+
return i.created().String()
213+
}
214+
215+
func (i imageReporter) CreatedSince() string {
216+
return i.Created()
217+
}
218+
219+
func (i imageReporter) CreatedTime() string {
220+
return i.CreatedAt()
221+
}
222+
223+
func (i imageReporter) size() int64 {
224+
return i.ImageSummary.Size
225+
}

0 commit comments

Comments
 (0)