diff --git a/cmd/container-structure-test/app/cmd/test.go b/cmd/container-structure-test/app/cmd/test.go index ac001896..6a0e3d3b 100644 --- a/cmd/container-structure-test/app/cmd/test.go +++ b/cmd/container-structure-test/app/cmd/test.go @@ -20,10 +20,9 @@ import ( "io/ioutil" "os" - "regexp" - "strings" "github.com/GoogleContainerTools/container-structure-test/cmd/container-structure-test/app/cmd/test" + v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/GoogleContainerTools/container-structure-test/pkg/color" "github.com/GoogleContainerTools/container-structure-test/pkg/config" @@ -33,6 +32,9 @@ import ( "github.com/GoogleContainerTools/container-structure-test/pkg/utils" 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" ) @@ -43,9 +45,6 @@ Be sure you know what you're doing before continuing! Continue? (y/n)` -var totalTests int -var testReportFile *os.File - var ( opts = &config.StructureTestOptions{} @@ -106,21 +105,76 @@ 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)) + } + + 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) + } + + var tag name.Tag + + ref := desc.Annotations[v1.AnnotationRefName] + if ref != "" { + 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) + } + } + if _, err = daemon.Write(tag, img); err != nil { + logrus.Fatalf("error loading oci layout into daemon: %v, %s", err) + } + + opts.ImagePath = tag.String() + args.Image = tag.String() + } + if opts.Pull { if opts.Driver != drivers.Docker { logrus.Fatal("image pull not supported when not using Docker driver") } - var repository, tag string - parts := splitImagePath(opts.ImagePath) - if len(parts) < 2 { - logrus.Fatal("no tag specified for provided image") + ref, err := name.ParseReference(opts.ImagePath) + if err != nil { + logrus.Fatal(err) } - repository = parts[0] - tag = parts[1] client, err := docker.NewClientFromEnv() + if err != nil { + logrus.Fatalf("error connecting to daemon: %v", err) + } if err = client.PullImage(docker.PullImageOptions{ - Repository: repository, - Tag: tag, + Repository: ref.Context().RepositoryStr(), + Tag: ref.Identifier(), + Registry: ref.Context().RegistryStr(), OutputStream: out, }, docker.AuthConfiguration{}); err != nil { logrus.Fatalf("error pulling remote image %s: %s", opts.ImagePath, err.Error()) @@ -163,38 +217,17 @@ func runTests(out io.Writer, channel chan interface{}, args *drivers.DriverConfi close(channel) } -func splitImagePath(imagePath string) []string { - var parts []string - if strings.Contains(imagePath, "@") { - parts = strings.Split(imagePath, "@") - } else { - switch countColons := strings.Count(imagePath, ":"); countColons { - case 0: - parts = []string{imagePath} - case 1: - match, _ := regexp.MatchString(`:\d{1,5}\/`, imagePath) - if match { - //colon is part of a registry port and no tag available - parts = []string{imagePath} - } else { - //colon separates image name and tag - parts = strings.Split(imagePath, ":") - } - default: - imageTagRegex, _ := regexp.Compile("(.*):(.*)") - parts = imageTagRegex.FindStringSubmatch(imagePath)[1:] - } - } - return parts -} - func AddTestFlags(cmd *cobra.Command) { cmd.Flags().StringVarP(&opts.ImagePath, "image", "i", "", "path to test image") + cmd.Flags().StringVar(&opts.ImageFromLayout, "image-from-oci-layout", "", "path to the oci layout to test against") + cmd.Flags().StringVar(&opts.DefaultImageTag, "default-image-tag", "", "default image tag to used when loading images to the daemon. required when --image-from-oci-layout refers to a oci layout lacking the reference annotation.") + cmd.MarkFlagsMutuallyExclusive("image", "image-from-oci-layout") cmd.Flags().StringVarP(&opts.Driver, "driver", "d", "docker", "driver to use when running tests") cmd.Flags().StringVar(&opts.Metadata, "metadata", "", "path to image metadata file") cmd.Flags().StringVar(&opts.Runtime, "runtime", "", "runtime to use with docker driver") cmd.Flags().BoolVar(&opts.Pull, "pull", false, "force a pull of the image before running tests") + cmd.MarkFlagsMutuallyExclusive("image-from-oci-layout", "pull") cmd.Flags().BoolVar(&opts.Save, "save", false, "preserve created containers after test run") cmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "flag to suppress output") cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "force run of host driver (without user prompt)") diff --git a/cmd/container-structure-test/app/cmd/test/util.go b/cmd/container-structure-test/app/cmd/test/util.go index 943d25a1..5683c71b 100644 --- a/cmd/container-structure-test/app/cmd/test/util.go +++ b/cmd/container-structure-test/app/cmd/test/util.go @@ -41,8 +41,8 @@ func ValidateArgs(opts *config.StructureTestOptions) error { return fmt.Errorf("Cannot provide both image path and metadata file") } } else { - if opts.ImagePath == "" { - return fmt.Errorf("Please supply path to image or tarball to test against") + if opts.ImagePath == "" && opts.ImageFromLayout == "" { + return fmt.Errorf("Please supply path to image or oci image layout to test against") } if opts.Metadata != "" { return fmt.Errorf("Cannot provide both image path and metadata file") diff --git a/go.mod b/go.mod index 94ad91cc..dc33a3d8 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/google/go-containerregistry v0.5.1 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.8.1 - github.com/spf13/cobra v1.0.0 + github.com/spf13/cobra v1.5.0 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 112545fd..3415e7c4 100644 --- a/go.sum +++ b/go.sum @@ -216,6 +216,7 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -576,6 +577,7 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= @@ -602,6 +604,8 @@ github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKv github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= diff --git a/pkg/config/options.go b/pkg/config/options.go index 76a89f0e..86df8c05 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -17,12 +17,14 @@ package config import "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned" type StructureTestOptions struct { - ImagePath string - Driver string - Runtime string - Metadata string - TestReport string - ConfigFiles []string + ImagePath string + ImageFromLayout string + DefaultImageTag string + Driver string + Runtime string + Metadata string + TestReport string + ConfigFiles []string JSON bool Output unversioned.OutputValue diff --git a/tests/structure_test_tests.sh b/tests/structure_test_tests.sh index 757dcbf7..d6af5830 100755 --- a/tests/structure_test_tests.sh +++ b/tests/structure_test_tests.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Copyright 2017 Google Inc. All rights reserved. @@ -125,3 +125,38 @@ then else echo "PASS: Failure test failed" fi + + +echo "###" +echo "# OCI layout test case" +echo "###" + +go install github.com/google/go-containerregistry/cmd/crane/cmd +tmp="$(mktemp -d)" + +crane pull "$test_image" --format=oci "$tmp" --platform=linux/arm64 + + +res=$(./out/container-structure-test test --image-from-oci-layout="$tmp" --config "${test_config_dir}/ubuntu_20_04_test.yaml" 2>&1) +code=$? +if ! [[ ("$res" =~ "index does not contain a reference annotation. --default-image-tag must be provided." && "$code" == "1") ]]; +then + echo "FAIL: oci failing test case" + echo "$res" + echo "$code" + failures=$((failures +1)) +else + echo "PASS: oci failing test case" +fi + +res=$(./out/container-structure-test test --image-from-oci-layout="$tmp" --default-image-tag="test.local/library/$test_image" --config "${test_config_dir}/ubuntu_20_04_test.yaml" 2>&1) +code=$? +if ! [[ ("$res" =~ "PASS" && "$code" == "0") ]]; +then + echo "FAIL: oci success test case" + echo "$res" + echo "$code" + failures=$((failures +1)) +else + echo "PASS: oci success test case" +fi \ No newline at end of file