Skip to content

Commit aa553fc

Browse files
DaliborKrdebarshiray
authored andcommitted
cmd, pkg/podman: Turn Image into an interface
In reaction to the conversation in PR #1707, I made suggested changes to the implementation in terms of how Image information, received from Podman, is represented and treated in the Toolbx code. I used the same procedure as in the case of representing Containers: - Commit e611969 - Commit ec7eb59 In short, the JSON from 'podman inspect --type image' and 'podman images' are different, so logically, there should be different implementations of the JSON.Unmarshaler interface [1] for them as well. The new Image interface provides access to the values ​​parsed from the JSONs and two different concrete types, which are implemented separately to handle the differences in the JSONs. GetImages() now returns an Images iterator-like structure that handles image flattening and sorting internally. InspectImage() returns an Image interface instead of map[string]interface{}, and the IsToolboxImage() function was replaced with the IsToolbx() method on the Image interface. Tested were results of Podman starting on version 1.1.2, which should be sufficient since the minimum required Podman version is 1.6.4 (see commit 8e80dd5). Covered are structures representing podman commands 'podman inspect --type image' and 'podman images'. [1] https://pkg.go.dev/encoding/json#Unmarshaler #1724 Signed-off-by: Dalibor Kricka <dalidalk@seznam.cz>
1 parent 4cd38d3 commit aa553fc

8 files changed

Lines changed: 2473 additions & 142 deletions

File tree

src/cmd/completion.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,13 @@ func completionImageNames(cmd *cobra.Command, _ []string, _ string) ([]string, c
138138
if images, err := podman.GetImages(true); err != nil {
139139
logrus.Debugf("Getting all images failed: %s", err)
140140
} else {
141-
for _, image := range images {
142-
if len(image.Names) != 1 {
141+
for images.Next() {
142+
image := images.Get()
143+
if len(image.Names()) != 1 {
143144
panic("cannot complete unflattened Image")
144145
}
145146

146-
imageNames = append(imageNames, image.Names[0])
147+
imageNames = append(imageNames, image.Names()[0])
147148
}
148149
}
149150

@@ -158,15 +159,16 @@ func completionImageNamesFiltered(_ *cobra.Command, args []string, _ string) ([]
158159
if images, err := podman.GetImages(true); err != nil {
159160
logrus.Debugf("Getting all images failed: %s", err)
160161
} else {
161-
for _, image := range images {
162+
for images.Next() {
162163
skip := false
163164

164-
if len(image.Names) != 1 {
165+
image := images.Get()
166+
if len(image.Names()) != 1 {
165167
panic("cannot complete unflattened Image")
166168
}
167169

168170
for _, arg := range args {
169-
if arg == image.Names[0] {
171+
if arg == image.Names()[0] {
170172
skip = true
171173
break
172174
}
@@ -176,7 +178,7 @@ func completionImageNamesFiltered(_ *cobra.Command, args []string, _ string) ([]
176178
continue
177179
}
178180

179-
imageNames = append(imageNames, image.Names[0])
181+
imageNames = append(imageNames, image.Names()[0])
180182
}
181183
}
182184

src/cmd/list.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func list(cmd *cobra.Command, args []string) error {
8181
lsImages = false
8282
}
8383

84-
var images []podman.Image
84+
var images *podman.Images
8585
var containers []podman.Container
8686
var err error
8787

@@ -147,26 +147,27 @@ func listHelp(cmd *cobra.Command, args []string) {
147147
}
148148
}
149149

150-
func listOutput(images []podman.Image, containers []podman.Container) {
151-
if len(images) != 0 {
150+
func listOutput(images *podman.Images, containers []podman.Container) {
151+
if images.Len() != 0 {
152152
writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
153153
fmt.Fprintf(writer, "%s\t%s\t%s\n", "IMAGE ID", "IMAGE NAME", "CREATED")
154154

155-
for _, image := range images {
156-
if len(image.Names) != 1 {
155+
for images.Next() {
156+
image := images.Get()
157+
if len(image.Names()) != 1 {
157158
panic("cannot list unflattened Image")
158159
}
159160

160161
fmt.Fprintf(writer, "%s\t%s\t%s\n",
161-
utils.ShortID(image.ID),
162-
image.Names[0],
163-
image.Created)
162+
utils.ShortID(image.ID()),
163+
image.Names()[0],
164+
image.Created())
164165
}
165166

166167
writer.Flush()
167168
}
168169

169-
if len(images) != 0 && len(containers) != 0 {
170+
if images.Len() != 0 && len(containers) != 0 {
170171
fmt.Println()
171172
}
172173

src/cmd/rmi.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ func rmi(cmd *cobra.Command, args []string) error {
7676
return errors.New("failed to get images")
7777
}
7878

79-
for _, image := range toolboxImages {
80-
imageID := image.ID
79+
for toolboxImages.Next() {
80+
image := toolboxImages.Get()
81+
imageID := image.ID()
8182
if err := podman.RemoveImage(imageID, rmiFlags.forceDelete); err != nil {
8283
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
8384
continue
@@ -94,8 +95,14 @@ func rmi(cmd *cobra.Command, args []string) error {
9495
}
9596

9697
for _, image := range args {
97-
if _, err := podman.IsToolboxImage(image); err != nil {
98-
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
98+
imageObj, err := podman.InspectImage(image)
99+
if err != nil {
100+
fmt.Fprintf(os.Stderr, "Error: failed to inspect image %s\n", image)
101+
continue
102+
}
103+
104+
if !imageObj.IsToolbx() {
105+
fmt.Fprintf(os.Stderr, "Error: %s is not a Toolbx image\n", image)
99106
continue
100107
}
101108

src/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ sources = files(
2323
'pkg/nvidia/nvidia.go',
2424
'pkg/podman/container.go',
2525
'pkg/podman/errors.go',
26+
'pkg/podman/image.go',
2627
'pkg/podman/podman.go',
2728
'pkg/podman/containerInspect_test.go',
2829
'pkg/shell/shell.go',

src/pkg/podman/image.go

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/*
2+
* Copyright © 2025 – 2026 Red Hat Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package podman
18+
19+
import (
20+
"encoding/json"
21+
22+
"github.com/containers/toolbox/pkg/utils"
23+
)
24+
25+
type Image interface {
26+
Created() string
27+
ID() string
28+
IsToolbx() bool
29+
Labels() map[string]string
30+
Names() []string
31+
RepoTags() []string
32+
}
33+
34+
type Images struct {
35+
data []imageImages
36+
i int
37+
}
38+
39+
type imageImages struct {
40+
created string
41+
id string
42+
labels map[string]string
43+
names []string
44+
repoTags []string
45+
}
46+
47+
type imageInspect struct {
48+
created string
49+
entrypoint []string
50+
envVars []string
51+
id string
52+
labels map[string]string
53+
namesHistory []string
54+
repoTags []string
55+
}
56+
57+
type imageSlice []imageImages
58+
59+
func (images *Images) Get() Image {
60+
if images.i < 1 {
61+
panic("called Images.Get() without calling Images.Next()")
62+
}
63+
64+
image := images.data[images.i-1]
65+
return &image
66+
}
67+
68+
func (images *Images) Len() int {
69+
return len(images.data)
70+
}
71+
72+
func (images *Images) Next() bool {
73+
available := images.i < len(images.data)
74+
if available {
75+
images.i++
76+
}
77+
78+
return available
79+
}
80+
81+
func (images *Images) Reset() {
82+
images.i = 0
83+
}
84+
85+
func (image *imageImages) Created() string {
86+
return image.created
87+
}
88+
89+
func (image *imageImages) flattenNames(fillNameWithID bool) []imageImages {
90+
var ret []imageImages
91+
92+
if len(image.Names()) == 0 {
93+
flattenedImage := *image
94+
95+
if fillNameWithID {
96+
shortID := utils.ShortID(image.ID())
97+
flattenedImage.setNames([]string{shortID})
98+
} else {
99+
flattenedImage.setNames([]string{"<none>"})
100+
}
101+
102+
ret = []imageImages{flattenedImage}
103+
return ret
104+
}
105+
106+
ret = make([]imageImages, 0, len(image.Names()))
107+
108+
for _, name := range image.Names() {
109+
flattenedImage := *image
110+
flattenedImage.setNames([]string{name})
111+
ret = append(ret, flattenedImage)
112+
}
113+
114+
return ret
115+
}
116+
117+
func (image *imageImages) ID() string {
118+
return image.id
119+
}
120+
121+
func (image *imageImages) IsToolbx() bool {
122+
return isToolbx(image.labels)
123+
}
124+
125+
func (image *imageImages) Labels() map[string]string {
126+
return image.labels
127+
}
128+
129+
func (image *imageImages) Names() []string {
130+
return image.names
131+
}
132+
133+
func (image *imageImages) setNames(names []string) {
134+
image.names = names
135+
}
136+
137+
func (image *imageImages) RepoTags() []string {
138+
return image.repoTags
139+
}
140+
141+
func (image *imageImages) UnmarshalJSON(data []byte) error {
142+
var raw struct {
143+
Created interface{}
144+
ID string
145+
Labels map[string]string
146+
Names []string
147+
RepoTags []string
148+
}
149+
150+
if err := json.Unmarshal(data, &raw); err != nil {
151+
return err
152+
}
153+
154+
// Until Podman 2.0.x the field 'Created' held a human-readable string in
155+
// format "5 minutes ago". Since Podman 2.1 the field holds an integer with
156+
// Unix time. Go interprets numbers in JSON as float64.
157+
switch value := raw.Created.(type) {
158+
case string:
159+
image.created = value
160+
case float64:
161+
image.created = utils.HumanDuration(int64(value))
162+
}
163+
164+
image.id = raw.ID
165+
image.labels = raw.Labels
166+
image.names = raw.Names
167+
image.repoTags = raw.RepoTags
168+
return nil
169+
}
170+
171+
func (image *imageInspect) Created() string {
172+
return image.created
173+
}
174+
175+
func (image *imageInspect) Entrypoint() []string {
176+
return image.entrypoint
177+
}
178+
179+
func (image *imageInspect) EnvVars() []string {
180+
return image.envVars
181+
}
182+
183+
func (image *imageInspect) ID() string {
184+
return image.id
185+
}
186+
187+
func (image *imageInspect) IsToolbx() bool {
188+
return isToolbx(image.labels)
189+
}
190+
191+
func (image *imageInspect) Labels() map[string]string {
192+
return image.labels
193+
}
194+
195+
func (image *imageInspect) Names() []string {
196+
return image.namesHistory
197+
}
198+
199+
func (image *imageInspect) RepoTags() []string {
200+
return image.repoTags
201+
}
202+
203+
func (image *imageInspect) UnmarshalJSON(data []byte) error {
204+
var raw struct {
205+
Created interface{}
206+
ID string
207+
Config struct {
208+
Labels map[string]string
209+
Env []string
210+
Entrypoint []string
211+
}
212+
NamesHistory []string
213+
RepoTags []string
214+
}
215+
216+
if err := json.Unmarshal(data, &raw); err != nil {
217+
return err
218+
}
219+
220+
switch value := raw.Created.(type) {
221+
case string:
222+
image.created = value
223+
case float64:
224+
image.created = utils.HumanDuration(int64(value))
225+
}
226+
227+
image.id = raw.ID
228+
image.labels = raw.Config.Labels
229+
image.envVars = raw.Config.Env
230+
image.namesHistory = raw.NamesHistory
231+
image.repoTags = raw.RepoTags
232+
image.entrypoint = raw.Config.Entrypoint
233+
return nil
234+
}
235+
236+
func (images imageSlice) Len() int {
237+
return len(images)
238+
}
239+
240+
func (images imageSlice) Less(i, j int) bool {
241+
if len(images[i].Names()) != 1 {
242+
panic("cannot sort unflattened Images")
243+
}
244+
245+
if len(images[j].Names()) != 1 {
246+
panic("cannot sort unflattened Images")
247+
}
248+
249+
return images[i].Names()[0] < images[j].Names()[0]
250+
}
251+
252+
func (images imageSlice) Swap(i, j int) {
253+
images[i], images[j] = images[j], images[i]
254+
}

0 commit comments

Comments
 (0)