diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index c0912021..a9f64cce 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -39,8 +39,9 @@ "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6", - "https://bcr.bazel.build/modules/bazel_features/1.33.0/source.json": "13617db3930328c2cd2807a0f13d52ca870ac05f96db9668655113265147b2a6", "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", + "https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080", + "https://bcr.bazel.build/modules/bazel_features/1.42.1/source.json": "fcd4396b2df85f64f2b3bb436ad870793ecf39180f1d796f913cc9276d355309", "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", "https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d", @@ -59,8 +60,8 @@ "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb", - "https://bcr.bazel.build/modules/buildozer/8.2.1/MODULE.bazel": "61e9433c574c2bd9519cad7fa66b9c1d2b8e8d5f3ae5d6528a2c2d26e68d874d", - "https://bcr.bazel.build/modules/buildozer/8.2.1/source.json": "7c33f6a26ee0216f85544b4bca5e9044579e0219b6898dd653f5fb449cf2e484", + "https://bcr.bazel.build/modules/buildozer/8.5.1/MODULE.bazel": "a35d9561b3fc5b18797c330793e99e3b834a473d5fbd3d7d7634aafc9bdb6f8f", + "https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd", "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8", "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/source.json": "fa7b512dfcb5eafd90ce3959cf42a2a6fe96144ebbb4b3b3928054895f2afac2", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", @@ -126,8 +127,8 @@ "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c", "https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37", - "https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8", - "https://bcr.bazel.build/modules/rules_cc/0.2.14/source.json": "55d0a4587c5592fad350f6e698530f4faf0e7dd15e69d43f8d87e220c78bea54", + "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84", + "https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07", "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642", "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", diff --git a/cmd/container-structure-test/app/cmd/test.go b/cmd/container-structure-test/app/cmd/test.go index 069b1464..f4c13550 100644 --- a/cmd/container-structure-test/app/cmd/test.go +++ b/cmd/container-structure-test/app/cmd/test.go @@ -21,6 +21,7 @@ import ( "runtime" "github.com/GoogleContainerTools/container-structure-test/cmd/container-structure-test/app/cmd/test" + "github.com/GoogleContainerTools/container-structure-test/internal/pkgutil" v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/GoogleContainerTools/container-structure-test/pkg/color" @@ -33,7 +34,6 @@ import ( docker "github.com/fsouza/go-dockerclient" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/daemon" - "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -106,66 +106,51 @@ func run(out io.Writer) error { var err error if opts.ImageFromLayout != "" { - if opts.Driver != drivers.Docker { - logrus.Fatal("--image-from-oci-layout is not supported when not using Docker driver") - } - l, err := layout.ImageIndexFromPath(opts.ImageFromLayout) - if err != nil { - logrus.Fatalf("loading %s as OCI layout: %v", opts.ImageFromLayout, err) - } - m, err := l.IndexManifest() - if err != nil { - logrus.Fatalf("could not read OCI index manifest %s: %v", opts.ImageFromLayout, err) - } - - if len(m.Manifests) != 1 { - logrus.Fatalf("OCI layout contains %d entries. expected only one", len(m.Manifests)) + if opts.Driver != drivers.Docker && opts.Driver != drivers.Tar { + logrus.Fatal("--image-from-oci-layout is only supported with Docker or Tar drivers") } - desc := m.Manifests[0] - - if desc.MediaType.IsIndex() { - logrus.Fatal("multi-arch images are not supported yet.") - } - - img, err := l.Image(desc.Digest) - - if err != nil { - logrus.Fatalf("could not get image from %s: %v", opts.ImageFromLayout, err) - } + if opts.Driver == drivers.Tar { + args.OCILayout = opts.ImageFromLayout + } else { + img, desc, err := pkgutil.ImageFromOCILayout(opts.ImageFromLayout) + if err != nil { + logrus.Fatalf("loading OCI layout %s: %v", opts.ImageFromLayout, err) + } - var tag name.Tag + var tag name.Tag - ref := desc.Annotations[v1.AnnotationRefName] - if ref != "" && !opts.IgnoreRefAnnotation { - tag, err = name.NewTag(ref) - if err != nil { - logrus.Fatalf("could not parse ref annotation %s: %v", v1.AnnotationRefName, err) + ref := desc.Annotations[v1.AnnotationRefName] + if ref != "" && !opts.IgnoreRefAnnotation { + tag, err = name.NewTag(ref) + if err != nil { + logrus.Fatalf("could not parse ref annotation %s: %v", v1.AnnotationRefName, err) + } + } else { + if opts.DefaultImageTag == "" { + logrus.Fatalf("index does not contain a reference annotation. --default-image-tag must be provided.") + } + tag, err = name.NewTag(opts.DefaultImageTag, name.StrictValidation) + if err != nil { + logrus.Fatalf("could parse the default image tag %s: %v", opts.DefaultImageTag, err) + } } - } else { - if opts.DefaultImageTag == "" { - logrus.Fatalf("index does not contain a reference annotation. --default-image-tag must be provided.") + var r string + if r, err = daemon.Write(tag, img); err != nil { + logrus.Fatalf("error loading oci layout into daemon:, %v", err) } - tag, err = name.NewTag(opts.DefaultImageTag, name.StrictValidation) + // For some reason, daemon.Write doesn't return errors for some edge cases. + // We should always print what the daemon sent back so that errors are transparent. + fmt.Println("Loaded ", tag.String(), r) + + _, err = daemon.Image(tag) if err != nil { - logrus.Fatalf("could parse the default image tag %s: %v", opts.DefaultImageTag, err) + logrus.Fatalf("error loading oci layout into daemon: %v", err) } - } - var r string - if r, err = daemon.Write(tag, img); err != nil { - logrus.Fatalf("error loading oci layout into daemon: %v, %s", err) - } - // For some reason, daemon.Write doesn't return errors for some edge cases. - // We should always print what the daemon sent back so that errors are transparent. - fmt.Println("Loaded ", tag.String(), r) - _, err = daemon.Image(tag) - if err != nil { - logrus.Fatalf("error loading oci layout into daemon: %v", err) + opts.ImagePath = tag.String() + args.Image = tag.String() } - - opts.ImagePath = tag.String() - args.Image = tag.String() } if opts.Pull { diff --git a/internal/pkgutil/image_utils.go b/internal/pkgutil/image_utils.go index 24c38a35..1020b819 100644 --- a/internal/pkgutil/image_utils.go +++ b/internal/pkgutil/image_utils.go @@ -32,6 +32,7 @@ import ( "github.com/google/go-containerregistry/pkg/name" "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/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/tarball" @@ -180,6 +181,58 @@ func GetImage(imageName string, includeLayers bool, cacheDir string) (Image, err }, nil } +// ImageFromV1 takes a pre-loaded v1.Image and extracts its filesystem, +// returning an Image suitable for use with the tar driver. +func ImageFromV1(img v1.Image, source string) (Image, error) { + imageDigest, err := getImageDigest(img) + if err != nil { + return Image{}, err + } + path, err := getExtractPathForName("", "") + if err != nil { + return Image{}, err + } + if err := GetFileSystemForImage(img, path, nil); err != nil { + os.RemoveAll(path) + return Image{}, errors.Wrap(err, "getting filesystem for image") + } + return Image{ + Image: img, + Source: source, + FSPath: path, + Digest: imageDigest, + }, nil +} + +// ImageFromOCILayout loads a single image from an OCI image layout directory. +// It returns the image and its descriptor (for access to annotations). +// The layout must contain exactly one non-index manifest entry. +func ImageFromOCILayout(path string) (v1.Image, v1.Descriptor, error) { + l, err := layout.ImageIndexFromPath(path) + if err != nil { + return nil, v1.Descriptor{}, errors.Wrapf(err, "loading %s as OCI layout", path) + } + m, err := l.IndexManifest() + if err != nil { + return nil, v1.Descriptor{}, errors.Wrapf(err, "reading OCI index manifest %s", path) + } + if len(m.Manifests) != 1 { + return nil, v1.Descriptor{}, errors.Errorf("OCI layout contains %d entries, expected only one", len(m.Manifests)) + } + + desc := m.Manifests[0] + if desc.MediaType.IsIndex() { + return nil, v1.Descriptor{}, errors.New("multi-arch images are not supported yet") + } + + img, err := l.Image(desc.Digest) + if err != nil { + return nil, v1.Descriptor{}, errors.Wrapf(err, "getting image from %s", path) + } + + return img, desc, nil +} + func getExtractPathForName(name string, cacheDir string) (string, error) { path := cacheDir var err error diff --git a/pkg/drivers/driver.go b/pkg/drivers/driver.go index a2145696..f9149ba6 100644 --- a/pkg/drivers/driver.go +++ b/pkg/drivers/driver.go @@ -29,12 +29,13 @@ const ( ) type DriverConfig struct { - Image string // used by Docker/Tar drivers - Save bool // used by Docker/Tar drivers - Metadata string // used by Host driver - Runtime string // used by Docker driver - Platform string // used by Docker driver - RunOpts unversioned.ContainerRunOptions // used by Docker driver + Image string // used by Docker/Tar drivers + Save bool // used by Docker/Tar drivers + Metadata string // used by Host driver + Runtime string // used by Docker driver + Platform string // used by Docker driver + RunOpts unversioned.ContainerRunOptions // used by Docker driver + OCILayout string // used by Tar driver for OCI layout directories } type Driver interface { diff --git a/pkg/drivers/tar_driver.go b/pkg/drivers/tar_driver.go index af0a2021..fdc10724 100644 --- a/pkg/drivers/tar_driver.go +++ b/pkg/drivers/tar_driver.go @@ -35,6 +35,16 @@ type TarDriver struct { } func NewTarDriver(args DriverConfig) (Driver, error) { + if args.OCILayout != "" { + image, err := imageFromOCILayout(args.OCILayout) + if err != nil { + return nil, errors.Wrap(err, "processing OCI layout") + } + return &TarDriver{ + Image: image, + Save: args.Save, + }, nil + } if pkgutil.IsTar(args.Image) { // tar provided, so don't provide any prefix. container-diff can figure this out. image, err := pkgutil.GetImageForName(args.Image) @@ -68,6 +78,14 @@ func NewTarDriver(args DriverConfig) (Driver, error) { }, nil } +func imageFromOCILayout(path string) (pkgutil.Image, error) { + img, _, err := pkgutil.ImageFromOCILayout(path) + if err != nil { + return pkgutil.Image{}, err + } + return pkgutil.ImageFromV1(img, path) +} + func (d *TarDriver) Destroy() { if !d.Save { pkgutil.CleanupImage(d.Image) diff --git a/tests/amd64/ubuntu_22_04_tar_test.yaml b/tests/amd64/ubuntu_22_04_tar_test.yaml new file mode 100644 index 00000000..3d908e52 --- /dev/null +++ b/tests/amd64/ubuntu_22_04_tar_test.yaml @@ -0,0 +1,24 @@ +schemaVersion: '2.0.0' +fileContentTests: +- name: 'Debian Sources' + excludedContents: ['.*gce_debian_mirror.*'] + expectedContents: ['.*archive\.ubuntu\.com.*'] + path: '/etc/apt/sources.list' +- name: 'Passwd file' + expectedContents: ['root:x:0:0:root:/root:/bin/bash'] + path: '/etc/passwd' +fileExistenceTests: +- name: 'Date' + path: '/bin/date' + isExecutableBy: 'owner' +- name: 'Hosts File' + path: '/etc/hosts' + shouldExist: true +- name: 'Dummy File' + path: '/etc/dummy' + shouldExist: false +licenseTests: +- debian: false + files: + - "/usr/share/doc/ubuntu-keyring/copyright" + - "/usr/share/doc/dash/copyright" diff --git a/tests/arm64/ubuntu_22_04_tar_test.yaml b/tests/arm64/ubuntu_22_04_tar_test.yaml new file mode 100644 index 00000000..f6d50dae --- /dev/null +++ b/tests/arm64/ubuntu_22_04_tar_test.yaml @@ -0,0 +1,24 @@ +schemaVersion: '2.0.0' +fileContentTests: +- name: 'Debian Sources' + excludedContents: ['.*gce_debian_mirror.*'] + expectedContents: ['.*ports\.ubuntu\.com.*'] + path: '/etc/apt/sources.list' +- name: 'Passwd file' + expectedContents: ['root:x:0:0:root:/root:/bin/bash'] + path: '/etc/passwd' +fileExistenceTests: +- name: 'Date' + path: '/bin/date' + isExecutableBy: 'owner' +- name: 'Hosts File' + path: '/etc/hosts' + shouldExist: true +- name: 'Dummy File' + path: '/etc/dummy' + shouldExist: false +licenseTests: +- debian: false + files: + - "/usr/share/doc/ubuntu-keyring/copyright" + - "/usr/share/doc/dash/copyright" diff --git a/tests/ppc64le/ubuntu_22_04_tar_test.yaml b/tests/ppc64le/ubuntu_22_04_tar_test.yaml new file mode 100644 index 00000000..f6d50dae --- /dev/null +++ b/tests/ppc64le/ubuntu_22_04_tar_test.yaml @@ -0,0 +1,24 @@ +schemaVersion: '2.0.0' +fileContentTests: +- name: 'Debian Sources' + excludedContents: ['.*gce_debian_mirror.*'] + expectedContents: ['.*ports\.ubuntu\.com.*'] + path: '/etc/apt/sources.list' +- name: 'Passwd file' + expectedContents: ['root:x:0:0:root:/root:/bin/bash'] + path: '/etc/passwd' +fileExistenceTests: +- name: 'Date' + path: '/bin/date' + isExecutableBy: 'owner' +- name: 'Hosts File' + path: '/etc/hosts' + shouldExist: true +- name: 'Dummy File' + path: '/etc/dummy' + shouldExist: false +licenseTests: +- debian: false + files: + - "/usr/share/doc/ubuntu-keyring/copyright" + - "/usr/share/doc/dash/copyright" diff --git a/tests/s390x/ubuntu_22_04_tar_test.yaml b/tests/s390x/ubuntu_22_04_tar_test.yaml new file mode 100644 index 00000000..f6d50dae --- /dev/null +++ b/tests/s390x/ubuntu_22_04_tar_test.yaml @@ -0,0 +1,24 @@ +schemaVersion: '2.0.0' +fileContentTests: +- name: 'Debian Sources' + excludedContents: ['.*gce_debian_mirror.*'] + expectedContents: ['.*ports\.ubuntu\.com.*'] + path: '/etc/apt/sources.list' +- name: 'Passwd file' + expectedContents: ['root:x:0:0:root:/root:/bin/bash'] + path: '/etc/passwd' +fileExistenceTests: +- name: 'Date' + path: '/bin/date' + isExecutableBy: 'owner' +- name: 'Hosts File' + path: '/etc/hosts' + shouldExist: true +- name: 'Dummy File' + path: '/etc/dummy' + shouldExist: false +licenseTests: +- debian: false + files: + - "/usr/share/doc/ubuntu-keyring/copyright" + - "/usr/share/doc/dash/copyright" diff --git a/tests/structure_test_tests.sh b/tests/structure_test_tests.sh index 87fac56a..207de42f 100755 --- a/tests/structure_test_tests.sh +++ b/tests/structure_test_tests.sh @@ -246,6 +246,22 @@ else echo "PASS: oci success test case" fi +HEADER "OCI layout with tar driver test case" + +res=$(./out/container-structure-test test --driver tar --image-from-oci-layout="$tmp" --config "${test_config_dir}/ubuntu_22_04_tar_test.yaml" 2>&1) +code=$? +if ! [[ ("$res" =~ "PASS" && "$code" == "0") ]]; +then + echo "FAIL: oci layout with tar driver test case" + echo "$res" + echo "$code" + failures=$((failures +1)) +else + echo "PASS: oci layout with tar driver test case" +fi + +rm -rf "$tmp" + HEADER "Platform test cases" docker run --rm --privileged tonistiigi/binfmt --install all > /dev/null