From c7b8663df4f99c1f35a00fad25319c406ef805ab Mon Sep 17 00:00:00 2001 From: Brandon Palm Date: Fri, 30 Sep 2022 09:23:48 -0500 Subject: [PATCH] Add lib to preflight Testing the lib Switch back to old repo name Dep updates Operator Framework update lock otel more dep updates Dep update update deps Add NewManualConfig Add NewManualOperatorConfig Rename back --- certification/runtime/config.go | 22 ++ cmd/check.go | 20 -- cmd/check_container.go | 106 +----- cmd/check_container_test.go | 463 ++------------------------- cmd/check_operator.go | 50 +-- cmd/check_operator_test.go | 19 +- cmd/check_test.go | 13 +- cmd/preflight_check.go | 76 ----- lib/check_container_test.go | 431 +++++++++++++++++++++++++ cmd/fakes_test.go => lib/fakes.go | 38 ++- lib/lib.go | 229 +++++++++++++ lib/lib_test.go | 1 + {cmd => lib}/preflight_check_test.go | 26 +- {cmd => lib}/types.go | 88 +++-- 14 files changed, 839 insertions(+), 743 deletions(-) delete mode 100644 cmd/preflight_check.go create mode 100644 lib/check_container_test.go rename cmd/fakes_test.go => lib/fakes.go (87%) create mode 100644 lib/lib.go create mode 100644 lib/lib_test.go rename {cmd => lib}/preflight_check_test.go (87%) rename {cmd => lib}/types.go (72%) diff --git a/certification/runtime/config.go b/certification/runtime/config.go index 9dc7f18f..5fb6177e 100644 --- a/certification/runtime/config.go +++ b/certification/runtime/config.go @@ -56,6 +56,28 @@ func NewConfigFrom(vcfg viper.Viper) (*Config, error) { return &cfg, nil } +func NewManualContainerConfig(image, responseFormat, artifactsDir string, submit, writeJUnit bool) *Config { + return &Config{ + Image: image, + Submit: submit, + WriteJUnit: writeJUnit, + ResponseFormat: responseFormat, + Artifacts: artifactsDir, + } +} + +func NewManualOperatorConfig(image, responseFormat, artifactsDir string, writeJUnit bool) *Config { + return &Config{ + Image: image, + Submit: false, // operator results are not submitted + WriteJUnit: writeJUnit, + ResponseFormat: responseFormat, + Artifacts: artifactsDir, + Bundle: false, + Scratch: false, + } +} + // storeContainerPolicyConfiguration reads container-policy-specific config // items in viper, normalizes them, and stores them in Config. func (c *Config) storeContainerPolicyConfiguration(vcfg viper.Viper) { diff --git a/cmd/check.go b/cmd/check.go index 68d30e98..ad9283b5 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -3,7 +3,6 @@ package cmd import ( "bytes" "context" - "fmt" "strings" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/artifacts" @@ -63,25 +62,6 @@ func resultsFilenameWithExtension(ext string) string { return strings.Join([]string{"results", ext}, ".") } -func buildConnectURL(projectID string) string { - connectURL := fmt.Sprintf("https://connect.redhat.com/projects/%s", projectID) - - pyxisEnv := viper.GetString("pyxis_env") - if len(pyxisEnv) > 0 && pyxisEnv != "prod" { - connectURL = fmt.Sprintf("https://connect.%s.redhat.com/projects/%s", viper.GetString("pyxis_env"), projectID) - } - - return connectURL -} - -func buildOverviewURL(projectID string) string { - return fmt.Sprintf("%s/overview", buildConnectURL(projectID)) -} - -func buildScanResultsURL(projectID string, imageID string) string { - return fmt.Sprintf("%s/images/%s/scan-results", buildConnectURL(projectID), imageID) -} - func convertPassedOverall(passedOverall bool) string { if passedOverall { return "PASSED" diff --git a/cmd/check_container.go b/cmd/check_container.go index 8e0a2180..e9bd995f 100644 --- a/cmd/check_container.go +++ b/cmd/check_container.go @@ -1,15 +1,13 @@ package cmd import ( - "context" "fmt" "strings" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/engine" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/formatters" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/policy" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/runtime" + "github.com/redhat-openshift-ecosystem/openshift-preflight/lib" "github.com/redhat-openshift-ecosystem/openshift-preflight/version" log "github.com/sirupsen/logrus" @@ -52,53 +50,6 @@ func checkContainerCmd() *cobra.Command { return checkContainerCmd } -// checkContainerRunner contains all of the components necessary to run checkContainer. -type checkContainerRunner struct { - cfg *runtime.Config - pc pyxisClient - eng engine.CheckEngine - formatter formatters.ResponseFormatter - rw resultWriter - rs resultSubmitter -} - -func newCheckContainerRunner(ctx context.Context, cfg *runtime.Config) (*checkContainerRunner, error) { - cfg.Policy = policy.PolicyContainer - cfg.Submit = submit - - pyxisClient := newPyxisClient(ctx, cfg.ReadOnly()) - // If we have a pyxisClient, we can query for container policy exceptions. - if pyxisClient != nil { - policy, err := getContainerPolicyExceptions(ctx, pyxisClient) - if err != nil { - return nil, err - } - - cfg.Policy = policy - } - - engine, err := engine.NewForConfig(ctx, cfg.ReadOnly()) - if err != nil { - return nil, err - } - - fmttr, err := formatters.NewForConfig(cfg.ReadOnly()) - if err != nil { - return nil, err - } - - rs := resolveSubmitter(pyxisClient, cfg.ReadOnly()) - - return &checkContainerRunner{ - cfg: cfg, - pc: pyxisClient, - eng: engine, - formatter: fmttr, - rw: &runtime.ResultWriterFile{}, - rs: rs, - }, nil -} - // checkContainerRunE executes checkContainer using the user args to inform the execution. func checkContainerRunE(cmd *cobra.Command, args []string) error { log.Info("certification library version ", version.Version.String()) @@ -114,62 +65,23 @@ func checkContainerRunE(cmd *cobra.Command, args []string) error { cfg.Image = containerImage cfg.ResponseFormat = formatters.DefaultFormat - checkContainer, err := newCheckContainerRunner(ctx, cfg) + checkContainer, err := lib.NewCheckContainerRunner(ctx, cfg, submit) if err != nil { return err } // Run the container check. cmd.SilenceUsage = true - return preflightCheck(ctx, - checkContainer.cfg, - checkContainer.pc, - checkContainer.eng, - checkContainer.formatter, - checkContainer.rw, - checkContainer.rs, + return lib.PreflightCheck(ctx, + checkContainer.Cfg, + checkContainer.Pc, + checkContainer.Eng, + checkContainer.Formatter, + checkContainer.Rw, + checkContainer.Rs, ) } -// resolveSubmitter will build out a resultSubmitter if the provided pyxisClient, pc, is not nil. -// The pyxisClient is a required component of the submitter. If pc is nil, then a noop submitter -// is returned instead, which does nothing. -func resolveSubmitter(pc pyxisClient, cfg certification.Config) resultSubmitter { - if pc != nil { - return &containerCertificationSubmitter{ - certificationProjectID: cfg.CertificationProjectID(), - pyxis: pc, - dockerConfig: cfg.DockerConfig(), - preflightLogFile: cfg.LogFile(), - } - } - - return &noopSubmitter{emitLog: true} -} - -// getContainerPolicyExceptions will query Pyxis to determine if -// a given project has a certification excemptions, such as root or scratch. -// This will then return the corresponding policy. -// -// If no policy exception flags are found on the project, the standard -// container policy is returned. -func getContainerPolicyExceptions(ctx context.Context, pc pyxisClient) (policy.Policy, error) { - certProject, err := pc.GetProject(ctx) - if err != nil { - return "", fmt.Errorf("could not retrieve project: %w", err) - } - log.Debugf("Certification project name is: %s", certProject.Name) - if certProject.Container.Type == "scratch" { - return policy.PolicyScratch, nil - } - - // if a partner sets `Host Level Access` in connect to `Privileged`, enable RootExceptionContainerPolicy checks - if certProject.Container.Privileged { - return policy.PolicyRoot, nil - } - return policy.PolicyContainer, nil -} - func checkContainerPositionalArgs(cmd *cobra.Command, args []string) error { if len(args) != 1 { return fmt.Errorf("a container image positional argument is required") diff --git a/cmd/check_container_test.go b/cmd/check_container_test.go index b35d5560..45d54961 100644 --- a/cmd/check_container_test.go +++ b/cmd/check_container_test.go @@ -3,24 +3,13 @@ package cmd import ( "bytes" "context" - "encoding/json" "os" - "path" "path/filepath" - "strings" - - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/artifacts" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/engine" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/formatters" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/policy" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/pyxis" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/runtime" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/sirupsen/logrus" - "github.com/spf13/afero" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/runtime" + "github.com/redhat-openshift-ecosystem/openshift-preflight/lib" "github.com/spf13/viper" ) @@ -36,432 +25,6 @@ var _ = Describe("Check Container Command", func() { }) }) - Context("When determining container policy exceptions", func() { - var fakePC *fakePyxisClient - BeforeEach(func() { - // reset the fake pyxis client before each execution - // as a precaution. - fakePC = &fakePyxisClient{ - findImagesByDigestFunc: fidbFuncNoop, - getProjectsFunc: gpFuncNoop, - submitResultsFunc: srFuncNoop, - } - }) - - It("should throw an error if unable to get the project from the API", func() { - fakePC.getProjectsFunc = gpFuncReturnError - _, err := getContainerPolicyExceptions(context.TODO(), fakePC) - Expect(err).To(HaveOccurred()) - }) - - It("should return a scratch policy exception if the project has the flag in the API", func() { - fakePC.getProjectsFunc = gpFuncReturnScratchException - p, err := getContainerPolicyExceptions(context.TODO(), fakePC) - Expect(p).To(Equal(policy.PolicyScratch)) - Expect(err).ToNot(HaveOccurred()) - }) - - It("should return a root policy exception if the project has the flag in the API", func() { - fakePC.getProjectsFunc = gpFuncReturnRootException - p, err := getContainerPolicyExceptions(context.TODO(), fakePC) - Expect(p).To(Equal(policy.PolicyRoot)) - Expect(err).ToNot(HaveOccurred()) - }) - - It("should return a container policy exception if the project no exceptions in the API", func() { - fakePC.getProjectsFunc = gpFuncReturnNoException - p, err := getContainerPolicyExceptions(context.TODO(), fakePC) - Expect(p).To(Equal(policy.PolicyContainer)) - Expect(err).ToNot(HaveOccurred()) - }) - }) - - Context("When using the containerCertificationSubmitter", func() { - var sbmt *containerCertificationSubmitter - var fakePC *fakePyxisClient - var dockerConfigPath string - var preflightLogPath string - - preflightLogFilename := "preflight.log" - dockerconfigFilename := "dockerconfig.json" - BeforeEach(func() { - dockerConfigPath = path.Join(artifacts.Path(), dockerconfigFilename) - preflightLogPath = path.Join(artifacts.Path(), preflightLogFilename) - // Normalize a fakePyxisClient with noop functions. - fakePC = &fakePyxisClient{ - findImagesByDigestFunc: fidbFuncNoop, - getProjectsFunc: gpFuncNoop, - submitResultsFunc: srFuncNoop, - } - - // Most tests will need a passing getProjects func so set that to - // avoid having to perform multiple BeforeEaches - fakePC.setGPFuncReturnBaseProject("") - - // configure the submitter - sbmt = &containerCertificationSubmitter{ - certificationProjectID: fakePC.baseProject("").ID, - pyxis: fakePC, - dockerConfig: dockerConfigPath, - preflightLogFile: preflightLogPath, - } - - certImageJSONBytes, err := json.Marshal(pyxis.CertImage{ - ID: "111111111111", - }) - Expect(err).ToNot(HaveOccurred()) - - preflightTestResultsJSONBytes, err := json.Marshal(runtime.Results{ - TestedImage: "foo", - PassedOverall: true, - }) - Expect(err).ToNot(HaveOccurred()) - - rpmManifestJSONBytes, err := json.Marshal(pyxis.RPMManifest{ - ID: "foo", - ImageID: "foo", - }) - Expect(err).ToNot(HaveOccurred()) - - // Create expected files. Use of Gomega's Expect here (without a subsequent test) is intentional. - // Expect automatically checks that additional return values are nil, and thus will fail if they - // are not. - Expect(artifacts.WriteFile(dockerconfigFilename, strings.NewReader("dockerconfig"))) - Expect(artifacts.WriteFile(preflightLogFilename, strings.NewReader("preflight log"))) - Expect(artifacts.WriteFile(certification.DefaultCertImageFilename, bytes.NewReader(certImageJSONBytes))) - Expect(artifacts.WriteFile(certification.DefaultTestResultsFilename, bytes.NewReader(preflightTestResultsJSONBytes))) - Expect(artifacts.WriteFile(certification.DefaultRPMManifestFilename, bytes.NewReader(rpmManifestJSONBytes))) - }) - - Context("and project cannot be obtained from the API", func() { - BeforeEach(func() { - fakePC.getProjectsFunc = gpFuncReturnError - }) - It("should throw an error", func() { - err := sbmt.Submit(context.TODO()) - Expect(err).To(HaveOccurred()) - }) - }) - - Context("and the provided docker config cannot be read from disk", func() { - It("should throw an error", func() { - err := os.Remove(dockerConfigPath) - Expect(err).ToNot(HaveOccurred()) - - err = sbmt.Submit(context.TODO()) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(dockerconfigFilename)) - }) - }) - - Context("and no docker config command argument was provided", func() { - BeforeEach(func() { - fakePC.setSRFuncSubmitSuccessfully("", "") - }) - It("should not throw an error", func() { - sbmt.dockerConfig = "" - err := os.Remove(dockerConfigPath) - Expect(err).ToNot(HaveOccurred()) - - err = sbmt.Submit(context.TODO()) - Expect(err).ToNot(HaveOccurred()) - }) - }) - - Context("and the cert image cannot be read from disk", func() { - It("should throw an error", func() { - err := os.Remove(path.Join(artifacts.Path(), certification.DefaultCertImageFilename)) - Expect(err).ToNot(HaveOccurred()) - - err = sbmt.Submit(context.TODO()) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(certification.DefaultCertImageFilename)) - }) - }) - - Context("and the preflight results cannot be read from disk", func() { - It("should throw an error", func() { - err := os.Remove(path.Join(artifacts.Path(), certification.DefaultTestResultsFilename)) - Expect(err).ToNot(HaveOccurred()) - - err = sbmt.Submit(context.TODO()) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(certification.DefaultTestResultsFilename)) - }) - }) - - Context("and the rpmManifest cannot be read from disk", func() { - It("should throw an error", func() { - err := os.Remove(path.Join(artifacts.Path(), certification.DefaultRPMManifestFilename)) - Expect(err).ToNot(HaveOccurred()) - - err = sbmt.Submit(context.TODO()) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(certification.DefaultRPMManifestFilename)) - }) - }) - - Context("and the preflight logfile cannot be read from disk", func() { - It("should throw an error", func() { - err := os.Remove(preflightLogPath) - Expect(err).ToNot(HaveOccurred()) - - err = sbmt.Submit(context.TODO()) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(preflightLogFilename)) - }) - }) - - Context("and the submission fails", func() { - BeforeEach(func() { - fakePC.submitResultsFunc = srFuncReturnError - }) - - It("should throw an error", func() { - err := sbmt.Submit(context.TODO()) - Expect(err).To(HaveOccurred()) - }) - }) - - Context("and the certproject returned from pyxis is nil, but no error was returned", func() { - BeforeEach(func() { - fakePC.getProjectsFunc = gpFuncNoop - }) - - It("should throw an error", func() { - err := sbmt.Submit(context.TODO()) - Expect(err).To(HaveOccurred()) - }) - }) - - Context("and one of the submission artifacts is malformed", func() { - JustBeforeEach(func() { - afs := afero.NewBasePathFs(afero.NewOsFs(), artifacts.Path()) - Expect(afs.Remove(certification.DefaultRPMManifestFilename)).To(Succeed()) - Expect(artifacts.WriteFile(certification.DefaultRPMManifestFilename, strings.NewReader("malformed"))).To(ContainSubstring(certification.DefaultRPMManifestFilename)) - }) - - It("should throw an error finalizing the submission", func() { - err := sbmt.Submit(context.TODO()) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("unable to finalize data")) - }) - }) - - Context("and the submission succeeds", func() { - BeforeEach(func() { - fakePC.setSRFuncSubmitSuccessfully("", "") - }) - It("should not throw an error", func() { - err := sbmt.Submit(context.TODO()) - Expect(err).ToNot(HaveOccurred()) - }) - }) - }) - - Context("When using the noop submitter", func() { - var bf *bytes.Buffer - var noop *noopSubmitter - - BeforeEach(func() { - bufferLogger := logrus.New() - bf = bytes.NewBuffer([]byte{}) - bufferLogger.SetOutput(bf) - - noop = &noopSubmitter{log: bufferLogger} - }) - - Context("and enabling log emitting", func() { - BeforeEach(func() { - noop.emitLog = true - }) - - It("should include the reason in the emitted log if specified", func() { - testReason := "test reason" - noop.reason = testReason - err := noop.Submit(context.TODO()) - Expect(err).ToNot(HaveOccurred()) - Expect(bf.String()).To(ContainSubstring(testReason)) - }) - - It("should emit logs when calling submit", func() { - err := noop.Submit(context.TODO()) - Expect(err).ToNot(HaveOccurred()) - Expect(bf.String()).To(ContainSubstring("Results are not being sent for submission.")) - }) - }) - - Context("and disabling log emitting", func() { - It("should not emit logs when calling submit", func() { - noop.emitLog = false - err := noop.Submit(context.TODO()) - Expect(err).ToNot(HaveOccurred()) - Expect(bf.String()).To(BeEmpty()) - }) - }) - }) - - Context("When resolving the submitter", func() { - Context("with a valid pyxis client", func() { - cfg := runtime.Config{ - CertificationProjectID: "projectid", - PyxisHost: "host", - PyxisAPIToken: "apitoken", - DockerConfig: "dockercfg", - LogFile: "logfile", - } - - pc := newPyxisClient(context.TODO(), cfg.ReadOnly()) - Expect(pc).ToNot(BeNil()) - - It("should return a containerCertificationSubmitter", func() { - submitter := resolveSubmitter(pc, cfg.ReadOnly()) - typed, ok := submitter.(*containerCertificationSubmitter) - Expect(typed).ToNot(BeNil()) - Expect(ok).To(BeTrue()) - }) - }) - - Context("With no pyxis client", func() { - cfg := runtime.Config{} - It("should return a no-op submitter", func() { - submitter := resolveSubmitter(nil, cfg.ReadOnly()) - typed, ok := submitter.(*noopSubmitter) - Expect(typed).ToNot(BeNil()) - Expect(ok).To(BeTrue()) - }) - }) - }) - - Context("When establishing a pyxis client.", func() { - Context("with none of the required values", func() { - cfgNoCertProjectID := runtime.Config{} - - It("Should return a nil pyxis client", func() { - pc := newPyxisClient(context.TODO(), cfgNoCertProjectID.ReadOnly()) - Expect(pc).To(BeNil()) - }) - }) - - Context("Missing any of the required values", func() { - cfgMissingCertProjectID := runtime.Config{ - PyxisHost: "foo", - PyxisAPIToken: "bar", - } - - cfgMissingPyxisHost := runtime.Config{ - CertificationProjectID: "foo", - PyxisAPIToken: "bar", - } - - cfgMissingPyxisAPIToken := runtime.Config{ - CertificationProjectID: "foo", - PyxisHost: "bar", - } - - It("Should return a nil pyxis client", func() { - pc := newPyxisClient(context.TODO(), cfgMissingCertProjectID.ReadOnly()) - Expect(pc).To(BeNil()) - - pc = newPyxisClient(context.TODO(), cfgMissingPyxisHost.ReadOnly()) - Expect(pc).To(BeNil()) - - pc = newPyxisClient(context.TODO(), cfgMissingPyxisAPIToken.ReadOnly()) - Expect(pc).To(BeNil()) - }) - }) - - Context("With all the required values", func() { - cfgValid := runtime.Config{ - CertificationProjectID: "foo", - PyxisHost: "bar", - PyxisAPIToken: "baz", - } - - It("should return a pyxis client", func() { - pc := newPyxisClient(context.TODO(), cfgValid.ReadOnly()) - Expect(pc).ToNot(BeNil()) - }) - }) - }) - - Context("When instantiating a checkContainerRunner", func() { - var cfg *runtime.Config - - BeforeEach(func() { - cfg = &runtime.Config{ - Image: "quay.io/example/foo:latest", - ResponseFormat: formatters.DefaultFormat, - } - }) - - Context("and the user passed the submit flag, but no credentials", func() { - It("should return a noop submitter as credentials are required for submission", func() { - runner, err := newCheckContainerRunner(context.TODO(), cfg) - Expect(err).ToNot(HaveOccurred()) - _, rsIsCorrectType := runner.rs.(*noopSubmitter) - Expect(rsIsCorrectType).To(BeTrue()) - }) - }) - - Context("and the user did not pass the submit flag", func() { - var origSubmitValue bool - BeforeEach(func() { - origSubmitValue = submit - submit = false - }) - - AfterEach(func() { - submit = origSubmitValue - }) - It("should return a noopSubmitter resultSubmitter", func() { - runner, err := newCheckContainerRunner(context.TODO(), cfg) - Expect(err).ToNot(HaveOccurred()) - _, rsIsCorrectType := runner.rs.(*noopSubmitter) - Expect(rsIsCorrectType).To(BeTrue()) - }) - }) - - Context("with a valid policy formatter", func() { - It("should return with no error, and the appropriate formatter", func() { - cfg.ResponseFormat = "xml" - runner, err := newCheckContainerRunner(context.TODO(), cfg) - Expect(err).ToNot(HaveOccurred()) - expectedFormatter, err := formatters.NewByName(cfg.ResponseFormat) - Expect(err).ToNot(HaveOccurred()) - Expect(runner.formatter.PrettyName()).To(Equal(expectedFormatter.PrettyName())) - }) - }) - - Context("with an invalid policy definition", func() { - It("should return the container policy engine anyway", func() { - runner, err := newCheckContainerRunner(context.TODO(), cfg) - Expect(err).ToNot(HaveOccurred()) - - expectedEngine, err := engine.NewForConfig(context.TODO(), cfg.ReadOnly()) - Expect(runner.eng).To(BeEquivalentTo(expectedEngine)) - Expect(err).ToNot(HaveOccurred()) - }) - }) - // NOTE(): There's no way to test policy exceptions here because - // without valid credentials to pyxis. - - Context("with an invalid formatter definition", func() { - It("should return an error", func() { - cfg.ResponseFormat = "foo" - _, err := newCheckContainerRunner(context.TODO(), cfg) - Expect(err).To(HaveOccurred()) - }) - }) - - It("should contain a ResultWriterFile resultWriter", func() { - runner, err := newCheckContainerRunner(context.TODO(), cfg) - Expect(err).ToNot(HaveOccurred()) - _, rwIsExpectedType := runner.rw.(*runtime.ResultWriterFile) - Expect(rwIsExpectedType).To(BeTrue()) - }) - }) - 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() { @@ -574,4 +137,26 @@ certification_project_id: mycertid` }) }) }) + + Context("When instantiating a checkContainerRunner", func() { + var cfg *runtime.Config + + Context("and the user did not pass the submit flag", func() { + var origSubmitValue bool + BeforeEach(func() { + origSubmitValue = submit + submit = false + }) + + AfterEach(func() { + submit = origSubmitValue + }) + It("should return a noopSubmitter resultSubmitter", func() { + runner, err := lib.NewCheckContainerRunner(context.TODO(), cfg, false) + Expect(err).ToNot(HaveOccurred()) + _, rsIsCorrectType := runner.Rs.(*lib.NoopSubmitter) + Expect(rsIsCorrectType).To(BeTrue()) + }) + }) + }) }) diff --git a/cmd/check_operator.go b/cmd/check_operator.go index 6199720e..9a6bf388 100644 --- a/cmd/check_operator.go +++ b/cmd/check_operator.go @@ -1,14 +1,12 @@ package cmd import ( - "context" "fmt" "os" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/engine" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/formatters" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/policy" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/runtime" + "github.com/redhat-openshift-ecosystem/openshift-preflight/lib" "github.com/redhat-openshift-ecosystem/openshift-preflight/version" log "github.com/sirupsen/logrus" @@ -48,38 +46,6 @@ func checkOperatorCmd() *cobra.Command { return checkOperatorCmd } -// checkOperatorRunner contains all of the components necessary to run checkOperator. -type checkOperatorRunner struct { - cfg *runtime.Config - eng engine.CheckEngine - formatter formatters.ResponseFormatter - rw resultWriter -} - -// newCheckOperatorRunner returns a checkOperatorRunner containing all of the tooling necessary -// to run checkOperator. -func newCheckOperatorRunner(ctx context.Context, cfg *runtime.Config) (*checkOperatorRunner, error) { - cfg.Policy = policy.PolicyOperator - cfg.Submit = false // there's no such thing as submitting for operators today. - - engine, err := engine.NewForConfig(ctx, cfg.ReadOnly()) - if err != nil { - return nil, err - } - - fmttr, err := formatters.NewForConfig(cfg.ReadOnly()) - if err != nil { - return nil, err - } - - return &checkOperatorRunner{ - cfg: cfg, - eng: engine, - formatter: fmttr, - rw: &runtime.ResultWriterFile{}, - }, nil -} - // ensureKubeconfigIsSet ensures that the KUBECONFIG environment variable has a value. func ensureKubeconfigIsSet() error { if _, ok := os.LookupEnv("KUBECONFIG"); !ok { @@ -116,20 +82,20 @@ func checkOperatorRunE(cmd *cobra.Command, args []string) error { cfg.Bundle = true cfg.Scratch = true - checkOperator, err := newCheckOperatorRunner(ctx, cfg) + checkOperator, err := lib.NewCheckOperatorRunner(ctx, cfg) if err != nil { return err } // Run the operator check cmd.SilenceUsage = true - return preflightCheck(ctx, - checkOperator.cfg, + return lib.PreflightCheck(ctx, + checkOperator.Cfg, nil, // no pyxisClient is necessary - checkOperator.eng, - checkOperator.formatter, - checkOperator.rw, - &noopSubmitter{}, // we do not submit these results. + checkOperator.Eng, + checkOperator.Formatter, + checkOperator.Rw, + &lib.NoopSubmitter{}, // we do not submit these results. ) } diff --git a/cmd/check_operator_test.go b/cmd/check_operator_test.go index a4844d7e..94573572 100644 --- a/cmd/check_operator_test.go +++ b/cmd/check_operator_test.go @@ -8,6 +8,7 @@ import ( "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/formatters" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/policy" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/runtime" + "github.com/redhat-openshift-ecosystem/openshift-preflight/lib" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -128,18 +129,18 @@ var _ = Describe("Check Operator", func() { Context("with a valid policy formatter", func() { It("should return with no error, and the appropriate formatter", func() { cfg.ResponseFormat = "xml" - runner, err := newCheckOperatorRunner(context.TODO(), cfg) + runner, err := lib.NewCheckOperatorRunner(context.TODO(), cfg) Expect(err).ToNot(HaveOccurred()) expectedFormatter, err := formatters.NewByName(cfg.ResponseFormat) Expect(err).ToNot(HaveOccurred()) - Expect(runner.formatter.PrettyName()).To(Equal(expectedFormatter.PrettyName())) + Expect(runner.Formatter.PrettyName()).To(Equal(expectedFormatter.PrettyName())) }) }) Context("with an invalid policy formatter", func() { It("should return an error", func() { cfg.ResponseFormat = "foo" - _, err := newCheckOperatorRunner(context.TODO(), cfg) + _, err := lib.NewCheckOperatorRunner(context.TODO(), cfg) Expect(err).To(HaveOccurred()) }) }) @@ -148,12 +149,12 @@ var _ = Describe("Check Operator", func() { It("should return the container policy engine anyway", func() { cfg.Policy = "badpolicy" beforeCfg := *cfg - runner, err := newCheckOperatorRunner(context.TODO(), cfg) + runner, err := lib.NewCheckOperatorRunner(context.TODO(), cfg) Expect(err).ToNot(HaveOccurred()) _, err = engine.NewForConfig(context.TODO(), cfg.ReadOnly()) - Expect(runner.cfg.Policy).ToNot(Equal(beforeCfg.Policy)) - Expect(runner.cfg.Policy).To(Equal(policy.PolicyOperator)) + Expect(runner.Cfg.Policy).ToNot(Equal(beforeCfg.Policy)) + Expect(runner.Cfg.Policy).To(Equal(policy.PolicyOperator)) Expect(err).ToNot(HaveOccurred()) }) }) @@ -161,15 +162,15 @@ var _ = Describe("Check Operator", func() { Context("with an invalid formatter definition", func() { It("should return an error", func() { cfg.ResponseFormat = "foo" - _, err := newCheckOperatorRunner(context.TODO(), cfg) + _, err := lib.NewCheckOperatorRunner(context.TODO(), cfg) Expect(err).To(HaveOccurred()) }) }) It("should contain a ResultWriterFile resultWriter", func() { - runner, err := newCheckOperatorRunner(context.TODO(), cfg) + runner, err := lib.NewCheckOperatorRunner(context.TODO(), cfg) Expect(err).ToNot(HaveOccurred()) - _, rwIsExpectedType := runner.rw.(*runtime.ResultWriterFile) + _, rwIsExpectedType := runner.Rw.(*runtime.ResultWriterFile) Expect(rwIsExpectedType).To(BeTrue()) }) }) diff --git a/cmd/check_test.go b/cmd/check_test.go index 3422ff05..f6153024 100644 --- a/cmd/check_test.go +++ b/cmd/check_test.go @@ -7,6 +7,7 @@ import ( "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/artifacts" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/runtime" + "github.com/redhat-openshift-ecosystem/openshift-preflight/lib" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -46,7 +47,7 @@ var _ = Describe("cmd package check command", func() { Context("Regular Connect URL", func() { It("should return a URL with just a project ID", func() { expected := "https://connect.redhat.com/projects/this-is-my-project-id" - actual := buildConnectURL(projectID) + actual := lib.BuildConnectURL(projectID) Expect(expected).To(Equal(actual)) }) }) @@ -56,7 +57,7 @@ var _ = Describe("cmd package check command", func() { }) It("should return a URL for QA", func() { expected := "https://connect.qa.redhat.com/projects/this-is-my-project-id" - actual := buildConnectURL(projectID) + actual := lib.BuildConnectURL(projectID) Expect(expected).To(Equal(actual)) }) }) @@ -66,7 +67,7 @@ var _ = Describe("cmd package check command", func() { }) It("should return a URL for UAT", func() { expected := "https://connect.uat.redhat.com/projects/this-is-my-project-id/images/my-image-id/scan-results" - actual := buildScanResultsURL(projectID, imageID) + actual := lib.BuildScanResultsURL(projectID, imageID) Expect(expected).To(Equal(actual)) }) }) @@ -76,7 +77,7 @@ var _ = Describe("cmd package check command", func() { }) It("should return a URL for QA", func() { expected := "https://connect.qa.redhat.com/projects/this-is-my-project-id/overview" - actual := buildOverviewURL(projectID) + actual := lib.BuildOverviewURL(projectID) Expect(expected).To(Equal(actual)) }) }) @@ -86,12 +87,12 @@ var _ = Describe("cmd package check command", func() { }) It("should return a Prod overview URL", func() { expected := "https://connect.redhat.com/projects/this-is-my-project-id/overview" - actual := buildOverviewURL(projectID) + actual := lib.BuildOverviewURL(projectID) Expect(expected).To(Equal(actual)) }) It("should return a Prod scan URL", func() { expected := "https://connect.redhat.com/projects/this-is-my-project-id/images/my-image-id/scan-results" - actual := buildScanResultsURL(projectID, imageID) + actual := lib.BuildScanResultsURL(projectID, imageID) Expect(expected).To(Equal(actual)) }) }) diff --git a/cmd/preflight_check.go b/cmd/preflight_check.go deleted file mode 100644 index acdd4af8..00000000 --- a/cmd/preflight_check.go +++ /dev/null @@ -1,76 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "io" - "os" - "strings" - - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/artifacts" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/engine" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/formatters" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/runtime" - - log "github.com/sirupsen/logrus" -) - -// preflightCheck executes checks, interacts with pyxis, format output, writes, and submits results. -func preflightCheck( - ctx context.Context, - cfg *runtime.Config, - pc pyxisClient, //nolint:unparam // pyxisClient is currently unused. - eng engine.CheckEngine, - formatter formatters.ResponseFormatter, - rw resultWriter, - rs resultSubmitter, -) error { - // configure the artifacts directory if the user requested a different directory. - if cfg.Artifacts != "" { - artifacts.SetDir(cfg.Artifacts) - } - - // create the results file early to catch cases where we are not - // able to write to the filesystem before we attempt to execute checks. - resultsFilePath, err := artifacts.WriteFile(resultsFilenameWithExtension(formatter.FileExtension()), strings.NewReader("")) - if err != nil { - return err - } - resultsFile, err := rw.OpenFile(resultsFilePath) - if err != nil { - return err - } - defer resultsFile.Close() - - resultsOutputTarget := io.MultiWriter(os.Stdout, resultsFile) - - // execute the checks - if err := eng.ExecuteChecks(ctx); err != nil { - return err - } - results := eng.Results(ctx) - - // return results to the user and then close output files - formattedResults, err := formatter.Format(ctx, results) - if err != nil { - return err - } - - fmt.Fprintln(resultsOutputTarget, string(formattedResults)) - - if cfg.WriteJUnit { - if err := writeJUnit(ctx, results); err != nil { - return err - } - } - - if cfg.Submit { - if err := rs.Submit(ctx); err != nil { - return err - } - } - - log.Infof("Preflight result: %s", convertPassedOverall(results.PassedOverall)) - - return nil -} diff --git a/lib/check_container_test.go b/lib/check_container_test.go new file mode 100644 index 00000000..0fbfa2cd --- /dev/null +++ b/lib/check_container_test.go @@ -0,0 +1,431 @@ +package lib + +import ( + "bytes" + "context" + "encoding/json" + "os" + "path" + "strings" + + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/artifacts" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/engine" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/formatters" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/policy" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/pyxis" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/runtime" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" + "github.com/spf13/afero" +) + +var _ = Describe("Check Container Command", func() { + // BeforeEach(createAndCleanupDirForArtifactsAndLogs) + + Context("When determining container policy exceptions", func() { + var fakePC *FakePyxisClient + BeforeEach(func() { + // reset the fake pyxis client before each execution + // as a precaution. + fakePC = &FakePyxisClient{ + findImagesByDigestFunc: fidbFuncNoop, + getProjectsFunc: gpFuncNoop, + submitResultsFunc: srFuncNoop, + } + }) + + It("should throw an error if unable to get the project from the API", func() { + fakePC.getProjectsFunc = gpFuncReturnError + _, err := GetContainerPolicyExceptions(context.TODO(), fakePC) + Expect(err).To(HaveOccurred()) + }) + + It("should return a scratch policy exception if the project has the flag in the API", func() { + fakePC.getProjectsFunc = gpFuncReturnScratchException + p, err := GetContainerPolicyExceptions(context.TODO(), fakePC) + Expect(p).To(Equal(policy.PolicyScratch)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return a root policy exception if the project has the flag in the API", func() { + fakePC.getProjectsFunc = gpFuncReturnRootException + p, err := GetContainerPolicyExceptions(context.TODO(), fakePC) + Expect(p).To(Equal(policy.PolicyRoot)) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should return a container policy exception if the project no exceptions in the API", func() { + fakePC.getProjectsFunc = gpFuncReturnNoException + p, err := GetContainerPolicyExceptions(context.TODO(), fakePC) + Expect(p).To(Equal(policy.PolicyContainer)) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("When using the containerCertificationSubmitter", func() { + var sbmt *ContainerCertificationSubmitter + var fakePC *FakePyxisClient + var dockerConfigPath string + var preflightLogPath string + + preflightLogFilename := "preflight.log" + dockerconfigFilename := "dockerconfig.json" + BeforeEach(func() { + dockerConfigPath = path.Join(artifacts.Path(), dockerconfigFilename) + preflightLogPath = path.Join(artifacts.Path(), preflightLogFilename) + // Normalize a FakePyxisClient with noop functions. + fakePC = NewFakePyxisClientNoop() + + // Most tests will need a passing getProjects func so set that to + // avoid having to perform multiple BeforeEaches + fakePC.setGPFuncReturnBaseProject("") + + // configure the submitter + sbmt = &ContainerCertificationSubmitter{ + CertificationProjectID: fakePC.baseProject("").ID, + Pyxis: fakePC, + DockerConfig: dockerConfigPath, + PreflightLogFile: preflightLogPath, + } + + certImageJSONBytes, err := json.Marshal(pyxis.CertImage{ + ID: "111111111111", + }) + Expect(err).ToNot(HaveOccurred()) + + preflightTestResultsJSONBytes, err := json.Marshal(runtime.Results{ + TestedImage: "foo", + PassedOverall: true, + }) + Expect(err).ToNot(HaveOccurred()) + + rpmManifestJSONBytes, err := json.Marshal(pyxis.RPMManifest{ + ID: "foo", + ImageID: "foo", + }) + Expect(err).ToNot(HaveOccurred()) + + // Create expected files. Use of Gomega's Expect here (without a subsequent test) is intentional. + // Expect automatically checks that additional return values are nil, and thus will fail if they + // are not. + Expect(artifacts.WriteFile(dockerconfigFilename, strings.NewReader("dockerconfig"))) + Expect(artifacts.WriteFile(preflightLogFilename, strings.NewReader("preflight log"))) + Expect(artifacts.WriteFile(certification.DefaultCertImageFilename, bytes.NewReader(certImageJSONBytes))) + Expect(artifacts.WriteFile(certification.DefaultTestResultsFilename, bytes.NewReader(preflightTestResultsJSONBytes))) + Expect(artifacts.WriteFile(certification.DefaultRPMManifestFilename, bytes.NewReader(rpmManifestJSONBytes))) + }) + + Context("and project cannot be obtained from the API", func() { + BeforeEach(func() { + fakePC.getProjectsFunc = gpFuncReturnError + }) + It("should throw an error", func() { + err := sbmt.Submit(context.TODO()) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("and the provided docker config cannot be read from disk", func() { + It("should throw an error", func() { + err := os.Remove(dockerConfigPath) + Expect(err).ToNot(HaveOccurred()) + + err = sbmt.Submit(context.TODO()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(dockerconfigFilename)) + }) + }) + + Context("and no docker config command argument was provided", func() { + BeforeEach(func() { + fakePC.setSRFuncSubmitSuccessfully("", "") + }) + It("should not throw an error", func() { + sbmt.DockerConfig = "" + err := os.Remove(dockerConfigPath) + Expect(err).ToNot(HaveOccurred()) + + err = sbmt.Submit(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("and the cert image cannot be read from disk", func() { + It("should throw an error", func() { + err := os.Remove(path.Join(artifacts.Path(), certification.DefaultCertImageFilename)) + Expect(err).ToNot(HaveOccurred()) + + err = sbmt.Submit(context.TODO()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(certification.DefaultCertImageFilename)) + }) + }) + + Context("and the preflight results cannot be read from disk", func() { + It("should throw an error", func() { + err := os.Remove(path.Join(artifacts.Path(), certification.DefaultTestResultsFilename)) + Expect(err).ToNot(HaveOccurred()) + + err = sbmt.Submit(context.TODO()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(certification.DefaultTestResultsFilename)) + }) + }) + + Context("and the rpmManifest cannot be read from disk", func() { + It("should throw an error", func() { + err := os.Remove(path.Join(artifacts.Path(), certification.DefaultRPMManifestFilename)) + Expect(err).ToNot(HaveOccurred()) + + err = sbmt.Submit(context.TODO()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(certification.DefaultRPMManifestFilename)) + }) + }) + + Context("and the preflight logfile cannot be read from disk", func() { + It("should throw an error", func() { + err := os.Remove(preflightLogPath) + Expect(err).ToNot(HaveOccurred()) + + err = sbmt.Submit(context.TODO()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(preflightLogFilename)) + }) + }) + + Context("and the submission fails", func() { + BeforeEach(func() { + fakePC.submitResultsFunc = srFuncReturnError + }) + + It("should throw an error", func() { + err := sbmt.Submit(context.TODO()) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("and the certproject returned from pyxis is nil, but no error was returned", func() { + BeforeEach(func() { + fakePC.getProjectsFunc = gpFuncNoop + }) + + It("should throw an error", func() { + err := sbmt.Submit(context.TODO()) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("and one of the submission artifacts is malformed", func() { + JustBeforeEach(func() { + afs := afero.NewBasePathFs(afero.NewOsFs(), artifacts.Path()) + Expect(afs.Remove(certification.DefaultRPMManifestFilename)).To(Succeed()) + Expect(artifacts.WriteFile(certification.DefaultRPMManifestFilename, strings.NewReader("malformed"))).To(ContainSubstring(certification.DefaultRPMManifestFilename)) + }) + + It("should throw an error finalizing the submission", func() { + err := sbmt.Submit(context.TODO()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unable to finalize data")) + }) + }) + + Context("and the submission succeeds", func() { + BeforeEach(func() { + fakePC.setSRFuncSubmitSuccessfully("", "") + }) + It("should not throw an error", func() { + err := sbmt.Submit(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) + + Context("When using the noop submitter", func() { + var bf *bytes.Buffer + var noop *NoopSubmitter + + BeforeEach(func() { + bufferLogger := logrus.New() + bf = bytes.NewBuffer([]byte{}) + bufferLogger.SetOutput(bf) + + noop = NewNoopSubmitter(false, "", bufferLogger) + }) + + Context("and enabling log emitting", func() { + BeforeEach(func() { + noop.SetEmitLog(true) + }) + + It("should include the reason in the emitted log if specified", func() { + testReason := "test reason" + noop.SetReason(testReason) + err := noop.Submit(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + Expect(bf.String()).To(ContainSubstring(testReason)) + }) + + It("should emit logs when calling submit", func() { + err := noop.Submit(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + Expect(bf.String()).To(ContainSubstring("Results are not being sent for submission.")) + }) + }) + + Context("and disabling log emitting", func() { + It("should not emit logs when calling submit", func() { + noop.SetEmitLog(false) + err := noop.Submit(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + Expect(bf.String()).To(BeEmpty()) + }) + }) + }) + + Context("When resolving the submitter", func() { + Context("with a valid pyxis client", func() { + cfg := runtime.Config{ + CertificationProjectID: "projectid", + PyxisHost: "host", + PyxisAPIToken: "apitoken", + DockerConfig: "dockercfg", + LogFile: "logfile", + } + + pc := NewPyxisClient(context.TODO(), cfg.ReadOnly()) + Expect(pc).ToNot(BeNil()) + + It("should return a containerCertificationSubmitter", func() { + submitter := ResolveSubmitter(pc, cfg.ReadOnly()) + typed, ok := submitter.(*ContainerCertificationSubmitter) + Expect(typed).ToNot(BeNil()) + Expect(ok).To(BeTrue()) + }) + }) + + Context("With no pyxis client", func() { + cfg := runtime.Config{} + It("should return a no-op submitter", func() { + submitter := ResolveSubmitter(nil, cfg.ReadOnly()) + typed, ok := submitter.(*NoopSubmitter) + Expect(typed).ToNot(BeNil()) + Expect(ok).To(BeTrue()) + }) + }) + }) + + Context("When establishing a pyxis client.", func() { + Context("with none of the required values", func() { + cfgNoCertProjectID := runtime.Config{} + + It("Should return a nil pyxis client", func() { + pc := NewPyxisClient(context.TODO(), cfgNoCertProjectID.ReadOnly()) + Expect(pc).To(BeNil()) + }) + }) + + Context("Missing any of the required values", func() { + cfgMissingCertProjectID := runtime.Config{ + PyxisHost: "foo", + PyxisAPIToken: "bar", + } + + cfgMissingPyxisHost := runtime.Config{ + CertificationProjectID: "foo", + PyxisAPIToken: "bar", + } + + cfgMissingPyxisAPIToken := runtime.Config{ + CertificationProjectID: "foo", + PyxisHost: "bar", + } + + It("Should return a nil pyxis client", func() { + pc := NewPyxisClient(context.TODO(), cfgMissingCertProjectID.ReadOnly()) + Expect(pc).To(BeNil()) + + pc = NewPyxisClient(context.TODO(), cfgMissingPyxisHost.ReadOnly()) + Expect(pc).To(BeNil()) + + pc = NewPyxisClient(context.TODO(), cfgMissingPyxisAPIToken.ReadOnly()) + Expect(pc).To(BeNil()) + }) + }) + + Context("With all the required values", func() { + cfgValid := runtime.Config{ + CertificationProjectID: "foo", + PyxisHost: "bar", + PyxisAPIToken: "baz", + } + + It("should return a pyxis client", func() { + pc := NewPyxisClient(context.TODO(), cfgValid.ReadOnly()) + Expect(pc).ToNot(BeNil()) + }) + }) + }) + + Context("When instantiating a checkContainerRunner", func() { + var cfg *runtime.Config + + BeforeEach(func() { + cfg = &runtime.Config{ + Image: "quay.io/example/foo:latest", + ResponseFormat: formatters.DefaultFormat, + } + }) + + Context("and the user passed the submit flag, but no credentials", func() { + It("should return a noop submitter as credentials are required for submission", func() { + runner, err := NewCheckContainerRunner(context.TODO(), cfg, false) + Expect(err).ToNot(HaveOccurred()) + _, rsIsCorrectType := runner.Rs.(*NoopSubmitter) + Expect(rsIsCorrectType).To(BeTrue()) + }) + }) + + Context("with a valid policy formatter", func() { + It("should return with no error, and the appropriate formatter", func() { + cfg.ResponseFormat = "xml" + runner, err := NewCheckContainerRunner(context.TODO(), cfg, false) + Expect(err).ToNot(HaveOccurred()) + expectedFormatter, err := formatters.NewByName(cfg.ResponseFormat) + Expect(err).ToNot(HaveOccurred()) + Expect(runner.Formatter.PrettyName()).To(Equal(expectedFormatter.PrettyName())) + }) + }) + + Context("with an invalid policy definition", func() { + It("should return the container policy engine anyway", func() { + runner, err := NewCheckContainerRunner(context.TODO(), cfg, false) + Expect(err).ToNot(HaveOccurred()) + + expectedEngine, err := engine.NewForConfig(context.TODO(), cfg.ReadOnly()) + Expect(runner.Eng).To(BeEquivalentTo(expectedEngine)) + Expect(err).ToNot(HaveOccurred()) + }) + }) + // NOTE(): There's no way to test policy exceptions here because + // without valid credentials to pyxis. + + Context("with an invalid formatter definition", func() { + It("should return an error", func() { + cfg.ResponseFormat = "foo" + _, err := NewCheckContainerRunner(context.TODO(), cfg, false) + Expect(err).To(HaveOccurred()) + }) + }) + + It("should contain a ResultWriterFile resultWriter", func() { + runner, err := NewCheckContainerRunner(context.TODO(), cfg, false) + Expect(err).ToNot(HaveOccurred()) + _, rwIsExpectedType := runner.Rw.(*runtime.ResultWriterFile) + Expect(rwIsExpectedType).To(BeTrue()) + }) + }) +}) diff --git a/cmd/fakes_test.go b/lib/fakes.go similarity index 87% rename from cmd/fakes_test.go rename to lib/fakes.go index af368641..e1ffb71d 100644 --- a/cmd/fakes_test.go +++ b/lib/fakes.go @@ -1,4 +1,4 @@ -package cmd +package lib import ( "context" @@ -19,9 +19,17 @@ type ( srFunc func(context.Context, *pyxis.CertificationInput) (*pyxis.CertificationResults, error) ) -// fakePyxisClient is a configurable pyxisClient for use in testing. It accepts function definitions to +func NewFakePyxisClientNoop() *FakePyxisClient { + return &FakePyxisClient{ + findImagesByDigestFunc: fidbFuncNoop, + getProjectsFunc: gpFuncNoop, + submitResultsFunc: srFuncNoop, + } +} + +// FakePyxisClient is a configurable pyxisClient for use in testing. It accepts function definitions to // use to implement a cmd.pyxisClient. -type fakePyxisClient struct { +type FakePyxisClient struct { findImagesByDigestFunc fibdFunc getProjectsFunc gpFunc submitResultsFunc srFunc @@ -29,7 +37,7 @@ type fakePyxisClient struct { // baseProject returns a pyxis.CertProject with an id of projectID, or a base value // if none is provided. -func (pc *fakePyxisClient) baseProject(projectID string) pyxis.CertProject { +func (pc *FakePyxisClient) baseProject(projectID string) pyxis.CertProject { pid := "000000000000" if len(projectID) > 0 { pid = projectID @@ -44,7 +52,7 @@ func (pc *fakePyxisClient) baseProject(projectID string) pyxis.CertProject { // successfulCertResults returns a pyxis.CertificationResults for use in tests emulating successful // submission. -func (pc *fakePyxisClient) successfulCertResults(projectID, certImageID string) pyxis.CertificationResults { +func (pc *FakePyxisClient) successfulCertResults(projectID, certImageID string) pyxis.CertificationResults { pid := "000000000000" if len(projectID) > 0 { pid = projectID @@ -65,14 +73,14 @@ func (pc *fakePyxisClient) successfulCertResults(projectID, certImageID string) } // setGPFuncReturnBaseProject sets pc.getProjectFunc to a function that returns baseProject. -// This is a fakePyxisClient method because it enables standardizing on a single value of -// a CertificationProject for GetProject calls, tied to the instance of fakePyxisClient -func (pc *fakePyxisClient) setGPFuncReturnBaseProject(projectID string) { +// This is a FakePyxisClient method because it enables standardizing on a single value of +// a CertificationProject for GetProject calls, tied to the instance of FakePyxisClient +func (pc *FakePyxisClient) setGPFuncReturnBaseProject(projectID string) { baseproj := pc.baseProject(projectID) pc.getProjectsFunc = func(context.Context) (*pyxis.CertProject, error) { return &baseproj, nil } } -func (pc *fakePyxisClient) setSRFuncSubmitSuccessfully(projectID, certImageID string) { +func (pc *FakePyxisClient) setSRFuncSubmitSuccessfully(projectID, certImageID string) { baseproj := pc.baseProject(projectID) certresults := pc.successfulCertResults(baseproj.ID, certImageID) pc.submitResultsFunc = func(context.Context, *pyxis.CertificationInput) (*pyxis.CertificationResults, error) { @@ -80,15 +88,15 @@ func (pc *fakePyxisClient) setSRFuncSubmitSuccessfully(projectID, certImageID st } } -func (pc *fakePyxisClient) FindImagesByDigest(ctx context.Context, digests []string) ([]pyxis.CertImage, error) { +func (pc *FakePyxisClient) FindImagesByDigest(ctx context.Context, digests []string) ([]pyxis.CertImage, error) { return pc.findImagesByDigestFunc(ctx, digests) } -func (pc *fakePyxisClient) GetProject(ctx context.Context) (*pyxis.CertProject, error) { +func (pc *FakePyxisClient) GetProject(ctx context.Context) (*pyxis.CertProject, error) { return pc.getProjectsFunc(ctx) } -func (pc *fakePyxisClient) SubmitResults(ctx context.Context, ci *pyxis.CertificationInput) (*pyxis.CertificationResults, error) { +func (pc *FakePyxisClient) SubmitResults(ctx context.Context, ci *pyxis.CertificationInput) (*pyxis.CertificationResults, error) { return pc.submitResultsFunc(ctx, ci) } @@ -131,17 +139,17 @@ func srFuncReturnError(ctx context.Context, ci *pyxis.CertificationInput) (*pyxi return nil, errors.New("some submission error") } -// fidbFuncNoop implements a fidbFunc, best to use while instantiating fakePyxisClient. +// fidbFuncNoop implements a fidbFunc, best to use while instantiating FakePyxisClient. func fidbFuncNoop(ctx context.Context, digests []string) ([]pyxis.CertImage, error) { return nil, nil } -// gpFuncNoop implements a gpFunc, best to use while instantiating fakePyxisClient. +// gpFuncNoop implements a gpFunc, best to use while instantiating FakePyxisClient. func gpFuncNoop(ctx context.Context) (*pyxis.CertProject, error) { return nil, nil } -// srFuncNoop implements a srFuncNoop, best to use while instantiating fakePyxisClient. +// srFuncNoop implements a srFuncNoop, best to use while instantiating FakePyxisClient. func srFuncNoop(ctx context.Context, ci *pyxis.CertificationInput) (*pyxis.CertificationResults, error) { return nil, nil } diff --git a/lib/lib.go b/lib/lib.go new file mode 100644 index 00000000..1ef90d47 --- /dev/null +++ b/lib/lib.go @@ -0,0 +1,229 @@ +package lib + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/artifacts" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/engine" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/formatters" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/policy" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/runtime" + log "github.com/sirupsen/logrus" +) + +// CheckContainerRunner contains all of the components necessary to run checkContainer. +type CheckContainerRunner struct { + Cfg *runtime.Config + Pc PyxisClient + Eng engine.CheckEngine + Formatter formatters.ResponseFormatter + Rw ResultWriter + Rs ResultSubmitter +} + +func NewCheckContainerRunner(ctx context.Context, cfg *runtime.Config, submit bool) (*CheckContainerRunner, error) { + cfg.Policy = policy.PolicyContainer + cfg.Submit = submit + + pyxisClient := NewPyxisClient(ctx, cfg.ReadOnly()) + // If we have a pyxisClient, we can query for container policy exceptions. + if pyxisClient != nil { + policy, err := GetContainerPolicyExceptions(ctx, pyxisClient) + if err != nil { + return nil, err + } + + cfg.Policy = policy + } + + engine, err := engine.NewForConfig(ctx, cfg.ReadOnly()) + if err != nil { + return nil, err + } + + fmttr, err := formatters.NewForConfig(cfg.ReadOnly()) + if err != nil { + return nil, err + } + + rs := ResolveSubmitter(pyxisClient, cfg.ReadOnly()) + + return &CheckContainerRunner{ + Cfg: cfg, + Pc: pyxisClient, + Eng: engine, + Formatter: fmttr, + Rw: &runtime.ResultWriterFile{}, + Rs: rs, + }, nil +} + +// checkOperatorRunner contains all of the components necessary to run checkOperator. +type CheckOperatorRunner struct { + Cfg *runtime.Config + Eng engine.CheckEngine + Formatter formatters.ResponseFormatter + Rw ResultWriter +} + +// newCheckOperatorRunner returns a checkOperatorRunner containing all of the tooling necessary +// to run checkOperator. +func NewCheckOperatorRunner(ctx context.Context, cfg *runtime.Config) (*CheckOperatorRunner, error) { + cfg.Policy = policy.PolicyOperator + cfg.Submit = false // there's no such thing as submitting for operators today. + + engine, err := engine.NewForConfig(ctx, cfg.ReadOnly()) + if err != nil { + return nil, err + } + + fmttr, err := formatters.NewForConfig(cfg.ReadOnly()) + if err != nil { + return nil, err + } + + return &CheckOperatorRunner{ + Cfg: cfg, + Eng: engine, + Formatter: fmttr, + Rw: &runtime.ResultWriterFile{}, + }, nil +} + +// resolveSubmitter will build out a resultSubmitter if the provided pyxisClient, pc, is not nil. +// The pyxisClient is a required component of the submitter. If pc is nil, then a noop submitter +// is returned instead, which does nothing. +func ResolveSubmitter(pc PyxisClient, cfg certification.Config) ResultSubmitter { + if pc != nil { + return &ContainerCertificationSubmitter{ + CertificationProjectID: cfg.CertificationProjectID(), + Pyxis: pc, + DockerConfig: cfg.DockerConfig(), + PreflightLogFile: cfg.LogFile(), + } + } + return NewNoopSubmitter(true, "", nil) +} + +// GetContainerPolicyExceptions will query Pyxis to determine if +// a given project has a certification excemptions, such as root or scratch. +// This will then return the corresponding policy. +// +// If no policy exception flags are found on the project, the standard +// container policy is returned. +func GetContainerPolicyExceptions(ctx context.Context, pc PyxisClient) (policy.Policy, error) { + certProject, err := pc.GetProject(ctx) + if err != nil { + return "", fmt.Errorf("could not retrieve project: %w", err) + } + // log.Debugf("Certification project name is: %s", certProject.Name) + if certProject.Container.Type == "scratch" { + return policy.PolicyScratch, nil + } + + // if a partner sets `Host Level Access` in connect to `Privileged`, enable RootExceptionContainerPolicy checks + if certProject.Container.Privileged { + return policy.PolicyRoot, nil + } + return policy.PolicyContainer, nil +} + +// PreflightCheck executes checks, interacts with pyxis, format output, writes, and submits results. +func PreflightCheck( + ctx context.Context, + cfg *runtime.Config, + pc PyxisClient, //nolint:unparam // pyxisClient is currently unused. + eng engine.CheckEngine, + formatter formatters.ResponseFormatter, + rw ResultWriter, + rs ResultSubmitter, +) error { + // configure the artifacts directory if the user requested a different directory. + if cfg.Artifacts != "" { + artifacts.SetDir(cfg.Artifacts) + } + + // create the results file early to catch cases where we are not + // able to write to the filesystem before we attempt to execute checks. + resultsFilePath, err := artifacts.WriteFile(resultsFilenameWithExtension(formatter.FileExtension()), strings.NewReader("")) + if err != nil { + return err + } + resultsFile, err := rw.OpenFile(resultsFilePath) + if err != nil { + return err + } + defer resultsFile.Close() + + resultsOutputTarget := io.MultiWriter(os.Stdout, resultsFile) + + // execute the checks + if err := eng.ExecuteChecks(ctx); err != nil { + return err + } + results := eng.Results(ctx) + + // return results to the user and then close output files + formattedResults, err := formatter.Format(ctx, results) + if err != nil { + return err + } + + fmt.Fprintln(resultsOutputTarget, string(formattedResults)) + + if cfg.WriteJUnit { + if err := writeJUnit(ctx, results); err != nil { + return err + } + } + + if cfg.Submit { + if err := rs.Submit(ctx); err != nil { + return err + } + } + + log.Infof("Preflight result: %s", convertPassedOverall(results.PassedOverall)) + + return nil +} + +func writeJUnit(ctx context.Context, results runtime.Results) error { + var cfg runtime.Config + cfg.ResponseFormat = "junitxml" + + junitformatter, err := formatters.NewForConfig(cfg.ReadOnly()) + if err != nil { + return err + } + junitResults, err := junitformatter.Format(ctx, results) + if err != nil { + return err + } + + junitFilename, err := artifacts.WriteFile("results-junit.xml", bytes.NewReader((junitResults))) + if err != nil { + return err + } + log.Tracef("JUnitXML written to %s", junitFilename) + + return nil +} + +func resultsFilenameWithExtension(ext string) string { + return strings.Join([]string{"results", ext}, ".") +} + +func convertPassedOverall(passedOverall bool) string { + if passedOverall { + return "PASSED" + } + + return "FAILED" +} diff --git a/lib/lib_test.go b/lib/lib_test.go new file mode 100644 index 00000000..55c21f80 --- /dev/null +++ b/lib/lib_test.go @@ -0,0 +1 @@ +package lib diff --git a/cmd/preflight_check_test.go b/lib/preflight_check_test.go similarity index 87% rename from cmd/preflight_check_test.go rename to lib/preflight_check_test.go index 8597db17..da7321c7 100644 --- a/cmd/preflight_check_test.go +++ b/lib/preflight_check_test.go @@ -1,4 +1,4 @@ -package cmd +package lib import ( "context" @@ -23,11 +23,11 @@ var _ = Describe("Preflight Check Func", func() { var localArtifactsDir string var cfg *runtime.Config - var pc pyxisClient + var pc PyxisClient var eng engine.CheckEngine var fmttr formatters.ResponseFormatter - var rw resultWriter - var rs resultSubmitter + var rw ResultWriter + var rs ResultSubmitter BeforeEach(func() { // instantiate err to make sure we can equal-assign in the following line. @@ -45,7 +45,7 @@ var _ = Describe("Preflight Check Func", func() { Artifacts: localArtifactsDir, } - pc = &fakePyxisClient{ + pc = &FakePyxisClient{ findImagesByDigestFunc: fidbFuncNoop, getProjectsFunc: gpFuncNoop, submitResultsFunc: srFuncNoop, @@ -58,7 +58,7 @@ var _ = Describe("Preflight Check Func", func() { fmttr, _ = formatters.NewByName(formatters.DefaultFormat) rw = &runtime.ResultWriterFile{} - rs = &noopSubmitter{} + rs = NewNoopSubmitter(false, "", nil) DeferCleanup(os.RemoveAll, localTempDir) DeferCleanup(os.RemoveAll, localArtifactsDir) @@ -68,7 +68,7 @@ var _ = Describe("Preflight Check Func", func() { Context("with a customized artifacts directory", func() { It("should set the artifacts directory accordingly", func() { // it's possible this will throw an error, but we dont' care for this test. - _ = preflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) + _ = PreflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) Expect(artifacts.Path()).To(Equal(localArtifactsDir)) }) }) @@ -79,7 +79,7 @@ var _ = Describe("Preflight Check Func", func() { }) It("should throw an error", func() { - err := preflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) + err := PreflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("some result writer error")) }) @@ -92,7 +92,7 @@ var _ = Describe("Preflight Check Func", func() { eng = fakeCheckEngine{errorRunningChecks: true, errorMsg: msg} }) It("should thrown an error", func() { - err := preflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) + err := PreflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring(msg)) }) @@ -106,7 +106,7 @@ var _ = Describe("Preflight Check Func", func() { }) It("should throw an error", func() { - err := preflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) + err := PreflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring(msg)) }) @@ -117,7 +117,7 @@ var _ = Describe("Preflight Check Func", func() { cfg.WriteJUnit = true }) It("should write a junit file in the artifacts directory", func() { - err := preflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) + err := PreflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) Expect(err).ToNot(HaveOccurred()) Expect(path.Join(artifacts.Path(), "results-junit.xml")).To(BeAnExistingFile()) }) @@ -134,7 +134,7 @@ var _ = Describe("Preflight Check Func", func() { }) It("should throw an error", func() { - err := preflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) + err := PreflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring(msg)) }) @@ -146,7 +146,7 @@ var _ = Describe("Preflight Check Func", func() { }) It("should complete with no errors", func() { - err := preflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) + err := PreflightCheck(context.TODO(), cfg, pc, eng, fmttr, rw, rs) Expect(err).ToNot(HaveOccurred()) }) }) diff --git a/cmd/types.go b/lib/types.go similarity index 72% rename from cmd/types.go rename to lib/types.go index 472031de..db771611 100644 --- a/cmd/types.go +++ b/lib/types.go @@ -1,4 +1,4 @@ -package cmd +package lib import ( "context" @@ -13,23 +13,24 @@ import ( "github.com/redhat-openshift-ecosystem/openshift-preflight/certification" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/artifacts" "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/pyxis" + "github.com/spf13/viper" log "github.com/sirupsen/logrus" ) // resultWriter defines methods associated with writing check results. -type resultWriter interface { +type ResultWriter interface { OpenFile(name string) (io.WriteCloser, error) io.WriteCloser } // resultSubmitter defines methods associated with submitting results to Red HAt. -type resultSubmitter interface { +type ResultSubmitter interface { Submit(context.Context) error } // pyxisClient defines pyxis API interactions that are relevant to check executions in cmd. -type pyxisClient interface { +type PyxisClient interface { FindImagesByDigest(ctx context.Context, digests []string) ([]pyxis.CertImage, error) GetProject(context.Context) (*pyxis.CertProject, error) SubmitResults(context.Context, *pyxis.CertificationInput) (*pyxis.CertificationResults, error) @@ -40,7 +41,7 @@ type pyxisClient interface { // Callers should treat a nil pyxis client as an indicator that pyxis calls should not be made. // //nolint:unparam // ctx is unused. Keep for future use. -func newPyxisClient(ctx context.Context, cfg certification.Config) pyxisClient { +func NewPyxisClient(ctx context.Context, cfg certification.Config) PyxisClient { if cfg.CertificationProjectID() == "" || cfg.PyxisAPIToken() == "" || cfg.PyxisHost() == "" { return nil } @@ -53,20 +54,20 @@ func newPyxisClient(ctx context.Context, cfg certification.Config) pyxisClient { ) } -// containerCertificationSubmitter submits container results to Pyxis, and implements +// ContainerCertificationSubmitter submits container results to Pyxis, and implements // a resultSubmitter. -type containerCertificationSubmitter struct { - certificationProjectID string - pyxis pyxisClient - dockerConfig string - preflightLogFile string +type ContainerCertificationSubmitter struct { + CertificationProjectID string + Pyxis PyxisClient + DockerConfig string + PreflightLogFile string } -func (s *containerCertificationSubmitter) Submit(ctx context.Context) error { +func (s *ContainerCertificationSubmitter) Submit(ctx context.Context) error { log.Info("preparing results that will be submitted to Red Hat") // get the project info from pyxis - certProject, err := s.pyxis.GetProject(ctx) + certProject, err := s.Pyxis.GetProject(ctx) if err != nil { return fmt.Errorf("could not retrieve project: %w", err) } @@ -83,11 +84,11 @@ func (s *containerCertificationSubmitter) Submit(ctx context.Context) error { // only read the dockerfile if the user provides a location for the file // at this point in the flow, if `cfg.DockerConfig` is empty we know the repo is public and can continue the submission flow - if s.dockerConfig != "" { - dockerConfigJSONBytes, err := os.ReadFile(s.dockerConfig) + if s.DockerConfig != "" { + dockerConfigJSONBytes, err := os.ReadFile(s.DockerConfig) if err != nil { return fmt.Errorf("could not open file for submission: %s: %w", - s.dockerConfig, + s.DockerConfig, err, ) } @@ -100,7 +101,7 @@ func (s *containerCertificationSubmitter) Submit(ctx context.Context) error { // if we were to send what pyixs just sent us in a update call, pyxis would throw a validation error saying it's not valid json // the below code aims to set the DockerConfigJSON to an empty string, and since this field is `omitempty` when we marshall it // we will not get a validation error - if s.dockerConfig == "" { + if s.DockerConfig == "" { certProject.Container.DockerConfigJSON = "" } @@ -137,11 +138,11 @@ func (s *containerCertificationSubmitter) Submit(ctx context.Context) error { } defer rpmManifest.Close() - logfile, err := os.Open(s.preflightLogFile) + logfile, err := os.Open(s.PreflightLogFile) if err != nil { return fmt.Errorf( "could not open file for submission: %s: %w", - s.preflightLogFile, + s.PreflightLogFile, err, ) } @@ -155,14 +156,14 @@ func (s *containerCertificationSubmitter) Submit(ctx context.Context) error { // The certification engine writes the rpmManifest for images not based on scratch. WithRPMManifest(rpmManifest). // Include the preflight execution log file. - WithArtifact(logfile, filepath.Base(s.preflightLogFile)) + WithArtifact(logfile, filepath.Base(s.PreflightLogFile)) input, err := submission.Finalize() if err != nil { return fmt.Errorf("unable to finalize data that would be sent to pyxis: %w", err) } - certResults, err := s.pyxis.SubmitResults(ctx, input) + certResults, err := s.Pyxis.SubmitResults(ctx, input) if err != nil { return fmt.Errorf("could not submit to pyxis: %w", err) } @@ -170,23 +171,31 @@ func (s *containerCertificationSubmitter) Submit(ctx context.Context) error { log.Info("Test results have been submitted to Red Hat.") log.Info("These results will be reviewed by Red Hat for final certification.") log.Infof("The container's image id is: %s.", certResults.CertImage.ID) - log.Infof("Please check %s to view scan results.", buildScanResultsURL(s.certificationProjectID, certResults.CertImage.ID)) - log.Infof("Please check %s to monitor the progress.", buildOverviewURL(s.certificationProjectID)) + log.Infof("Please check %s to view scan results.", BuildScanResultsURL(s.CertificationProjectID, certResults.CertImage.ID)) + log.Infof("Please check %s to monitor the progress.", BuildOverviewURL(s.CertificationProjectID)) return nil } // noopSubmitter is a no-op resultSubmitter that optionally logs a message // and a reason as to why results were not submitted. -type noopSubmitter struct { +type NoopSubmitter struct { emitLog bool reason string log *log.Logger } -var _ resultSubmitter = &noopSubmitter{} +func NewNoopSubmitter(emitLog bool, reason string, log *log.Logger) *NoopSubmitter { + return &NoopSubmitter{ + emitLog: emitLog, + reason: reason, + log: log, + } +} -func (s *noopSubmitter) Submit(ctx context.Context) error { +var _ ResultSubmitter = &NoopSubmitter{} + +func (s *NoopSubmitter) Submit(ctx context.Context) error { if s.emitLog { msg := "Results are not being sent for submission." if s.reason != "" { @@ -198,3 +207,30 @@ func (s *noopSubmitter) Submit(ctx context.Context) error { return nil } + +func (s *NoopSubmitter) SetEmitLog(emitLog bool) { + s.emitLog = emitLog +} + +func (s *NoopSubmitter) SetReason(reason string) { + s.reason = reason +} + +func BuildConnectURL(projectID string) string { + connectURL := fmt.Sprintf("https://connect.redhat.com/projects/%s", projectID) + + pyxisEnv := viper.GetString("pyxis_env") + if len(pyxisEnv) > 0 && pyxisEnv != "prod" { + connectURL = fmt.Sprintf("https://connect.%s.redhat.com/projects/%s", viper.GetString("pyxis_env"), projectID) + } + + return connectURL +} + +func BuildOverviewURL(projectID string) string { + return fmt.Sprintf("%s/overview", BuildConnectURL(projectID)) +} + +func BuildScanResultsURL(projectID string, imageID string) string { + return fmt.Sprintf("%s/images/%s/scan-results", BuildConnectURL(projectID), imageID) +}