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)) }) })