Skip to content

Commit a91423d

Browse files
committed
Add support for multi-arch image indexes
The image loaded is based on the platform flag passed to the structure test binary, which defaults to linux/GOARCH. On hosts that are multi-platform capable, specifying an different platform flag will result in an alternative image being loaded from the index. Fixes #435
1 parent 56c7201 commit a91423d

File tree

4 files changed

+137
-6
lines changed

4 files changed

+137
-6
lines changed

cmd/container-structure-test/app/cmd/test.go

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
package cmd
1616

1717
import (
18+
"errors"
1819
"fmt"
1920
"io"
2021
"os"
2122
"runtime"
23+
"strings"
2224

2325
"github.com/GoogleContainerTools/container-structure-test/cmd/container-structure-test/app/cmd/test"
2426
v1 "github.com/opencontainers/image-spec/specs-go/v1"
@@ -32,6 +34,7 @@ import (
3234

3335
docker "github.com/fsouza/go-dockerclient"
3436
"github.com/google/go-containerregistry/pkg/name"
37+
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
3538
"github.com/google/go-containerregistry/pkg/v1/daemon"
3639
"github.com/google/go-containerregistry/pkg/v1/layout"
3740
"github.com/sirupsen/logrus"
@@ -44,13 +47,60 @@ Be sure you know what you're doing before continuing!
4447
4548
Continue? (y/n)`
4649

50+
const maxSubIndexes = 5
51+
4752
var (
4853
opts = &config.StructureTestOptions{}
4954

5055
args *drivers.DriverConfig
5156
driverImpl func(drivers.DriverConfig) (drivers.Driver, error)
57+
58+
errNoImageFound = errors.New("no compatible image found")
5259
)
5360

61+
func parsePlatform(s string) (*gcrv1.Platform, error) {
62+
parts := strings.Split(s, "/")
63+
if len(parts) != 2 {
64+
return nil, fmt.Errorf("invalid platform %q", s)
65+
}
66+
os, arch := parts[0], parts[1]
67+
platform := &gcrv1.Platform{
68+
Architecture: arch,
69+
OS: os,
70+
}
71+
return platform, nil
72+
}
73+
74+
func findImageInIndex(index gcrv1.ImageIndex, requirements *gcrv1.Platform, depth int) (gcrv1.Image, error) {
75+
if depth > maxSubIndexes {
76+
return nil, fmt.Errorf("too many subindexes (%d)", maxSubIndexes)
77+
}
78+
79+
manifest, err := index.IndexManifest()
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to read index manifest: %w", err)
82+
}
83+
84+
for _, desc := range manifest.Manifests {
85+
if desc.MediaType.IsImage() && desc.Platform.Satisfies(*requirements) {
86+
return index.Image(desc.Digest)
87+
}
88+
if desc.MediaType.IsIndex() {
89+
// Recursively check subindex.
90+
childIndex, err := index.ImageIndex(desc.Digest)
91+
if err != nil {
92+
return nil, err
93+
}
94+
img, err := findImageInIndex(childIndex, requirements, depth+1)
95+
if !errors.Is(err, errNoImageFound) {
96+
return img, err
97+
}
98+
}
99+
}
100+
101+
return nil, errNoImageFound
102+
}
103+
54104
func NewCmdTest(out io.Writer) *cobra.Command {
55105
var testCmd = &cobra.Command{
56106
Use: "test",
@@ -124,14 +174,22 @@ func run(out io.Writer) error {
124174

125175
desc := m.Manifests[0]
126176

177+
var img gcrv1.Image
127178
if desc.MediaType.IsIndex() {
128-
logrus.Fatal("multi-arch images are not supported yet.")
129-
}
130-
131-
img, err := l.Image(desc.Digest)
179+
platform, err := parsePlatform(opts.Platform)
180+
if err != nil {
181+
logrus.Fatalf("%s", err)
182+
}
132183

133-
if err != nil {
134-
logrus.Fatalf("could not get image from %s: %v", opts.ImageFromLayout, err)
184+
img, err = findImageInIndex(l, platform, 0)
185+
if err != nil {
186+
logrus.Fatalf("could not get image from %s (platform %v): %v", opts.ImageFromLayout, opts.Platform, err)
187+
}
188+
} else {
189+
img, err = l.Image(desc.Digest)
190+
if err != nil {
191+
logrus.Fatalf("could not get image from %s: %v", opts.ImageFromLayout, err)
192+
}
135193
}
136194

137195
var tag name.Tag

tests/amd64/fluent_bit_test.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
schemaVersion: '2.0.0' # Make sure to test the latest schema version
2+
commandTests:
3+
- name: 'fluent-bit'
4+
command: '/fluent-bit/bin/fluent-bit'
5+
args: ['--version']
6+
expectedOutput: ['Fluent Bit v4.0.4']
7+
fileContentTests:
8+
- name: 'Passwd file'
9+
expectedContents: ['root:x:0:0:root:/root:/sbin/nologin']
10+
path: '/etc/passwd'
11+
fileExistenceTests:
12+
- name: 'Root'
13+
path: '/'
14+
shouldExist: true
15+
uid: 0
16+
gid: 0
17+
- name: 'libc'
18+
path: '/lib/x86_64-linux-gnu/libc.so.6'
19+
shouldExist: true

tests/arm64/fluent_bit_test.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
schemaVersion: '2.0.0' # Make sure to test the latest schema version
2+
commandTests:
3+
- name: 'fluent-bit'
4+
command: '/fluent-bit/bin/fluent-bit'
5+
args: ['--version']
6+
expectedOutput: ['Fluent Bit v4.0.4']
7+
fileContentTests:
8+
- name: 'Passwd file'
9+
expectedContents: ['root:x:0:0:root:/root:/sbin/nologin']
10+
path: '/etc/passwd'
11+
fileExistenceTests:
12+
- name: 'Root'
13+
path: '/'
14+
shouldExist: true
15+
uid: 0
16+
gid: 0
17+
- name: 'libc'
18+
path: '/lib/aarch64-linux-gnu/libc.so.6'
19+
shouldExist: true

tests/structure_test_tests.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,41 @@ else
246246
echo "PASS: oci success test case"
247247
fi
248248

249+
HEADER "OCI multi-arch image index test case"
250+
251+
if [[ "$go_architecture" == "amd64" || "$go_architecture" == "arm64" ]]; then
252+
test_index="cr.fluentbit.io/fluent/fluent-bit:4.0.4"
253+
254+
manifest=$(docker manifest inspect "$test_index")
255+
if [[ $(echo "$manifest" | jq '.mediaType') != '"application/vnd.docker.distribution.manifest.list.v2+json"' ]]; then
256+
echo "FAIL: multi-arch image index test case - $test_index is not an image index"
257+
echo "$manifest" | jq '.mediaType'
258+
failures=$((failures +1))
259+
fi
260+
image_count=$(echo "$manifest" | jq '.manifests | length')
261+
if [[ $image_count < 2 ]]; then
262+
echo "FAIL: multi-arch image index test case - $test_index only has $image_count image(s)"
263+
failures=$((failures +1))
264+
fi
265+
266+
tmp="$(mktemp -d)"
267+
crane pull "$test_index" --format=oci "$tmp"
268+
269+
res=$(./out/container-structure-test test --image-from-oci-layout="$tmp" --default-image-tag="test.local/$test_index" --config "${test_config_dir}/fluent_bit_test.yaml" 2>&1)
270+
code=$?
271+
if ! [[ ("$res" =~ "PASS" && "$code" == "0") ]];
272+
then
273+
echo "FAIL: oci image index success test case"
274+
echo "$res"
275+
echo "$code"
276+
failures=$((failures +1))
277+
else
278+
echo "PASS: oci image index test case"
279+
fi
280+
else
281+
echo "SKIP: oci image index text case not supported on $go_architecture"
282+
fi
283+
249284
HEADER "Platform test cases"
250285

251286
docker run --rm --privileged tonistiigi/binfmt --install all > /dev/null

0 commit comments

Comments
 (0)