Repository: redhat-openshift-ecosystem/openshift-preflight
Pull request #751 (2022-08-02 17:42:46)

Refactor cmd/ to have less coupling
No more inits are being used in the individual command files.
The one that remains is to make sure that initConfig is called 
when cobra is initialized.

This should increase coverage slightly, and make the commands less
interdependent.

Signed-off-by: Brad P. Crochet <brad@redhat.com>

openshift-ci[bot]: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/751#" title="Author self-approved">bcrochet</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=redhat-openshift-ecosystem%2Fopenshift-preflight).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/redhat-openshift-ecosystem/openshift-preflight/blob/main/OWNERS)~~ [bcrochet]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
coveralls: 
[![Coverage Status](https://coveralls.io/builds/51360179/badge)](https://coveralls.io/builds/51360179)

Coverage increased (+0.2%) to 84.582% when pulling **20e95745cbde5c752ca4f8d8f1e269e21b35787d on bcrochet:cmd-refactor** into **1052865efcdce850fe8f70c6d0cc7eae6997e47b on redhat-openshift-ecosystem:main**.

dcibot: -  SUCCESS https://www.distributed-ci.io/jobs/65c06d60-b951-4627-bdb4-ab35f5c7f9c8/jobStates
 from https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/751
dcibot: -  SUCCESS https://www.distributed-ci.io/jobs/92c6980e-ff69-48f9-8641-7fd1ba60d6d4/jobStates
 from https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/751
dcibot: -  SUCCESS https://www.distributed-ci.io/jobs/8866adcd-ba86-42ef-8772-890913bcc7ca/jobStates
 from https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/751
bcrochet: /retest
dcibot: -  FAILURE https://www.distributed-ci.io/jobs/3ccee6cf-50e2-4426-a2aa-25d0e7a12f03/jobStates
 from https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/751
openshift-ci[bot]: @bcrochet: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.8-e2e | 6063d89cedeaf0d2dffdb980c69b373110821da9 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-openshift-ecosystem_openshift-preflight/751/pull-ci-redhat-openshift-ecosystem-openshift-preflight-main-4.8-e2e/1555219148991107072) | true | `/test 4.8-e2e`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-openshift-ecosystem&repo=openshift-preflight&pr=751). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:bcrochet).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
dcibot: -  SUCCESS https://www.distributed-ci.io/jobs/4ffdfa34-34da-4d56-b8cd-3a8df5c58304/jobStates
 from https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/751
dcibot: -  SUCCESS https://www.distributed-ci.io/jobs/03760e52-5986-468a-9302-760a7eb27d82/jobStates
 from https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/751

Patch:
diff --git a/certification/engine/engine.go b/certification/engine/engine.go
index 87ff1be..2298ec4 100644
--- a/certification/engine/engine.go
+++ b/certification/engine/engine.go
@@ -63,6 +63,7 @@ func initializeChecks(ctx context.Context, p policy.Policy, cfg certification.Co
 				&http.Client{Timeout: 60 * time.Second}),
 			),
 			operatorpol.NewSecurityContextConstraintsCheck(),
+			&operatorpol.RelatedImagesCheck{},
 		}, nil
 	case policy.PolicyContainer:
 		return []certification.Check{
diff --git a/certification/internal/policy/operator/certified_images.go b/certification/internal/policy/operator/certified_images.go
index 49d7c9a..ab86a9c 100644
--- a/certification/internal/policy/operator/certified_images.go
+++ b/certification/internal/policy/operator/certified_images.go
@@ -73,7 +73,18 @@ func (p *certifiedImagesCheck) validate(ctx context.Context, imageDigests []stri
 		return false, err
 	}
 
+	foundMap := make(map[string]pyxis.CertImage, len(pyxisImages))
 	for _, img := range pyxisImages {
+		foundMap[img.DockerImageDigest] = img
+	}
+
+	for _, digest := range imageDigests {
+		img, ok := foundMap[digest]
+		if !ok {
+			log.Warningf("Image not found in Pyxis, therefore it is not certified: %s", digest)
+			p.nonCertifiedImages = append(p.nonCertifiedImages, digest)
+			continue
+		}
 		if !img.Certified {
 			fullImg := fmt.Sprintf("%s/%s@%s", img.Repositories[0].Registry, img.Repositories[0].Repository, img.DockerImageDigest)
 			log.Warningf("Image is not certified: %s", fullImg)
diff --git a/cmd/check.go b/cmd/check.go
index eae609b..ff6f12f 100644
--- a/cmd/check.go
+++ b/cmd/check.go
@@ -13,13 +13,13 @@ import (
 	"github.com/spf13/viper"
 )
 
-var checkCmd = &cobra.Command{
-	Use:   "check",
-	Short: "Run checks for an operator or container",
-	Long:  "This command will allow you to execute the Red Hat Certification tests for an operator or a container.",
-}
+func checkCmd() *cobra.Command {
+	checkCmd := &cobra.Command{
+		Use:   "check",
+		Short: "Run checks for an operator or container",
+		Long:  "This command will allow you to execute the Red Hat Certification tests for an operator or a container.",
+	}
 
-func init() {
 	checkCmd.PersistentFlags().StringP("docker-config", "d", "", "Path to docker config.json file. This value is optional for publicly accessible images.\n"+
 		"However, it is strongly encouraged for public Docker Hub images,\n"+
 		"due to the rate limit imposed for unauthenticated requests. (env: PFLT_DOCKERCONFIG)")
@@ -28,7 +28,10 @@ func init() {
 	checkCmd.PersistentFlags().String("artifacts", "", "Where check-specific artifacts will be written. (env: PFLT_ARTIFACTS)")
 	viper.BindPFlag("artifacts", checkCmd.PersistentFlags().Lookup("artifacts"))
 
-	rootCmd.AddCommand(checkCmd)
+	checkCmd.AddCommand(checkContainerCmd())
+	checkCmd.AddCommand(checkOperatorCmd())
+
+	return checkCmd
 }
 
 // writeJUnit will write results as JUnit XML using the built-in formatter.
@@ -61,7 +64,8 @@ func resultsFilenameWithExtension(ext string) string {
 func buildConnectURL(projectID string) string {
 	connectURL := fmt.Sprintf("https://connect.redhat.com/projects/%s", projectID)
 
-	if viper.GetString("pyxis_env") != "prod" {
+	pyxis_env := viper.GetString("pyxis_env")
+	if len(pyxis_env) > 0 && pyxis_env != "prod" {
 		connectURL = fmt.Sprintf("https://connect.%s.redhat.com/projects/%s", viper.GetString("pyxis_env"), projectID)
 	}
 
diff --git a/cmd/check_container.go b/cmd/check_container.go
index 4f6fd37..3c66e54 100644
--- a/cmd/check_container.go
+++ b/cmd/check_container.go
@@ -17,14 +17,35 @@ import (
 
 var submit bool
 
-var checkContainerCmd = &cobra.Command{
-	Use:   "container",
-	Short: "Run checks for a container",
-	Long:  `This command will run the Certification checks for a container image. `,
-	Args:  checkContainerPositionalArgs,
-	// this fmt.Sprintf is in place to keep spacing consistent with cobras two spaces that's used in: Usage, Flags, etc
-	Example: fmt.Sprintf("  %s", "preflight check container quay.io/repo-name/container-name:version"),
-	RunE:    checkContainerRunE,
+func checkContainerCmd() *cobra.Command {
+	checkContainerCmd := &cobra.Command{
+		Use:   "container",
+		Short: "Run checks for a container",
+		Long:  `This command will run the Certification checks for a container image. `,
+		Args:  checkContainerPositionalArgs,
+		// this fmt.Sprintf is in place to keep spacing consistent with cobras two spaces that's used in: Usage, Flags, etc
+		Example: fmt.Sprintf("  %s", "preflight check container quay.io/repo-name/container-name:version"),
+		RunE:    checkContainerRunE,
+	}
+
+	checkContainerCmd.Flags().BoolVarP(&submit, "submit", "s", false, "submit check container results to red hat")
+	viper.BindPFlag("submit", checkContainerCmd.Flags().Lookup("submit"))
+
+	checkContainerCmd.Flags().String("pyxis-api-token", "", "API token for Pyxis authentication (env: PFLT_PYXIS_API_TOKEN)")
+	viper.BindPFlag("pyxis_api_token", checkContainerCmd.Flags().Lookup("pyxis-api-token"))
+
+	checkContainerCmd.Flags().String("pyxis-host", "", fmt.Sprintf("Host to use for Pyxis submissions. This will override Pyxis Env. Only set this if you know what you are doing.\n"+
+		"If you do set it, it should include just the host, and the URI path. (env: PFLT_PYXIS_HOST)"))
+	viper.BindPFlag("pyxis_host", checkContainerCmd.Flags().Lookup("pyxis-host"))
+
+	checkContainerCmd.Flags().String("pyxis-env", certification.DefaultPyxisEnv, "Env to use for Pyxis submissions.")
+	viper.BindPFlag("pyxis_env", checkContainerCmd.Flags().Lookup("pyxis-env"))
+
+	checkContainerCmd.Flags().String("certification-project-id", "", fmt.Sprintf("Certification Project ID from connect.redhat.com/projects/{certification-project-id}/overview\n"+
+		"URL paramater. This value may differ from the PID on the overview page. (env: PFLT_CERTIFICATION_PROJECT_ID)"))
+	viper.BindPFlag("certification_project_id", checkContainerCmd.Flags().Lookup("certification-project-id"))
+
+	return checkContainerCmd
 }
 
 // checkContainerRunner contains all of the components necessary to run checkContainer.
@@ -162,24 +183,3 @@ func checkContainerPositionalArgs(cmd *cobra.Command, args []string) error {
 
 	return nil
 }
-
-func init() {
-	checkContainerCmd.Flags().BoolVarP(&submit, "submit", "s", false, "submit check container results to red hat")
-	viper.BindPFlag("submit", checkContainerCmd.Flags().Lookup("submit"))
-
-	checkContainerCmd.Flags().String("pyxis-api-token", "", "API token for Pyxis authentication (env: PFLT_PYXIS_API_TOKEN)")
-	viper.BindPFlag("pyxis_api_token", checkContainerCmd.Flags().Lookup("pyxis-api-token"))
-
-	checkContainerCmd.Flags().String("pyxis-host", "", fmt.Sprintf("Host to use for Pyxis submissions. This will override Pyxis Env. Only set this if you know what you are doing.\n"+
-		"If you do set it, it should include just the host, and the URI path. (env: PFLT_PYXIS_HOST)"))
-	viper.BindPFlag("pyxis_host", checkContainerCmd.Flags().Lookup("pyxis-host"))
-
-	checkContainerCmd.Flags().String("pyxis-env", certification.DefaultPyxisEnv, "Env to use for Pyxis submissions.")
-	viper.BindPFlag("pyxis_env", checkContainerCmd.Flags().Lookup("pyxis-env"))
-
-	checkContainerCmd.Flags().String("certification-project-id", "", fmt.Sprintf("Certification Project ID from connect.redhat.com/projects/{certification-project-id}/overview\n"+
-		"URL paramater. This value may differ from the PID on the overview page. (env: PFLT_CERTIFICATION_PROJECT_ID)"))
-	viper.BindPFlag("certification_project_id", checkContainerCmd.Flags().Lookup("certification-project-id"))
-
-	checkCmd.AddCommand(checkContainerCmd)
-}
diff --git a/cmd/check_operator.go b/cmd/check_operator.go
index d5a4bbc..37e701b 100644
--- a/cmd/check_operator.go
+++ b/cmd/check_operator.go
@@ -15,14 +15,36 @@ import (
 	"github.com/spf13/viper"
 )
 
-var checkOperatorCmd = &cobra.Command{
-	Use:   "operator",
-	Short: "Run checks for an Operator",
-	Long:  `This command will run the Certification checks for an Operator bundle image. `,
-	Args:  checkOperatorPositionalArgs,
-	// this fmt.Sprintf is in place to keep spacing consistent with cobras two spaces that's used in: Usage, Flags, etc
-	Example: fmt.Sprintf("  %s", "preflight check operator quay.io/repo-name/operator-bundle:version"),
-	RunE:    checkOperatorRunE,
+func checkOperatorCmd() *cobra.Command {
+	checkOperatorCmd := &cobra.Command{
+		Use:   "operator",
+		Short: "Run checks for an Operator",
+		Long:  `This command will run the Certification checks for an Operator bundle image. `,
+		Args:  checkOperatorPositionalArgs,
+		// this fmt.Sprintf is in place to keep spacing consistent with cobras two spaces that's used in: Usage, Flags, etc
+		Example: fmt.Sprintf("  %s", "preflight check operator quay.io/repo-name/operator-bundle:version"),
+		RunE:    checkOperatorRunE,
+	}
+	checkOperatorCmd.Flags().String("namespace", "", "The namespace to use when running OperatorSDK Scorecard. (env: PFLT_NAMESPACE)")
+	viper.BindPFlag("namespace", checkOperatorCmd.Flags().Lookup("namespace"))
+
+	checkOperatorCmd.Flags().String("serviceaccount", "", "The service account to use when running OperatorSDK Scorecard. (env: PFLT_SERVICEACCOUNT)")
+	viper.BindPFlag("serviceaccount", checkOperatorCmd.Flags().Lookup("serviceaccount"))
+
+	checkOperatorCmd.Flags().String("scorecard-image", "", "A uri that points to the scorecard image digest, used in disconnected environments.\n"+
+		"It should only be used in a disconnected environment. Use preflight runtime-assets on a connected \n"+
+		"workstation to generate the digest that needs to be mirrored. (env: PFLT_SCORECARD_IMAGE)")
+	viper.BindPFlag("scorecard_image", checkOperatorCmd.Flags().Lookup("scorecard-image"))
+
+	checkOperatorCmd.Flags().String("scorecard-wait-time", "", "A time value that will be passed to scorecard's --wait-time environment variable.\n"+
+		"(env: PFLT_SCORECARD_WAIT_TIME)")
+	viper.BindPFlag("scorecard_wait_time", checkOperatorCmd.Flags().Lookup("scorecard-wait-time"))
+
+	checkOperatorCmd.Flags().String("channel", "", "The name of the operator channel which is used by DeployableByOLM to deploy the operator.\n"+
+		"If empty, the default operator channel in bundle's annotations file is used.. (env: PFLT_CHANNEL)")
+	viper.BindPFlag("channel", checkOperatorCmd.Flags().Lookup("channel"))
+
+	return checkOperatorCmd
 }
 
 // checkOperatorRunner contains all of the components necessary to run checkOperator.
@@ -125,26 +147,3 @@ func checkOperatorPositionalArgs(cmd *cobra.Command, args []string) error {
 
 	return nil
 }
-
-func init() {
-	checkOperatorCmd.Flags().String("namespace", "", "The namespace to use when running OperatorSDK Scorecard. (env: PFLT_NAMESPACE)")
-	viper.BindPFlag("namespace", checkOperatorCmd.Flags().Lookup("namespace"))
-
-	checkOperatorCmd.Flags().String("serviceaccount", "", "The service account to use when running OperatorSDK Scorecard. (env: PFLT_SERVICEACCOUNT)")
-	viper.BindPFlag("serviceaccount", checkOperatorCmd.Flags().Lookup("serviceaccount"))
-
-	checkOperatorCmd.Flags().String("scorecard-image", "", "A uri that points to the scorecard image digest, used in disconnected environments.\n"+
-		"It should only be used in a disconnected environment. Use preflight runtime-assets on a connected \n"+
-		"workstation to generate the digest that needs to be mirrored. (env: PFLT_SCORECARD_IMAGE)")
-	viper.BindPFlag("scorecard_image", checkOperatorCmd.Flags().Lookup("scorecard-image"))
-
-	checkOperatorCmd.Flags().String("scorecard-wait-time", "", "A time value that will be passed to scorecard's --wait-time environment variable.\n"+
-		"(env: PFLT_SCORECARD_WAIT_TIME)")
-	viper.BindPFlag("scorecard_wait_time", checkOperatorCmd.Flags().Lookup("scorecard-wait-time"))
-
-	checkOperatorCmd.Flags().String("channel", "", "The name of the operator channel which is used by DeployableByOLM to deploy the operator.\n"+
-		"If empty, the default operator channel in bundle's annotations file is used.. (env: PFLT_CHANNEL)")
-	viper.BindPFlag("channel", checkOperatorCmd.Flags().Lookup("channel"))
-
-	checkCmd.AddCommand(checkOperatorCmd)
-}
diff --git a/cmd/list_checks.go b/cmd/list_checks.go
index cca0bad..c73c24f 100644
--- a/cmd/list_checks.go
+++ b/cmd/list_checks.go
@@ -10,11 +10,14 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var listChecksCmd = &cobra.Command{
-	Use:   "list-checks",
-	Short: "List all checks that will be executed for each policy",
-	Long:  "This command will list all checks that preflight uses against an asset by policy type",
-	Run:   listChecksRunFunc,
+func listChecksCmd() *cobra.Command {
+	listChecksCmd := &cobra.Command{
+		Use:   "list-checks",
+		Short: "List all checks that will be executed for each policy",
+		Long:  "This command will list all checks that preflight uses against an asset by policy type",
+		Run:   listChecksRunFunc,
+	}
+	return listChecksCmd
 }
 
 // listChecksRunFunc binds printChecks to cobra's Run function
@@ -57,7 +60,3 @@ func formatList(list []string) string {
 func dashPrefix(s string) string {
 	return fmt.Sprintf("- %s", s)
 }
-
-func init() {
-	rootCmd.AddCommand(listChecksCmd)
-}
diff --git a/cmd/root.go b/cmd/root.go
index adbc897..b754cdc 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -15,30 +15,36 @@ import (
 
 var configFileUsed bool
 
-var rootCmd = &cobra.Command{
-	Use:              "preflight",
-	Short:            "Preflight Red Hat certification prep tool.",
-	Long:             "A utility that allows you to pre-test your bundles, operators, and container before submitting for Red Hat Certification.",
-	Version:          version.Version.String(),
-	Args:             cobra.MinimumNArgs(1),
-	PersistentPreRun: preRunConfig,
-}
-
 func init() {
 	cobra.OnInitialize(initConfig)
+}
+
+func rootCmd() *cobra.Command {
+	rootCmd := &cobra.Command{
+		Use:              "preflight",
+		Short:            "Preflight Red Hat certification prep tool.",
+		Long:             "A utility that allows you to pre-test your bundles, operators, and container before submitting for Red Hat Certification.",
+		Version:          version.Version.String(),
+		Args:             cobra.MinimumNArgs(1),
+		PersistentPreRun: preRunConfig,
+	}
 
 	rootCmd.PersistentFlags().String("logfile", "", "Where the execution logfile will be written. (env: PFLT_LOGFILE)")
 	viper.BindPFlag("logfile", rootCmd.PersistentFlags().Lookup("logfile"))
 
 	rootCmd.PersistentFlags().String("loglevel", "", "The verbosity of the preflight tool itself. Ex. warn, debug, trace, info, error. (env: PFLT_LOGLEVEL)")
 	viper.BindPFlag("loglevel", rootCmd.PersistentFlags().Lookup("loglevel"))
+
+	rootCmd.AddCommand(checkCmd())
+	rootCmd.AddCommand(listChecksCmd())
+	rootCmd.AddCommand(runtimeAssetsCmd())
+	rootCmd.AddCommand(supportCmd())
+
+	return rootCmd
 }
 
-func Execute() {
-	if err := rootCmd.ExecuteContext(context.Background()); err != nil {
-		log.Fatal(err)
-		os.Exit(-1)
-	}
+func Execute() error {
+	return rootCmd().ExecuteContext(context.Background())
 }
 
 func initConfig() {
diff --git a/cmd/runtime_assets.go b/cmd/runtime_assets.go
index 7c17af2..9caede7 100644
--- a/cmd/runtime_assets.go
+++ b/cmd/runtime_assets.go
@@ -10,11 +10,15 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var runtimeAssetsCmd = &cobra.Command{
-	Use:   "runtime-assets",
-	Short: "Returns information about assets used at runtime.",
-	Long:  `This command will return information on all runtime assets used by preflight. Useful for preparing a disconnected environment intending to utilize preflight.`,
-	RunE:  runtimeAssetsRunE,
+func runtimeAssetsCmd() *cobra.Command {
+	runtimeAssetsCmd := &cobra.Command{
+		Use:   "runtime-assets",
+		Short: "Returns information about assets used at runtime.",
+		Long:  `This command will return information on all runtime assets used by preflight. Useful for preparing a disconnected environment intending to utilize preflight.`,
+		RunE:  runtimeAssetsRunE,
+	}
+
+	return runtimeAssetsCmd
 }
 
 func runtimeAssetsRunE(cmd *cobra.Command, args []string) error {
@@ -47,7 +51,3 @@ func prettyPrintJSON(v interface{}) (string, error) {
 
 	return string(json), nil
 }
-
-func init() {
-	rootCmd.AddCommand(runtimeAssetsCmd)
-}
diff --git a/cmd/support.go b/cmd/support.go
index dbb47cf..91d50b3 100644
--- a/cmd/support.go
+++ b/cmd/support.go
@@ -1,4 +1,3 @@
-// go:test !race
 package cmd
 
 import (
@@ -28,35 +27,17 @@ var projectTypeMapping = map[string]string{
 	"operator":  operatorBundleImage,
 }
 
-var supportCmd = &cobra.Command{
-	Use:   "support <operator|container> <your project ID> [pullRequestURL if operator project]",
-	Short: "Creates a support request",
-	Args: cobra.MatchAll(
-		cobra.MinimumNArgs(2),
-		cobra.MaximumNArgs(3)),
-	Long: `Generate a URL that can be used to open a ticket with Red Hat Support if you're having an issue passing certification checks.`,
-	RunE: supportRunE,
-}
-
-func supportRunE(cmd *cobra.Command, args []string) error {
-	// the last positional argument is optional, so
-	// if the user didn't provide it, we'll make it an empty
-	// string.
-	if len(args) == 2 {
-		args = append(args, "")
+func supportCmd() *cobra.Command {
+	supportCmd := &cobra.Command{
+		Use:   "support <operator|container> <your project ID> [pullRequestURL if operator project]",
+		Short: "Creates a support request",
+		Long:  `Generate a URL that can be used to open a ticket with Red Hat Support if you're having an issue passing certification checks.`,
 	}
 
-	ptype := args[0]
-	pid := args[1]
-	prurl := args[2]
+	supportCmd.AddCommand(supportOperatorCmd())
+	supportCmd.AddCommand(supportContainerCmd())
 
-	support, err := newSupportTextGenerator(ptype, pid, prurl)
-	if err != nil {
-		return err
-	}
-
-	fmt.Fprint(cmd.OutOrStdout(), support.Generate())
-	return nil
+	return supportCmd
 }
 
 type supportTextGenerator struct {
@@ -107,10 +88,6 @@ func (g *supportTextGenerator) Generate() string {
 }
 
 func (g *supportTextGenerator) validate() error {
-	if err := projectTypeValidation(g.ProjectType); err != nil {
-		return err
-	}
-
 	if err := projectIDValidation(g.ProjectID); err != nil {
 		return err
 	}
@@ -168,15 +145,3 @@ func projectIDValidation(id string) error {
 
 	return nil
 }
-
-func projectTypeValidation(s string) error {
-	if !(s == "operator" || s == "container") {
-		return fmt.Errorf(`the project type must be "operator" or "container, received %s"`, s)
-	}
-
-	return nil
-}
-
-func init() {
-	rootCmd.AddCommand(supportCmd)
-}
diff --git a/cmd/support_container.go b/cmd/support_container.go
new file mode 100644
index 0000000..a7a6a0e
--- /dev/null
+++ b/cmd/support_container.go
@@ -0,0 +1,34 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+func supportContainerCmd() *cobra.Command {
+	supportContainer := &cobra.Command{
+		Use:   "container <your project ID>",
+		Short: "Creates a support request",
+		Args:  cobra.ExactArgs(1),
+		Long:  `Generate a URL that can be used to open a ticket with Red Hat Support if you're having an issue passing certification checks.`,
+		RunE:  supportContainerRunE,
+	}
+
+	return supportContainer
+}
+
+func supportContainerRunE(cmd *cobra.Command, args []string) error {
+	pid := args[0]
+	// prurl is not needed for container support
+	prurl := ""
+	ptype := "container"
+
+	support, err := newSupportTextGenerator(ptype, pid, prurl)
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprint(cmd.OutOrStdout(), support.Generate())
+	return nil
+}
diff --git a/cmd/support_operator.go b/cmd/support_operator.go
new file mode 100644
index 0000000..9a23d4a
--- /dev/null
+++ b/cmd/support_operator.go
@@ -0,0 +1,33 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+func supportOperatorCmd() *cobra.Command {
+	supportOperator := &cobra.Command{
+		Use:   "operator <your project ID> <pullRequestURL>",
+		Short: "Creates a support request for an operator",
+		Args:  cobra.ExactArgs(2),
+		Long:  `Generate a URL that can be used to open a ticket with Red Hat Support if you're having an issue passing certification checks.`,
+		RunE:  supportOperatorRunE,
+	}
+
+	return supportOperator
+}
+
+func supportOperatorRunE(cmd *cobra.Command, args []string) error {
+	pid := args[0]
+	prurl := args[1]
+	ptype := "operator"
+
+	support, err := newSupportTextGenerator(ptype, pid, prurl)
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprint(cmd.OutOrStdout(), support.Generate())
+	return nil
+}
diff --git a/main.go b/main.go
index 6e8e570..8f4086b 100644
--- a/main.go
+++ b/main.go
@@ -1,9 +1,13 @@
 package main
 
 import (
+	"log"
+
 	"github.com/redhat-openshift-ecosystem/openshift-preflight/cmd"
 )
 
 func main() {
-	cmd.Execute()
+	if err := cmd.Execute(); err != nil {
+		log.Fatal(err)
+	}
 }


Test patch:
diff --git a/certification/internal/policy/operator/certified_images_test.go b/certification/internal/policy/operator/certified_images_test.go
index e5b6d56..4e694af 100644
--- a/certification/internal/policy/operator/certified_images_test.go
+++ b/certification/internal/policy/operator/certified_images_test.go
@@ -26,6 +26,16 @@ func (c *certifiedImageFinder) FindImagesByDigest(ctx context.Context, digests [
 		},
 		DockerImageDigest: "sha256:f000432f07cd187469f0310e3ed9dcf9a5db2be14b8bab9c5293dd1ee8518176",
 	})
+	matchingImages = append(matchingImages, pyxis.CertImage{
+		Certified: true,
+		Repositories: []pyxis.Repository{
+			{
+				Registry:   "registry.example.io",
+				Repository: "foo/proxy",
+			},
+		},
+		DockerImageDigest: "sha256:5e33f9d095952866b9743cc8268fb740cce6d93439f00ce333a2de1e5974837e",
+	})
 	return matchingImages, nil
 }
 
@@ -43,6 +53,33 @@ func (c *uncertifiedImageFinder) FindImagesByDigest(ctx context.Context, digests
 		},
 		DockerImageDigest: "sha256:f000432f07cd187469f0310e3ed9dcf9a5db2be14b8bab9c5293dd1ee8518176",
 	})
+	matchingImages = append(matchingImages, pyxis.CertImage{
+		Certified: true,
+		Repositories: []pyxis.Repository{
+			{
+				Registry:   "registry.example.io",
+				Repository: "foo/proxy",
+			},
+		},
+		DockerImageDigest: "sha256:5e33f9d095952866b9743cc8268fb740cce6d93439f00ce333a2de1e5974837e",
+	})
+	return matchingImages, nil
+}
+
+type missingImageFinder struct{}
+
+func (c *missingImageFinder) FindImagesByDigest(ctx context.Context, digests []string) ([]pyxis.CertImage, error) {
+	var matchingImages []pyxis.CertImage
+	matchingImages = append(matchingImages, pyxis.CertImage{
+		Certified: true,
+		Repositories: []pyxis.Repository{
+			{
+				Registry:   "registry.example.io",
+				Repository: "foo/bar",
+			},
+		},
+		DockerImageDigest: "sha256:f000432f07cd187469f0310e3ed9dcf9a5db2be14b8bab9c5293dd1ee8518176",
+	})
 	return matchingImages, nil
 }
 
@@ -113,6 +150,18 @@ spec:
 			Expect(certifiedImagesCheck.nonCertifiedImages).To(HaveLen(1))
 		})
 	})
+	When("an image in the CSV is not in Pyxis", func() {
+		AfterEach(func() {
+			certifiedImagesCheck.imageFinder = &certifiedImageFinder{}
+		})
+		It("should still succeed", func() {
+			certifiedImagesCheck.imageFinder = &missingImageFinder{}
+			result, err := certifiedImagesCheck.Validate(context.TODO(), imageRef)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(result).To(BeTrue())
+			Expect(certifiedImagesCheck.nonCertifiedImages).To(HaveLen(1))
+		})
+	})
 	When("there is no CSV", func() {
 		It("should fail", func() {
 			Expect(os.RemoveAll(imageRef.ImageFSPath)).To(Succeed())
diff --git a/cmd/check_container_test.go b/cmd/check_container_test.go
index a2430f5..31589f4 100644
--- a/cmd/check_container_test.go
+++ b/cmd/check_container_test.go
@@ -26,7 +26,7 @@ var _ = Describe("Check Container Command", func() {
 	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(rootCmd, "check", "container", "example.com/example/image:mytag")
+				_, err := executeCommand(checkContainerCmd(), "example.com/example/image:mytag")
 				Expect(err).To(HaveOccurred())
 			})
 		})
@@ -457,21 +457,21 @@ var _ = Describe("Check Container Command", func() {
 	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() {
-				_, err := executeCommand(rootCmd, "check", "container", "foo", "bar")
+				_, err := executeCommand(checkContainerCmd(), "foo", "bar")
 				Expect(err).To(HaveOccurred())
 			})
 		})
 
 		Context("and the user provided less than 1 positional arg", func() {
 			It("should fail to run", func() {
-				_, err := executeCommand(rootCmd, "check", "container")
+				_, err := executeCommand(checkContainerCmd())
 				Expect(err).To(HaveOccurred())
 			})
 		})
 
 		Context("and the user has enabled the submit flag", func() {
 			It("should cause the certification-project-id and pyxis-api-token flag to be required", func() {
-				out, err := executeCommand(rootCmd, "check", "container", "--submit", "foo")
+				out, err := executeCommand(checkContainerCmd(), "--submit", "foo")
 				Expect(err).To(HaveOccurred())
 				Expect(out).To(ContainSubstring("required flag(s) \"%s\", \"%s\" not set", "certification-project-id", "pyxis-api-token"))
 			})
diff --git a/cmd/check_operator_test.go b/cmd/check_operator_test.go
index 228ad58..21ec242 100644
--- a/cmd/check_operator_test.go
+++ b/cmd/check_operator_test.go
@@ -18,14 +18,14 @@ var _ = Describe("Check Operator", func() {
 		BeforeEach(createAndCleanupDirForArtifactsAndLogs)
 		Context("without the operator bundle image being provided", func() {
 			It("should return an error", func() {
-				_, err := executeCommand(rootCmd, "check", "operator")
+				_, err := executeCommand(checkOperatorCmd())
 				Expect(err).To(HaveOccurred())
 			})
 		})
 
 		Context("without having set the KUBECONFIG environment variable", func() {
 			It("should return an error", func() {
-				out, err := executeCommand(rootCmd, "check", "operator", "quay.io/example/image:mytag")
+				out, err := executeCommand(checkOperatorCmd(), "quay.io/example/image:mytag")
 				Expect(err).To(HaveOccurred())
 				Expect(out).To(ContainSubstring("KUBECONFIG could not"))
 			})
@@ -35,7 +35,7 @@ var _ = Describe("Check Operator", func() {
 			BeforeEach(func() { os.Setenv("KUBECONFIG", "foo") })
 			AfterEach(func() { os.Unsetenv("KUBECONFIG") })
 			It("should return an error", func() {
-				out, err := executeCommand(rootCmd, "check", "operator", "quay.io/example/image:mytag")
+				out, err := executeCommand(checkOperatorCmd(), "quay.io/example/image:mytag")
 				Expect(err).To(HaveOccurred())
 				Expect(out).To(ContainSubstring("PFLT_INDEXIMAGE could not"))
 			})
@@ -53,7 +53,7 @@ var _ = Describe("Check Operator", func() {
 			})
 
 			It("should reach the core logic, but throw an error because of the placeholder values", func() {
-				_, err := executeCommand(rootCmd, "check", "operator", "quay.io/example/image:mytag")
+				_, err := executeCommand(checkOperatorCmd(), "quay.io/example/image:mytag")
 				Expect(err).To(HaveOccurred())
 			})
 		})
@@ -164,7 +164,7 @@ var _ = Describe("Check Operator", func() {
 			os.Unsetenv("KUBECONFIG")
 		})
 		It("should succeed when all positional arg constraints and environment constraints are correct", func() {
-			err := checkOperatorPositionalArgs(checkOperatorCmd, posArgs)
+			err := checkOperatorPositionalArgs(checkOperatorCmd(), posArgs)
 			Expect(err).ToNot(HaveOccurred())
 		})
 	})
diff --git a/cmd/list_checks_test.go b/cmd/list_checks_test.go
index e683c1c..47aec2d 100644
--- a/cmd/list_checks_test.go
+++ b/cmd/list_checks_test.go
@@ -82,7 +82,7 @@ var _ = Describe("list checks subcommand", func() {
 
 			// Run the command. Because we bind this command to the
 			// root command in init, we must pass rootCmd to executeCommand.
-			out, err := executeCommand(rootCmd, "list-checks")
+			out, err := executeCommand(listChecksCmd())
 			Expect(len(out) > 0).To(BeTrue())
 
 			Expect(err).ToNot(HaveOccurred())
diff --git a/cmd/root_test.go b/cmd/root_test.go
index ec4e34f..6eab617 100644
--- a/cmd/root_test.go
+++ b/cmd/root_test.go
@@ -29,6 +29,16 @@ func executeCommand(root *cobra.Command, args ...string) (output string, err err
 }
 
 var _ = Describe("cmd package utility functions", func() {
+	Describe("Get the root command", func() {
+		Context("when calling the root command function", func() {
+			It("should return a root command", func() {
+				cmd := rootCmd()
+				Expect(cmd).ToNot(BeNil())
+				Expect(cmd.Commands()).ToNot(BeEmpty())
+			})
+		})
+	})
+
 	DescribeTable("Determine filename to which to write test results",
 		func(extension, expected string) {
 			// Ensure resultsFilenameWithExtension accurately joins the
@@ -79,7 +89,6 @@ var _ = Describe("cmd package utility functions", func() {
 				PersistentPreRun: preRunConfig,
 				Run:              func(cmd *cobra.Command, args []string) {},
 			}
-			cobra.OnInitialize(initConfig)
 		})
 		Context("configuring a Cobra Command", func() {
 			var tmpDir string
diff --git a/cmd/runtime_assets_test.go b/cmd/runtime_assets_test.go
index e1584e6..dcf148f 100644
--- a/cmd/runtime_assets_test.go
+++ b/cmd/runtime_assets_test.go
@@ -50,7 +50,7 @@ var _ = Describe("runtime-assets test", func() {
 	Context("When calling the runtime-assets cobra command", func() {
 		BeforeEach(createAndCleanupDirForArtifactsAndLogs)
 		It("should print successfully and match the actual data", func() {
-			out, err := executeCommand(rootCmd, "runtime-assets")
+			out, err := executeCommand(runtimeAssetsCmd())
 			Expect(err).ToNot(HaveOccurred())
 
 			var printed runtime.AssetData
diff --git a/cmd/support_container_test.go b/cmd/support_container_test.go
new file mode 100644
index 0000000..48f5847
--- /dev/null
+++ b/cmd/support_container_test.go
@@ -0,0 +1,32 @@
+package cmd
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("support container command tests", func() {
+	Context("When running the support container cobra command", func() {
+		BeforeEach(createAndCleanupDirForArtifactsAndLogs)
+		Context("with valid inputs", func() {
+			It("should run without error", func() {
+				_, err := executeCommand(supportContainerCmd(), "000000000000")
+				Expect(err).ToNot(HaveOccurred())
+			})
+		})
+		Context("with an invalid project ID", func() {
+			It("should throw an error", func() {
+				out, err := executeCommand(supportContainerCmd(), "ospid-00000000")
+				Expect(err).To(HaveOccurred())
+				Expect(out).To(ContainSubstring("please remove leading characters ospid-"))
+			})
+		})
+		Context("with no project ID", func() {
+			It("should throw an error", func() {
+				out, err := executeCommand(supportContainerCmd())
+				Expect(err).To(HaveOccurred())
+				Expect(out).To(ContainSubstring("Error:"))
+			})
+		})
+	})
+})
diff --git a/cmd/support_operator_test.go b/cmd/support_operator_test.go
new file mode 100644
index 0000000..32dd662
--- /dev/null
+++ b/cmd/support_operator_test.go
@@ -0,0 +1,40 @@
+package cmd
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("support command tests", func() {
+	Context("When running the support operator cobra command", func() {
+		BeforeEach(createAndCleanupDirForArtifactsAndLogs)
+		Context("with valid inputs for an operator project", func() {
+			It("should run without error", func() {
+				_, err := executeCommand(supportOperatorCmd(), "000000000000", "https://github.com/example/pull/2")
+				Expect(err).ToNot(HaveOccurred())
+			})
+		})
+		Context("with an invalid project ID", func() {
+			It("should throw an error", func() {
+				out, err := executeCommand(supportOperatorCmd(), "ospid-00000000", "https://github.com/example/pull/2")
+				Expect(err).To(HaveOccurred())
+				Expect(out).To(ContainSubstring("please remove leading characters ospid-"))
+			})
+		})
+		Context("with an operator project designation but no pull request url", func() {
+			It("should throw an error", func() {
+				out, err := executeCommand(supportOperatorCmd(), "000000000000")
+				Expect(err).To(HaveOccurred())
+				Expect(out).To(ContainSubstring("Error:"))
+			})
+		})
+
+		Context("with an operator project designation but an invalid pull request url", func() {
+			It("should throw an error", func() {
+				out, err := executeCommand(supportOperatorCmd(), "000000000000", "github.com")
+				Expect(err).To(HaveOccurred())
+				Expect(out).To(ContainSubstring("please enter a valid url"))
+			})
+		})
+	})
+})
diff --git a/cmd/support_test.go b/cmd/support_test.go
index 97590dd..a5d927f 100644
--- a/cmd/support_test.go
+++ b/cmd/support_test.go
@@ -1,4 +1,3 @@
-// go:test !race
 package cmd
 
 import (
@@ -9,48 +8,11 @@ import (
 var _ = Describe("support command tests", func() {
 	Context("When running the support cobra command", func() {
 		BeforeEach(createAndCleanupDirForArtifactsAndLogs)
-		Context("with valid inputs for a container project", func() {
-			It("should run without error", func() {
-				_, err := executeCommand(rootCmd, "support", "container", "000000000000")
-				Expect(err).ToNot(HaveOccurred())
-			})
-		})
-		Context("with valid inputs for an operator project", func() {
-			It("should run without error", func() {
-				_, err := executeCommand(rootCmd, "support", "operator", "000000000000", "https://github.com/example/pull/2")
-				Expect(err).ToNot(HaveOccurred())
-			})
-		})
-
 		Context("with an invalid project type", func() {
 			It("should throw an error", func() {
-				out, err := executeCommand(rootCmd, "support", "fooproject", "000000000000")
-				Expect(err).To(HaveOccurred())
-				Expect(out).To(ContainSubstring("the project type must be"))
-			})
-		})
-
-		Context("with an invalid project ID", func() {
-			It("should throw an error", func() {
-				out, err := executeCommand(rootCmd, "support", "container", "ospid-00000000")
-				Expect(err).To(HaveOccurred())
-				Expect(out).To(ContainSubstring("please remove leading characters ospid-"))
-			})
-		})
-
-		Context("with an operator project designation but no pull request url", func() {
-			It("should throw an error", func() {
-				out, err := executeCommand(rootCmd, "support", "operator", "000000000000")
-				Expect(err).To(HaveOccurred())
-				Expect(out).To(ContainSubstring("a pull request URL is required"))
-			})
-		})
-
-		Context("with an operator project designation but an invalid pull request url", func() {
-			It("should throw an error", func() {
-				out, err := executeCommand(rootCmd, "support", "operator", "000000000000", "github.com")
+				out, err := executeCommand(supportCmd(), "fooproject", "000000000000")
 				Expect(err).To(HaveOccurred())
-				Expect(out).To(ContainSubstring("please enter a valid url"))
+				Expect(out).To(ContainSubstring("Error: unknown command"))
 			})
 		})
 	})