From 1bc39fc62e069c3c6c63048ddd5504fba280c37b 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 --- certification/runtime/result_writer.go | 2 +- cmd/check.go | 50 -- cmd/check_container.go | 106 +--- cmd/check_container_test.go | 470 ++---------------- cmd/check_operator.go | 50 +- cmd/check_operator_test.go | 64 --- cmd/check_test.go | 42 +- cmd/preflight_check.go | 76 --- {cmd => internal/lib}/fakes_test.go | 42 +- internal/lib/lib.go | 230 +++++++++ internal/lib/lib_container_test.go | 458 +++++++++++++++++ internal/lib/lib_operator_test.go | 75 +++ {cmd => internal/lib}/preflight_check_test.go | 26 +- internal/lib/suite_test.go | 28 ++ {cmd => internal/lib}/types.go | 99 ++-- 15 files changed, 953 insertions(+), 865 deletions(-) delete mode 100644 cmd/preflight_check.go rename {cmd => internal/lib}/fakes_test.go (85%) create mode 100644 internal/lib/lib.go create mode 100644 internal/lib/lib_container_test.go create mode 100644 internal/lib/lib_operator_test.go rename {cmd => internal/lib}/preflight_check_test.go (87%) create mode 100644 internal/lib/suite_test.go rename {cmd => internal/lib}/types.go (68%) diff --git a/certification/runtime/result_writer.go b/certification/runtime/result_writer.go index 96033348..876d8205 100644 --- a/certification/runtime/result_writer.go +++ b/certification/runtime/result_writer.go @@ -5,7 +5,7 @@ import ( "os" ) -// ResultWriterFile implements a resultWriter for use at preflight runtime. +// ResultWriterFile implements a ResultWriter for use at preflight runtime. type ResultWriterFile struct { file *os.File } diff --git a/cmd/check.go b/cmd/check.go index 68d30e98..39561c36 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -1,16 +1,8 @@ package cmd import ( - "bytes" - "context" - "fmt" "strings" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/artifacts" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/formatters" - "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/runtime" - - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -36,52 +28,10 @@ func checkCmd() *cobra.Command { return checkCmd } -// writeJUnit will write results as JUnit XML using the built-in formatter. -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 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..9911a74b 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/internal/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..6d7dbfef 100644 --- a/cmd/check_container_test.go +++ b/cmd/check_container_test.go @@ -3,25 +3,16 @@ 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/spf13/viper" + + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/formatters" + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/runtime" + "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/lib" ) var _ = Describe("Check Container Command", func() { @@ -36,432 +27,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 +139,31 @@ 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 + + cfg = &runtime.Config{ + Image: "quay.io/example/foo:latest", + ResponseFormat: formatters.DefaultFormat, + } + }) + + 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..daef6764 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/internal/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..2e2a2f1d 100644 --- a/cmd/check_operator_test.go +++ b/cmd/check_operator_test.go @@ -1,14 +1,8 @@ package cmd import ( - "context" "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/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/spf13/viper" @@ -116,64 +110,6 @@ var _ = Describe("Check Operator", func() { }) }) - Context("When instantiating a checkOperatorRunner", func() { - var cfg *runtime.Config - BeforeEach(func() { - cfg = &runtime.Config{ - Image: "quay.io/example/foo:latest", - ResponseFormat: formatters.DefaultFormat, - } - }) - - 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) - 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 formatter", func() { - It("should return an error", func() { - cfg.ResponseFormat = "foo" - _, err := newCheckOperatorRunner(context.TODO(), cfg) - Expect(err).To(HaveOccurred()) - }) - }) - - Context("with an invalid policy definition", func() { - It("should return the container policy engine anyway", func() { - cfg.Policy = "badpolicy" - beforeCfg := *cfg - runner, err := 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(err).ToNot(HaveOccurred()) - }) - }) - - Context("with an invalid formatter definition", func() { - It("should return an error", func() { - cfg.ResponseFormat = "foo" - _, err := newCheckOperatorRunner(context.TODO(), cfg) - Expect(err).To(HaveOccurred()) - }) - }) - - It("should contain a ResultWriterFile resultWriter", func() { - runner, err := newCheckOperatorRunner(context.TODO(), cfg) - Expect(err).ToNot(HaveOccurred()) - _, rwIsExpectedType := runner.rw.(*runtime.ResultWriterFile) - Expect(rwIsExpectedType).To(BeTrue()) - }) - }) - Context("When testing positional arg parsing", func() { // failure cases are tested earlier in this file by running executeCommand. // This tests the success case using the standalone function in order diff --git a/cmd/check_test.go b/cmd/check_test.go index 3422ff05..82671901 100644 --- a/cmd/check_test.go +++ b/cmd/check_test.go @@ -1,12 +1,10 @@ package cmd import ( - "context" "os" - "path/filepath" "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/internal/lib" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -46,7 +44,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 +54,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 +64,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 +74,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,40 +84,14 @@ 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)) }) }) }) - - Describe("JUnit", func() { - var results *runtime.Results - var junitfile string - - BeforeEach(func() { - results = &runtime.Results{ - TestedImage: "registry.example.com/example/image:0.0.1", - PassedOverall: true, - TestedOn: runtime.UnknownOpenshiftClusterVersion(), - CertificationHash: "sha256:deadb33f", - Passed: []runtime.Result{}, - Failed: []runtime.Result{}, - Errors: []runtime.Result{}, - } - junitfile = filepath.Join(artifacts.Path(), "results-junit.xml") - }) - - When("The additional JUnitXML results file is requested", func() { - It("should be written to the artifacts directory without error", func() { - Expect(writeJUnit(context.TODO(), *results)).To(Succeed()) - _, err := os.Stat(junitfile) - Expect(err).ToNot(HaveOccurred()) - }) - }) - }) }) 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/cmd/fakes_test.go b/internal/lib/fakes_test.go similarity index 85% rename from cmd/fakes_test.go rename to internal/lib/fakes_test.go index af368641..0b9b24df 100644 --- a/cmd/fakes_test.go +++ b/internal/lib/fakes_test.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 } @@ -190,7 +198,7 @@ func (e fakeCheckEngine) Results(ctx context.Context) runtime.Results { } } -// badResultWriter implements resultWriter and will automatically fail with the +// badResultWriter implements ResultWriter and will automatically fail with the // provided errmsg. type badResultWriter struct { errmsg string @@ -225,7 +233,7 @@ func (f *badFormatter) Format(ctx context.Context, r runtime.Results) ([]byte, e return nil, errors.New(f.errormsg) } -// badResultSubmitter implements resultSubmitter and fails to submit with the included errmsg. +// badResultSubmitter implements ResultSubmitter and fails to submit with the included errmsg. type badResultSubmitter struct { errmsg string } diff --git a/internal/lib/lib.go b/internal/lib/lib.go new file mode 100644 index 00000000..f9d038b0 --- /dev/null +++ b/internal/lib/lib.go @@ -0,0 +1,230 @@ +package lib + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "strings" + + log "github.com/sirupsen/logrus" + + "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" +) + +// 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/internal/lib/lib_container_test.go b/internal/lib/lib_container_test.go new file mode 100644 index 00000000..e46c4ffd --- /dev/null +++ b/internal/lib/lib_container_test.go @@ -0,0 +1,458 @@ +package lib + +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" +) + +var _ = Describe("Lib Container Functions", 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()) + }) + }) + + Describe("JUnit", func() { + var results *runtime.Results + var junitfile string + + BeforeEach(func() { + results = &runtime.Results{ + TestedImage: "registry.example.com/example/image:0.0.1", + PassedOverall: true, + TestedOn: runtime.UnknownOpenshiftClusterVersion(), + CertificationHash: "sha256:deadb33f", + Passed: []runtime.Result{}, + Failed: []runtime.Result{}, + Errors: []runtime.Result{}, + } + junitfile = filepath.Join(artifacts.Path(), "results-junit.xml") + }) + + When("The additional JUnitXML results file is requested", func() { + It("should be written to the artifacts directory without error", func() { + Expect(writeJUnit(context.TODO(), *results)).To(Succeed()) + _, err := os.Stat(junitfile) + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) +}) diff --git a/internal/lib/lib_operator_test.go b/internal/lib/lib_operator_test.go new file mode 100644 index 00000000..a5c41b80 --- /dev/null +++ b/internal/lib/lib_operator_test.go @@ -0,0 +1,75 @@ +package lib + +import ( + "context" + + "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/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Lib Operator Functions", func() { + BeforeEach(createAndCleanupDirForArtifactsAndLogs) + + Context("When instantiating a CheckOperatorRunner", func() { + var cfg *runtime.Config + BeforeEach(func() { + cfg = &runtime.Config{ + Image: "quay.io/example/foo:latest", + ResponseFormat: formatters.DefaultFormat, + } + }) + + 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) + 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 formatter", func() { + It("should return an error", func() { + cfg.ResponseFormat = "foo" + _, err := NewCheckOperatorRunner(context.TODO(), cfg) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("with an invalid policy definition", func() { + It("should return the container policy engine anyway", func() { + cfg.Policy = "badpolicy" + beforeCfg := *cfg + runner, err := 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(err).ToNot(HaveOccurred()) + }) + }) + + Context("with an invalid formatter definition", func() { + It("should return an error", func() { + cfg.ResponseFormat = "foo" + _, err := NewCheckOperatorRunner(context.TODO(), cfg) + Expect(err).To(HaveOccurred()) + }) + }) + + It("should contain a ResultWriterFile ResultWriter", func() { + runner, err := NewCheckOperatorRunner(context.TODO(), cfg) + Expect(err).ToNot(HaveOccurred()) + _, rwIsExpectedType := runner.Rw.(*runtime.ResultWriterFile) + Expect(rwIsExpectedType).To(BeTrue()) + }) + }) +}) diff --git a/cmd/preflight_check_test.go b/internal/lib/preflight_check_test.go similarity index 87% rename from cmd/preflight_check_test.go rename to internal/lib/preflight_check_test.go index 8597db17..da7321c7 100644 --- a/cmd/preflight_check_test.go +++ b/internal/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/internal/lib/suite_test.go b/internal/lib/suite_test.go new file mode 100644 index 00000000..4c7ad2b9 --- /dev/null +++ b/internal/lib/suite_test.go @@ -0,0 +1,28 @@ +package lib + +import ( + "os" + "path/filepath" + "testing" + + "github.com/redhat-openshift-ecosystem/openshift-preflight/certification/artifacts" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCMD(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "lib Suite") +} + +var createAndCleanupDirForArtifactsAndLogs = func() { + tmpDir, err := os.MkdirTemp("", "lib-execute-*") + Expect(err).ToNot(HaveOccurred()) + artifacts.SetDir(filepath.Join(tmpDir, "artifacts")) + os.Setenv("PFLT_ARTIFACTS", artifacts.Path()) + os.Setenv("PFLT_LOGFILE", filepath.Join(tmpDir, "preflight.log")) + DeferCleanup(os.RemoveAll, tmpDir) + DeferCleanup(os.Unsetenv, "PFLT_ARTIFACTS") + DeferCleanup(os.Unsetenv, "PFLT_LOGFILE") +} diff --git a/cmd/types.go b/internal/lib/types.go similarity index 68% rename from cmd/types.go rename to internal/lib/types.go index 472031de..95d74439 100644 --- a/cmd/types.go +++ b/internal/lib/types.go @@ -1,4 +1,4 @@ -package cmd +package lib import ( "context" @@ -10,6 +10,8 @@ import ( "path/filepath" "time" + "github.com/spf13/viper" + "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" @@ -17,19 +19,19 @@ import ( log "github.com/sirupsen/logrus" ) -// resultWriter defines methods associated with writing check results. -type resultWriter interface { +// ResultWriter defines methods associated with writing check results. +type ResultWriter interface { OpenFile(name string) (io.WriteCloser, error) io.WriteCloser } -// resultSubmitter defines methods associated with submitting results to Red HAt. -type resultSubmitter interface { +// ResultSubmitter defines methods associated with submitting results to Red HAt. +type ResultSubmitter interface { Submit(context.Context) error } -// pyxisClient defines pyxis API interactions that are relevant to check executions in cmd. -type pyxisClient interface { +// PyxisClient defines pyxis API interactions that are relevant to check executions in cmd. +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 +42,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 +55,20 @@ func newPyxisClient(ctx context.Context, cfg certification.Config) pyxisClient { ) } -// containerCertificationSubmitter submits container results to Pyxis, and implements -// a resultSubmitter. -type containerCertificationSubmitter struct { - certificationProjectID string - pyxis pyxisClient - dockerConfig string - preflightLogFile string +// ContainerCertificationSubmitter submits container results to Pyxis, and implements +// a ResultSubmitter. +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 +85,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 +102,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 +139,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 +157,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 +172,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 +// 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 +208,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) +}