Skip to content

Commit 044b8a7

Browse files
DaliborKrdebarshiray
authored andcommitted
cmd, pkg/podman: Turn Image into an interface
A subsequent commit will switch to unmarshalling the JSON returned from 'podman inspect --format json --type image' directly inside podman.InspectImage() to confine the details within the podman package and make it easier to write unit tests for it. eg., it requires tracking changes to the JSON output across different Podman versions. Unfortunately, the JSON from 'podman inspect --format json --type image' and 'podman images --format json' are considerably different and it will be awkward to use the same implementation of the json.Unmarshaler interface [1] for both. One option is to have two different concrete types separately implement json.Unmarshaler to handle the differences in the JSON, and then hiding these concrete types behind an Image interface that provides access to the values parsed from the JSON. The JSON samples for the unit tests were taken using the default Toolbx image on versions of Fedora that shipped a specific Podman and Toolbx version. This accounts for differences in the JSON caused by different major versions of Podman and the way different Toolbx images were built. One exception was Fedora 28, which had Podman 1.1.2 and Toolbx 0.0.9, which was the last Toolbx version before 'toolbox init-container' became the entry point for all Toolbx containers [2]. However, the default Toolbx image is no longer available from registry.fedoraproject.org. Hence, the image for Fedora 29 was used. The minimum required Podman version is 1.6.4 [3], and the Go implementation has been encouraging users to create containers with Toolbx version 0.0.97 or newer [4]. The versions used to collect the JSON samples for the unit tests were chosen accordingly. They don't exhaustively cover all possible supported and unsupported version combinations, but hopefully enough to be useful. The fedora-toolbox image went through a series of significant changes during the Fedora 39 and 40 development cycles. Fedora 38 was the last release where the image was built from a Container/Dockerfile [5]. For Fedora 39, it was rewritten in terms of fedora-kickstarts and pungi-fedora for the ToolbxReleaseBlocker Change [6]. For Fedora 40, it was rewritten as a KIWI description as part of the KiwiBuiltCloudImages Change [7]. Hence, all three Fedora versions were chosen to cover this transition. [1] https://pkg.go.dev/encoding/json#Unmarshaler [2] Commit 8b84b5e 8b84b5e4604921fa #160 [3] Commit 8e80dd5 8e80dd5db1e6f40b #1253 [4] Commit af1216b af1216b2720c7ab5 #1697 #1684 [5] https://src.fedoraproject.org/container/fedora-toolbox [6] https://fedoraproject.org/wiki/Changes/ToolbxReleaseBlocker [7] https://fedoraproject.org/wiki/Changes/KiwiBuiltCloudImages #1724 #1779
1 parent b611c8f commit 044b8a7

7 files changed

Lines changed: 1217 additions & 107 deletions

File tree

src/cmd/completion.go

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,10 @@ func completionImageNames(cmd *cobra.Command, _ []string, _ string) ([]string, c
150150
if images, err := podman.GetImages(true); err != nil {
151151
logrus.Debugf("Getting all images failed: %s", err)
152152
} else {
153-
for _, image := range images {
154-
if len(image.Names) != 1 {
155-
panic("cannot complete unflattened Image")
156-
}
157-
158-
imageNames = append(imageNames, image.Names[0])
153+
for images.Next() {
154+
image := images.Get()
155+
name := image.Name()
156+
imageNames = append(imageNames, name)
159157
}
160158
}
161159

@@ -170,15 +168,12 @@ func completionImageNamesFiltered(_ *cobra.Command, args []string, _ string) ([]
170168
if images, err := podman.GetImages(true); err != nil {
171169
logrus.Debugf("Getting all images failed: %s", err)
172170
} else {
173-
for _, image := range images {
171+
for images.Next() {
172+
image := images.Get()
173+
name := image.Name()
174174
skip := false
175-
176-
if len(image.Names) != 1 {
177-
panic("cannot complete unflattened Image")
178-
}
179-
180175
for _, arg := range args {
181-
if arg == image.Names[0] {
176+
if arg == name {
182177
skip = true
183178
break
184179
}
@@ -188,7 +183,7 @@ func completionImageNamesFiltered(_ *cobra.Command, args []string, _ string) ([]
188183
continue
189184
}
190185

191-
imageNames = append(imageNames, image.Names[0])
186+
imageNames = append(imageNames, name)
192187
}
193188
}
194189

src/cmd/list.go

Lines changed: 12 additions & 10 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.Containers
8686
var err error
8787

@@ -130,24 +130,26 @@ func listHelp(cmd *cobra.Command, args []string) {
130130
}
131131
}
132132

133-
func listOutput(images []podman.Image, containers *podman.Containers) {
134-
if len(images) != 0 {
133+
func listOutput(images *podman.Images, containers *podman.Containers) {
134+
if images.Len() != 0 {
135135
writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
136136
fmt.Fprintf(writer, "%s\t%s\t%s\n", "IMAGE ID", "IMAGE NAME", "CREATED")
137137

138-
for _, image := range images {
139-
if len(image.Names) != 1 {
140-
panic("cannot list unflattened Image")
141-
}
138+
for images.Next() {
139+
image := images.Get()
140+
created := image.Created()
141+
name := image.Name()
142+
143+
id := image.ID()
144+
shortID := utils.ShortID(id)
142145

143-
shortID := utils.ShortID(image.ID)
144-
fmt.Fprintf(writer, "%s\t%s\t%s\n", shortID, image.Names[0], image.Created)
146+
fmt.Fprintf(writer, "%s\t%s\t%s\n", shortID, name, created)
145147
}
146148

147149
writer.Flush()
148150
}
149151

150-
if len(images) != 0 && containers.Len() != 0 {
152+
if images.Len() != 0 && containers.Len() != 0 {
151153
fmt.Println()
152154
}
153155

src/cmd/rmi.go

Lines changed: 3 additions & 2 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

src/meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ sources = files(
2323
'pkg/nvidia/nvidia.go',
2424
'pkg/podman/container.go',
2525
'pkg/podman/errors.go',
26+
'pkg/podman/image.go',
27+
'pkg/podman/imageImages_test.go',
2628
'pkg/podman/podman.go',
2729
'pkg/podman/containerInspect_test.go',
2830
'pkg/shell/shell.go',

src/pkg/podman/image.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
Name() string
31+
Names() []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+
}
45+
46+
func (images *Images) Get() Image {
47+
if images.i < 1 {
48+
panic("called Images.Get() without calling Images.Next()")
49+
}
50+
51+
image := &images.data[images.i-1]
52+
return image
53+
}
54+
55+
func (images *Images) Len() int {
56+
if images == nil {
57+
return 0
58+
}
59+
60+
return len(images.data)
61+
}
62+
63+
func (images *Images) Next() bool {
64+
available := images.i < len(images.data)
65+
if available {
66+
images.i++
67+
}
68+
69+
return available
70+
}
71+
72+
func (images *Images) Reset() {
73+
images.i = 0
74+
}
75+
76+
func (image *imageImages) Created() string {
77+
return image.created
78+
}
79+
80+
func (image *imageImages) flattenNames(fillNameWithID bool) []imageImages {
81+
var ret []imageImages
82+
83+
names := image.Names()
84+
namesCount := len(names)
85+
if namesCount == 0 {
86+
flattenedImage := *image
87+
88+
if fillNameWithID {
89+
id := image.ID()
90+
shortID := utils.ShortID(id)
91+
flattenedImage.names = []string{shortID}
92+
} else {
93+
flattenedImage.names = []string{"<none>"}
94+
}
95+
96+
ret = []imageImages{flattenedImage}
97+
return ret
98+
}
99+
100+
ret = make([]imageImages, 0, namesCount)
101+
102+
for _, name := range names {
103+
flattenedImage := *image
104+
flattenedImage.names = []string{name}
105+
ret = append(ret, flattenedImage)
106+
}
107+
108+
return ret
109+
}
110+
111+
func (image *imageImages) ID() string {
112+
return image.id
113+
}
114+
115+
func (image *imageImages) IsToolbx() bool {
116+
return isToolbx(image.labels)
117+
}
118+
119+
func (image *imageImages) Labels() map[string]string {
120+
return image.labels
121+
}
122+
123+
func (image *imageImages) Name() string {
124+
if len(image.names) != 1 {
125+
panic("cannot get name from unflattened Image")
126+
}
127+
return image.names[0]
128+
}
129+
130+
func (image *imageImages) Names() []string {
131+
if image.names == nil {
132+
return nil
133+
}
134+
135+
namesCount := len(image.names)
136+
ret := make([]string, namesCount)
137+
copy(ret, image.names)
138+
return ret
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+
}
148+
149+
if err := json.Unmarshal(data, &raw); err != nil {
150+
return err
151+
}
152+
153+
// Until Podman 2.0.x the field 'Created' held a human-readable string in
154+
// format "5 minutes ago". Since Podman 2.1 the field holds an integer with
155+
// Unix time. Go interprets numbers in JSON as float64.
156+
switch value := raw.Created.(type) {
157+
case string:
158+
image.created = value
159+
case float64:
160+
image.created = utils.HumanDuration(int64(value))
161+
}
162+
163+
image.id = raw.ID
164+
image.labels = raw.Labels
165+
image.names = raw.Names
166+
return nil
167+
}

0 commit comments

Comments
 (0)