diff --git a/cmd/container-structure-test/app/cmd/test.go b/cmd/container-structure-test/app/cmd/test.go index 14953622..a7d095ce 100644 --- a/cmd/container-structure-test/app/cmd/test.go +++ b/cmd/container-structure-test/app/cmd/test.go @@ -15,10 +15,12 @@ package cmd import ( + "errors" "fmt" "io" "os" "runtime" + "strings" "github.com/GoogleContainerTools/container-structure-test/cmd/container-structure-test/app/cmd/test" v1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -32,6 +34,7 @@ import ( docker "github.com/fsouza/go-dockerclient" "github.com/google/go-containerregistry/pkg/name" + gcrv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/daemon" "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/sirupsen/logrus" @@ -44,13 +47,60 @@ Be sure you know what you're doing before continuing! Continue? (y/n)` +const maxSubIndexes = 5 + var ( opts = &config.StructureTestOptions{} args *drivers.DriverConfig driverImpl func(drivers.DriverConfig) (drivers.Driver, error) + + errNoImageFound = errors.New("no compatible image found") ) +func parsePlatform(s string) (*gcrv1.Platform, error) { + parts := strings.Split(s, "/") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid platform %q", s) + } + os, arch := parts[0], parts[1] + platform := &gcrv1.Platform{ + Architecture: arch, + OS: os, + } + return platform, nil +} + +func findImageInIndex(index gcrv1.ImageIndex, requirements *gcrv1.Platform, depth int) (gcrv1.Image, error) { + if depth > maxSubIndexes { + return nil, fmt.Errorf("too many subindexes (%d)", maxSubIndexes) + } + + manifest, err := index.IndexManifest() + if err != nil { + return nil, fmt.Errorf("failed to read index manifest: %w", err) + } + + for _, desc := range manifest.Manifests { + if desc.MediaType.IsImage() && desc.Platform.Satisfies(*requirements) { + return index.Image(desc.Digest) + } + if desc.MediaType.IsIndex() { + // Recursively check subindex. + childIndex, err := index.ImageIndex(desc.Digest) + if err != nil { + return nil, err + } + img, err := findImageInIndex(childIndex, requirements, depth+1) + if !errors.Is(err, errNoImageFound) { + return img, err + } + } + } + + return nil, errNoImageFound +} + func NewCmdTest(out io.Writer) *cobra.Command { var testCmd = &cobra.Command{ Use: "test", @@ -124,14 +174,22 @@ func run(out io.Writer) error { desc := m.Manifests[0] + var img gcrv1.Image if desc.MediaType.IsIndex() { - logrus.Fatal("multi-arch images are not supported yet.") - } - - img, err := l.Image(desc.Digest) + platform, err := parsePlatform(opts.Platform) + if err != nil { + logrus.Fatalf("%s", err) + } - if err != nil { - logrus.Fatalf("could not get image from %s: %v", opts.ImageFromLayout, err) + img, err = findImageInIndex(l, platform, 0) + if err != nil { + logrus.Fatalf("could not get image from %s (platform %v): %v", opts.ImageFromLayout, opts.Platform, err) + } + } else { + img, err = l.Image(desc.Digest) + if err != nil { + logrus.Fatalf("could not get image from %s: %v", opts.ImageFromLayout, err) + } } var tag name.Tag diff --git a/tests/amd64/fluent_bit_test.yaml b/tests/amd64/fluent_bit_test.yaml new file mode 100644 index 00000000..a8102751 --- /dev/null +++ b/tests/amd64/fluent_bit_test.yaml @@ -0,0 +1,19 @@ +schemaVersion: '2.0.0' # Make sure to test the latest schema version +commandTests: +- name: 'fluent-bit' + command: '/fluent-bit/bin/fluent-bit' + args: ['--version'] + expectedOutput: ['Fluent Bit v4.0.4'] +fileContentTests: +- name: 'Passwd file' + expectedContents: ['root:x:0:0:root:/root:/sbin/nologin'] + path: '/etc/passwd' +fileExistenceTests: +- name: 'Root' + path: '/' + shouldExist: true + uid: 0 + gid: 0 +- name: 'libc' + path: '/lib/x86_64-linux-gnu/libc.so.6' + shouldExist: true diff --git a/tests/arm64/fluent_bit_test.yaml b/tests/arm64/fluent_bit_test.yaml new file mode 100644 index 00000000..da48a54a --- /dev/null +++ b/tests/arm64/fluent_bit_test.yaml @@ -0,0 +1,19 @@ +schemaVersion: '2.0.0' # Make sure to test the latest schema version +commandTests: +- name: 'fluent-bit' + command: '/fluent-bit/bin/fluent-bit' + args: ['--version'] + expectedOutput: ['Fluent Bit v4.0.4'] +fileContentTests: +- name: 'Passwd file' + expectedContents: ['root:x:0:0:root:/root:/sbin/nologin'] + path: '/etc/passwd' +fileExistenceTests: +- name: 'Root' + path: '/' + shouldExist: true + uid: 0 + gid: 0 +- name: 'libc' + path: '/lib/aarch64-linux-gnu/libc.so.6' + shouldExist: true diff --git a/tests/structure_test_tests.sh b/tests/structure_test_tests.sh index 80162779..44b52527 100755 --- a/tests/structure_test_tests.sh +++ b/tests/structure_test_tests.sh @@ -246,6 +246,41 @@ else echo "PASS: oci success test case" fi +HEADER "OCI multi-arch image index test case" + +if [[ "$go_architecture" == "amd64" || "$go_architecture" == "arm64" ]]; then + test_index="cr.fluentbit.io/fluent/fluent-bit:4.0.4" + + manifest=$(docker manifest inspect "$test_index") + if [[ $(echo "$manifest" | jq '.mediaType') != '"application/vnd.docker.distribution.manifest.list.v2+json"' ]]; then + echo "FAIL: multi-arch image index test case - $test_index is not an image index" + echo "$manifest" | jq '.mediaType' + failures=$((failures +1)) + fi + image_count=$(echo "$manifest" | jq '.manifests | length') + if [[ $image_count < 2 ]]; then + echo "FAIL: multi-arch image index test case - $test_index only has $image_count image(s)" + failures=$((failures +1)) + fi + + tmp="$(mktemp -d)" + crane pull "$test_index" --format=oci "$tmp" + + 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) + code=$? + if ! [[ ("$res" =~ "PASS" && "$code" == "0") ]]; + then + echo "FAIL: oci image index success test case" + echo "$res" + echo "$code" + failures=$((failures +1)) + else + echo "PASS: oci image index test case" + fi +else + echo "SKIP: oci image index text case not supported on $go_architecture" +fi + HEADER "Platform test cases" docker run --rm --privileged tonistiigi/binfmt --install all > /dev/null