From fe9d181f888e2325fa0ae06b1374ed160a4fb925 Mon Sep 17 00:00:00 2001 From: "Brad P. Crochet" Date: Mon, 5 Jun 2023 17:01:48 -0400 Subject: [PATCH] Process image manifest if platform not specified If --platform is not given on 'check container', preflight will now process an manifest list if that's what the URL points to. If platform is specified, only process that platform. It will not do anything different yet for submission. It would be the equivalent of running preflight on each of the images in the manifest, with the 'arch' specified. This fixes the reported issue. However, more will be implemented when Pyxis and all other systems are ready to assosciate the images with an actual manifest list. Fixes #955 Signed-off-by: Brad P. Crochet --- cmd/preflight/cmd/check_container.go | 220 ++++++++++++++++------ cmd/preflight/cmd/check_container_test.go | 141 ++++++++++++-- cmd/preflight/cmd/support.go | 24 +-- container/check_container.go | 23 ++- internal/engine/engine.go | 42 +++-- internal/image/types.go | 13 +- internal/pyxis/types.go | 11 +- internal/runtime/config.go | 1 + internal/runtime/config_test.go | 4 +- 9 files changed, 358 insertions(+), 121 deletions(-) diff --git a/cmd/preflight/cmd/check_container.go b/cmd/preflight/cmd/check_container.go index e13d4d3e..7d421178 100644 --- a/cmd/preflight/cmd/check_container.go +++ b/cmd/preflight/cmd/check_container.go @@ -18,11 +18,16 @@ import ( "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/cli" "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/formatters" "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/lib" + "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/log" + "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/option" "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/runtime" "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/viper" "github.com/redhat-openshift-ecosystem/openshift-preflight/version" "github.com/go-logr/logr" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -80,8 +85,8 @@ func checkContainerCmd(runpreflight runPreflight) *cobra.Command { "URL paramater. This value may differ from the PID on the overview page. (env: PFLT_CERTIFICATION_PROJECT_ID)")) _ = viper.BindPFlag("certification_project_id", flags.Lookup("certification-project-id")) - checkContainerCmd.Flags().String("platform", rt.GOARCH, "Architecture of image to pull. Defaults to current platform.") - _ = viper.BindPFlag("platform", checkContainerCmd.Flags().Lookup("platform")) + flags.String("platform", rt.GOARCH, "Architecture of image to pull. Defaults to runtime platform.") + _ = viper.BindPFlag("platform", flags.Lookup("platform")) return checkContainerCmd } @@ -103,80 +108,89 @@ func checkContainerRunE(cmd *cobra.Command, args []string, runpreflight runPrefl return fmt.Errorf("invalid configuration: %w", err) } - artifactsWriter, err := artifacts.NewFilesystemWriter(artifacts.WithDirectory(cfg.Artifacts)) - if err != nil { - return err - } - - // Add the artifact writer to the context for use by checks. - ctx = artifacts.ContextWithWriter(ctx, artifactsWriter) + cfg.Image = containerImage - formatter, err := formatters.NewByName(formatters.DefaultFormat) + containerImagePlatforms, err := platformsToBeProcessed(cmd, cfg) if err != nil { return err } - opts := generateContainerCheckOptions(cfg) - - checkcontainer := container.NewCheck( - containerImage, - opts..., - ) + for _, platform := range containerImagePlatforms { + logger.Info(fmt.Sprintf("running checks for %s for platform %s", containerImage, platform)) + artifactsWriter, err := artifacts.NewFilesystemWriter(artifacts.WithDirectory(filepath.Join(cfg.Artifacts, platform))) + if err != nil { + return err + } - pc := lib.NewPyxisClient(ctx, cfg.CertificationProjectID, cfg.PyxisAPIToken, cfg.PyxisHost) - resultSubmitter := lib.ResolveSubmitter(pc, cfg.CertificationProjectID, cfg.DockerConfig, cfg.LogFile) + // Add the artifact writer to the context for use by checks. + ctx := artifacts.ContextWithWriter(ctx, artifactsWriter) - // Run the container check. - cmd.SilenceUsage = true + formatter, err := formatters.NewByName(formatters.DefaultFormat) + if err != nil { + return err + } - err = runpreflight( - ctx, - checkcontainer.Run, - cli.CheckConfig{ - IncludeJUnitResults: cfg.WriteJUnit, - SubmitResults: cfg.Submit, - }, - formatter, - &runtime.ResultWriterFile{}, - resultSubmitter, - ) + opts := generateContainerCheckOptions(cfg) + opts = append(opts, container.WithPlatform(platform)) + + checkcontainer := container.NewCheck( + containerImage, + opts..., + ) + + pc := lib.NewPyxisClient(ctx, cfg.CertificationProjectID, cfg.PyxisAPIToken, cfg.PyxisHost) + resultSubmitter := lib.ResolveSubmitter(pc, cfg.CertificationProjectID, cfg.DockerConfig, cfg.LogFile) + + // Run the container check. + cmd.SilenceUsage = true + + if err := runpreflight( + ctx, + checkcontainer.Run, + cli.CheckConfig{ + IncludeJUnitResults: cfg.WriteJUnit, + SubmitResults: cfg.Submit, + }, + formatter, + &runtime.ResultWriterFile{}, + resultSubmitter, + ); err != nil { + return err + } - if err != nil { - return err - } + // checking for offline flag, if present tar up the contents of the artifacts directory + if cfg.Offline { + src := artifactsWriter.Path() + var buf bytes.Buffer - // checking for offline flag, if present tar up the contents of the artifacts directory - if cfg.Offline { - src := artifactsWriter.Path() - var buf bytes.Buffer + // check to see if a tar file already exist to account for someone re-running + exists, err := artifactsWriter.Exists(check.DefaultArtifactsTarFileName) + if err != nil { + return fmt.Errorf("unable to check if tar already exists: %v", err) + } - // check to see if a tar file already exist to account for someone re-running - exists, err := artifactsWriter.Exists(check.DefaultArtifactsTarFileName) - if err != nil { - return fmt.Errorf("unable to check if tar already exists: %v", err) - } + // remove the tar file if it exists + if exists { + err = artifactsWriter.Remove(check.DefaultArtifactsTarFileName) + if err != nil { + return fmt.Errorf("unable to remove existing tar: %v", err) + } + } - // remove the tar file if it exists - if exists { - err = artifactsWriter.Remove(check.DefaultArtifactsTarFileName) + // tar the directory + err = artifactsTar(ctx, src, &buf) if err != nil { - return fmt.Errorf("unable to remove existing tar: %v", err) + return fmt.Errorf("unable to tar up artifacts directory: %v", err) } - } - // tar the directory - err = artifactsTar(ctx, src, &buf) - if err != nil { - return fmt.Errorf("unable to tar up artifacts directory: %v", err) - } + // writing the tar file to disk + _, err = artifactsWriter.WriteFile(check.DefaultArtifactsTarFileName, &buf) + if err != nil { + return fmt.Errorf("could not artifacts tar to artifacts dir: %w", err) + } - // writing the tar file to disk - _, err = artifactsWriter.WriteFile(check.DefaultArtifactsTarFileName, &buf) - if err != nil { - return fmt.Errorf("could not artifacts tar to artifacts dir: %w", err) + logger.Info("artifact tar written to disk", "filename", check.DefaultArtifactsTarFileName) } - - logger.Info("artifact tar written to disk", "filename", check.DefaultArtifactsTarFileName) } return nil @@ -251,6 +265,7 @@ func generateContainerCheckOptions(cfg *runtime.Config) []container.Option { // Always add PyxisHost, since the value is always set in viper config parsing. container.WithPyxisHost(cfg.PyxisHost), container.WithPlatform(cfg.Platform), + container.WithManifestListDigest(cfg.ManifestListDigest), } // set auth information if both are present in config. @@ -339,3 +354,90 @@ func artifactsTar(ctx context.Context, src string, w io.Writer) error { return nil } + +func platformsToBeProcessed(cmd *cobra.Command, cfg *runtime.Config) ([]string, error) { + ctx := cmd.Context() + logger := logr.FromContextOrDiscard(ctx) + + // flag.Changed is not set if the env is all that is set. + _, platformEnvPresent := os.LookupEnv("PFLT_PLATFORM") + platformChanged := cmd.Flags().Lookup("platform").Changed || platformEnvPresent + + containerImagePlatforms := []string{cfg.Platform} + + options := crane.GetOptions(option.GenerateCraneOptions(ctx, cfg)...) + ref, err := name.ParseReference(cfg.Image, options.Name...) + if err != nil { + return nil, fmt.Errorf("invalid image reference: %w", err) + } + + desc, err := remote.Get(ref, options.Remote...) + if err != nil { + return nil, fmt.Errorf("invalid manifest?: %w", err) + } + + if !desc.MediaType.IsIndex() { + // This means the passed image is just an image, and not a manifest list + // So, let's get the config to find out if the image matches the + // given platform. + img, err := desc.Image() + if err != nil { + return nil, fmt.Errorf("could not convert descriptor to image: %w", err) + } + cfgFile, err := img.ConfigFile() + if err != nil { + return nil, fmt.Errorf("could not retrieve image config: %w", err) + } + + // A specific arch was specified. This image does not contain that arch. + if cfgFile.Architecture != cfg.Platform && !platformChanged { + return nil, fmt.Errorf("cannot process image manifest of different arch without platform override") + } + + // At this point, we know that the original containerImagePlatform is correct, so + // we can just return it and skip the below. + // While we could just let this fall through to the end, I'd rather short-circuit + // here, in case any further changes disrupt that logic flow. + return containerImagePlatforms, nil + } + + // If platform param is not changed, it means that a platform was not specified on the + // command line. Therefore, we should process all platforms in the manifest list. + // As long as what is poinged to is a manifest list. Otherwise, it will just be the + // currnt runtime platform. + if desc.MediaType.IsIndex() { + logger.V(log.DBG).Info("manifest list detected, checking all platforms in manifest") + + idx, err := desc.ImageIndex() + if err != nil { + return nil, fmt.Errorf("could not convert descriptor to index: %w", err) + } + manifestListDigest, err := idx.Digest() + if err != nil { + return nil, fmt.Errorf("could not retrieve index digest: %w", err) + } + cfg.ManifestListDigest = manifestListDigest.String() + + manifest, err := idx.IndexManifest() + if err != nil { + return nil, fmt.Errorf("could not retrieve index manifest: %w", err) + } + + // Preflight was given a manifest list. --platform was not specified. + // Therefore, all platforms in the manifest list should be processed. + // Create a new slice since the original was for a single platform. + containerImagePlatforms = make([]string, 0, len(manifest.Manifests)) + for _, img := range manifest.Manifests { + if platformChanged && cfg.Platform != img.Platform.Architecture { + // The user selected a platform. If this isn't it, continue. + continue + } + containerImagePlatforms = append(containerImagePlatforms, img.Platform.Architecture) + } + if platformChanged && len(containerImagePlatforms) == 0 { + return nil, fmt.Errorf("invalid platform specified") + } + } + + return containerImagePlatforms, nil +} diff --git a/cmd/preflight/cmd/check_container_test.go b/cmd/preflight/cmd/check_container_test.go index 0c60d460..25c7de0a 100644 --- a/cmd/preflight/cmd/check_container_test.go +++ b/cmd/preflight/cmd/check_container_test.go @@ -4,8 +4,14 @@ import ( "bytes" "context" "errors" + "fmt" + "io" + "log" + "net/http/httptest" + "net/url" "os" "path/filepath" + "runtime" "github.com/redhat-openshift-ecosystem/openshift-preflight/artifacts" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification" @@ -16,22 +22,125 @@ import ( "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/viper" "github.com/go-logr/logr" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + cranev1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + cranev1types "github.com/google/go-containerregistry/pkg/v1/types" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" ) var _ = Describe("Check Container Command", func() { + var src string + var manifestListSrc string + var srcppc string + var manifests map[string]string + var s *httptest.Server + var u *url.URL + BeforeEach(func() { + manifests = make(map[string]string, 2) + // Set up a fake registry. + registryLogger := log.New(io.Discard, "", log.Ldate) + s = httptest.NewServer(registry.New(registry.Logger(registryLogger))) + DeferCleanup(s.Close) + + var err error + u, err = url.Parse(s.URL) + Expect(err).ToNot(HaveOccurred()) + + src = fmt.Sprintf("%s/test/crane", u.Host) + manifests["image"] = src + + // Expected values. + img, err := random.Image(1024, 5) + Expect(err).ToNot(HaveOccurred()) + + cfgFile, err := img.ConfigFile() + Expect(err).ToNot(HaveOccurred()) + + cfgFile.Architecture = "amd64" + + cfgImg, err := mutate.ConfigFile(img, cfgFile) + Expect(err).ToNot(HaveOccurred()) + + err = crane.Push(cfgImg, src) + Expect(err).ToNot(HaveOccurred()) + + srcppc = fmt.Sprintf("%s/test/craneppc", u.Host) + manifests["imageppc"] = srcppc + + newLayer, err := random.Layer(1024, cranev1types.OCILayer) + Expect(err).ToNot(HaveOccurred()) + + ppcImg, err := mutate.AppendLayers(img, newLayer) + Expect(err).ToNot(HaveOccurred()) + + ppcCfgFile, err := ppcImg.ConfigFile() + Expect(err).ToNot(HaveOccurred()) + + ppcCfgFile.Architecture = "ppc64le" + + ppcCfgImg, err := mutate.ConfigFile(ppcImg, ppcCfgFile) + Expect(err).ToNot(HaveOccurred()) + + Expect(crane.Push(ppcCfgImg, srcppc)).To(Succeed()) + + platforms := [4]string{"amd64", "arm64", "ppc64le", "s390x"} + manifestListSrc = fmt.Sprintf("%s/test/cranelist", u.Host) + manifests["index"] = manifestListSrc + lst, err := random.Index(1024, 5, int64(len(platforms))) + Expect(err).ToNot(HaveOccurred()) + + ref, err := name.ParseReference(manifestListSrc) + Expect(err).ToNot(HaveOccurred()) + + m, err := lst.IndexManifest() + Expect(err).ToNot(HaveOccurred()) + + for i, manifest := range m.Manifests { + switch { + case manifest.MediaType.IsImage(): + m.Manifests[i].Platform = &cranev1.Platform{ + Architecture: platforms[i], + OS: "linux", + } + } + } + err = remote.WriteIndex(ref, lst) + Expect(err).ToNot(HaveOccurred()) + }) BeforeEach(createAndCleanupDirForArtifactsAndLogs) - Context("when running the check container subcommand", func() { - Context("With all of the required parameters", func() { - It("should reach the core logic, but throw an error because of the placeholder values for the container image", func() { - _, err := executeCommand(checkContainerCmd(mockRunPreflightReturnNil), "example.com/example/image:mytag") - Expect(err).To(HaveOccurred()) + When("a manifest list is passed", func() { + When("default params otherwise", func() { + It("should not error", func() { + _, err := executeCommandWithLogger(checkContainerCmd(mockRunPreflightReturnNil), logr.Discard(), manifestListSrc) + Expect(err).ToNot(HaveOccurred()) }) }) }) + DescribeTable("--platform tests", + func(manifestKey string, platform string, match types.GomegaMatcher, includePlatformArg bool) { + args := []string{manifests[manifestKey]} + if includePlatformArg { + args = append(args, "--platform", platform) + } + _, err := executeCommandWithLogger(checkContainerCmd(mockRunPreflightReturnNil), logr.Discard(), args...) + Expect(err).To(match) + }, + Entry("image manifest, valid platform", "image", "amd64", Not(HaveOccurred()), true), + Entry("image manifest, different platform, modifier", "image", "none", Not(HaveOccurred()), true), + Entry("image manifest, different platform, no modifier", "imageppc", "none", HaveOccurred(), false), + Entry("index manifest, valid platform", "index", "amd64", Not(HaveOccurred()), true), + Entry("index manifest, invalid platform", "index", "none", HaveOccurred(), true), + ) + Context("When validating check container arguments and flags", func() { Context("and the user provided more than 1 positional arg", func() { It("should fail to run", func() { @@ -153,13 +262,13 @@ certification_project_id: mycertid` Context("when running the check container subcommand with a logger provided", func() { Context("with all of the required parameters", func() { It("should reach the core logic, and execute the mocked RunPreflight", func() { - _, err := executeCommandWithLogger(checkContainerCmd(mockRunPreflightReturnNil), logr.Discard(), "example.com/example/image:mytag") + _, err := executeCommandWithLogger(checkContainerCmd(mockRunPreflightReturnNil), logr.Discard(), src) Expect(err).ToNot(HaveOccurred()) }) }) Context("with all of the required parameters with error mocked", func() { It("should reach the core logic, and execute the mocked RunPreflight and return error", func() { - _, err := executeCommandWithLogger(checkContainerCmd(mockRunPreflightReturnErr), logr.Discard(), "example.com/example/image:mytag") + _, err := executeCommandWithLogger(checkContainerCmd(mockRunPreflightReturnErr), logr.Discard(), src) Expect(err).To(HaveOccurred()) }) }) @@ -172,12 +281,15 @@ certification_project_id: mycertid` Expect(err).ToNot(HaveOccurred()) DeferCleanup(os.RemoveAll, tmpDir) + platformDir := filepath.Join(tmpDir, runtime.GOARCH) + Expect(os.Mkdir(platformDir, 0o755)).Should(Succeed()) + // creating test files on in the tmpDir so the tar function has files to tar - f1, err := os.Create(filepath.Join(tmpDir, "test-file-1.json")) + f1, err := os.Create(filepath.Join(platformDir, "test-file-1.json")) Expect(err).ToNot(HaveOccurred()) defer f1.Close() - f2, err := os.Create(filepath.Join(tmpDir, "test-file-1.json")) + f2, err := os.Create(filepath.Join(platformDir, "test-file-1.json")) Expect(err).ToNot(HaveOccurred()) defer f2.Close() @@ -188,7 +300,7 @@ certification_project_id: mycertid` DeferCleanup(viper.Instance().Set, "offline", false) }) It("should reach core logic, and the additional offline logic", func() { - out, err := executeCommandWithLogger(checkContainerCmd(mockRunPreflightReturnNil), logr.Discard(), "example.com/example/image:mytag") + out, err := executeCommandWithLogger(checkContainerCmd(mockRunPreflightReturnNil), logr.Discard(), src) Expect(err).ToNot(HaveOccurred()) Expect(out).ToNot(BeNil()) }) @@ -199,12 +311,15 @@ certification_project_id: mycertid` Expect(err).ToNot(HaveOccurred()) DeferCleanup(os.RemoveAll, tmpDir) + platformDir := filepath.Join(tmpDir, runtime.GOARCH) + Expect(os.Mkdir(platformDir, 0o755)).Should(Succeed()) + // creating test files on in the tmpDir so the tar function has files to tar - f1, err := os.Create(filepath.Join(tmpDir, "test-file-1.json")) + f1, err := os.Create(filepath.Join(platformDir, "test-file-1.json")) Expect(err).ToNot(HaveOccurred()) defer f1.Close() - f2, err := os.Create(filepath.Join(tmpDir, "test-file-1.json")) + f2, err := os.Create(filepath.Join(platformDir, "test-file-1.json")) Expect(err).ToNot(HaveOccurred()) defer f2.Close() @@ -220,7 +335,7 @@ certification_project_id: mycertid` DeferCleanup(viper.Instance().Set, "offline", false) }) It("should reach the additional offline logic, and remove existing tar file", func() { - out, err := executeCommandWithLogger(checkContainerCmd(mockRunPreflightReturnNil), logr.Discard(), "example.com/example/image:mytag") + out, err := executeCommandWithLogger(checkContainerCmd(mockRunPreflightReturnNil), logr.Discard(), src) Expect(err).ToNot(HaveOccurred()) Expect(out).ToNot(BeNil()) }) diff --git a/cmd/preflight/cmd/support.go b/cmd/preflight/cmd/support.go index 91d50b3a..22ddc163 100644 --- a/cmd/preflight/cmd/support.go +++ b/cmd/preflight/cmd/support.go @@ -10,21 +10,21 @@ import ( ) const ( - baseURL = "https://connect.redhat.com/support/technology-partner/#/case/new?" - typeParam = "type" - typeValue = "CERT" - sourceParam = "source" - sourceValue = "preflight" - certProjectTypeParam = "cert_project_type" - certProjectIDParam = "cert_project_id" - pullRequestURLParam = "pull_request_url" - operatorBundleImage = "Operator Bundle Image" - containerImage = "Container Image" + baseURL = "https://connect.redhat.com/support/technology-partner/#/case/new?" + typeParam = "type" + typeValue = "CERT" + sourceParam = "source" + sourceValue = "preflight" + certProjectTypeParam = "cert_project_type" + certProjectIDParam = "cert_project_id" + pullRequestURLParam = "pull_request_url" + operatorBundleImageText = "Operator Bundle Image" + containerImageText = "Container Image" ) var projectTypeMapping = map[string]string{ - "container": containerImage, - "operator": operatorBundleImage, + "container": containerImageText, + "operator": operatorBundleImageText, } func supportCmd() *cobra.Command { diff --git a/container/check_container.go b/container/check_container.go index c697483d..88d57f62 100644 --- a/container/check_container.go +++ b/container/check_container.go @@ -71,12 +71,13 @@ func (c *containerCheck) Run(ctx context.Context) (certification.Results, error) } cfg := runtime.Config{ - Image: c.image, - DockerConfig: c.dockerconfigjson, - Scratch: pol == policy.PolicyScratch, - Bundle: false, - Insecure: c.insecure, - Platform: c.platform, + Image: c.image, + DockerConfig: c.dockerconfigjson, + Scratch: pol == policy.PolicyScratch, + Bundle: false, + Insecure: c.insecure, + Platform: c.platform, + ManifestListDigest: c.manifestListDigest, } eng, err := engine.New(ctx, checks, nil, cfg) if err != nil { @@ -151,6 +152,15 @@ func WithInsecureConnection() Option { } } +// WithManifestListDigest signifies that we have a manifest list and should add +// this digest to any Pyxis calls. +// This is only valid when submitting to Pyxis. Otherwise, it will be ignored. +func WithManifestListDigest(manifestListDigest string) Option { + return func(cc *containerCheck) { + cc.manifestListDigest = manifestListDigest + } +} + type containerCheck struct { image string dockerconfigjson string @@ -159,4 +169,5 @@ type containerCheck struct { pyxisHost string platform string insecure bool + manifestListDigest string } diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 47ed4e93..43985029 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -50,14 +50,15 @@ func New(ctx context.Context, cfg runtime.Config, ) (craneEngine, error) { return craneEngine{ - kubeconfig: kubeconfig, - dockerConfig: cfg.DockerConfig, - image: cfg.Image, - checks: checks, - isBundle: cfg.Bundle, - isScratch: cfg.Scratch, - platform: cfg.Platform, - insecure: cfg.Insecure, + kubeconfig: kubeconfig, + dockerConfig: cfg.DockerConfig, + image: cfg.Image, + checks: checks, + isBundle: cfg.Bundle, + isScratch: cfg.Scratch, + platform: cfg.Platform, + insecure: cfg.Insecure, + manifestListDigest: cfg.ManifestListDigest, }, nil } @@ -88,6 +89,9 @@ type craneEngine struct { // the registry crane connects with. insecure bool + // ManifestListDigest is the sha256 digest for the manifest list + manifestListDigest string + imageRef image.ImageReference results certification.Results } @@ -179,12 +183,13 @@ func (c *craneEngine) ExecuteChecks(ctx context.Context) error { // store the image internals in the engine image reference to pass to validations. c.imageRef = image.ImageReference{ - ImageURI: c.image, - ImageFSPath: containerFSPath, - ImageInfo: img, - ImageRegistry: reference.Context().RegistryStr(), - ImageRepository: reference.Context().RepositoryStr(), - ImageTagOrSha: reference.Identifier(), + ImageURI: c.image, + ImageFSPath: containerFSPath, + ImageInfo: img, + ImageRegistry: reference.Context().RegistryStr(), + ImageRepository: reference.Context().RepositoryStr(), + ImageTagOrSha: reference.Identifier(), + ManifestListDigest: c.manifestListDigest, } if err := writeCertImage(ctx, c.imageRef); err != nil { @@ -498,10 +503,11 @@ func writeCertImage(ctx context.Context, imageRef image.ImageReference) error { repositories := make([]pyxis.Repository, 0, 1) repositories = append(repositories, pyxis.Repository{ - PushDate: addedDate, - Registry: imageRef.ImageRegistry, - Repository: imageRef.ImageRepository, - Tags: tags, + PushDate: addedDate, + Registry: imageRef.ImageRegistry, + Repository: imageRef.ImageRepository, + Tags: tags, + ManifestListDigest: imageRef.ManifestListDigest, }) certImage := pyxis.CertImage{ diff --git a/internal/image/types.go b/internal/image/types.go index 8f7476a3..39a3b49c 100644 --- a/internal/image/types.go +++ b/internal/image/types.go @@ -4,10 +4,11 @@ import v1 "github.com/google/go-containerregistry/pkg/v1" // ImageReference holds all things image-related type ImageReference struct { - ImageURI string - ImageFSPath string - ImageInfo v1.Image - ImageRepository string - ImageRegistry string - ImageTagOrSha string + ImageURI string + ImageFSPath string + ImageInfo v1.Image + ImageRepository string + ImageRegistry string + ImageTagOrSha string + ManifestListDigest string } diff --git a/internal/pyxis/types.go b/internal/pyxis/types.go index 9d8351be..d2745059 100644 --- a/internal/pyxis/types.go +++ b/internal/pyxis/types.go @@ -60,11 +60,12 @@ type ParsedData struct { } type Repository struct { - Published bool `json:"published" default:"false"` - PushDate string `json:"push_date,omitempty"` // time.Now - Registry string `json:"registry,omitempty"` - Repository string `json:"repository,omitempty"` - Tags []Tag `json:"tags,omitempty"` + Published bool `json:"published" default:"false"` + PushDate string `json:"push_date,omitempty"` // time.Now + Registry string `json:"registry,omitempty"` + Repository string `json:"repository,omitempty"` + Tags []Tag `json:"tags,omitempty"` + ManifestListDigest string `json:"manifest_list_digest,omitempty"` } type Label struct { diff --git a/internal/runtime/config.go b/internal/runtime/config.go index 719cc5aa..9fa991cd 100644 --- a/internal/runtime/config.go +++ b/internal/runtime/config.go @@ -28,6 +28,7 @@ type Config struct { Platform string Insecure bool Offline bool + ManifestListDigest string // Operator-Specific Fields Namespace string ServiceAccount string diff --git a/internal/runtime/config_test.go b/internal/runtime/config_test.go index fd432a8e..4593cc33 100644 --- a/internal/runtime/config_test.go +++ b/internal/runtime/config_test.go @@ -64,12 +64,12 @@ var _ = Describe("Viper to Runtime Config", func() { }) }) - It("should only have 23 struct keys for tests to be valid", func() { + It("should only have 24 struct keys for tests to be valid", func() { // If this test fails, it means a developer has added or removed // keys from runtime.Config, and so these tests may no longer be // accurate in confirming that the derived configuration from viper // matches. keys := reflect.TypeOf(Config{}).NumField() - Expect(keys).To(Equal(23)) + Expect(keys).To(Equal(24)) }) })