Repository: redhat-openshift-ecosystem/openshift-preflight
Pull request #214 (2021-09-07 16:55:27)

Move BasedOnUBI to Crane Engine
This moves the check to Crane, and implements a check for the
"com.redhat.component" label, in that it contains "ubi".

It still checks for the contents of os-release.

Signed-off-by: Brad P. Crochet <brad@redhat.com>


Patch:
diff --git a/certification/engine/engine.go b/certification/engine/engine.go
index 15b7b33..c36f5b1 100644
--- a/certification/engine/engine.go
+++ b/certification/engine/engine.go
@@ -127,7 +127,7 @@ func queryNewChecks(checkName string) certification.Check {
 var deprecatedRunAsNonRootCheck certification.Check = &shell.RunAsNonRootCheck{}
 var deprecatedUnderLayerMaxCheck certification.Check = &shell.UnderLayerMaxCheck{}
 var deprecatedHasRequiredLabelCheck certification.Check = &shell.HasRequiredLabelsCheck{}
-var basedOnUbiCheck certification.Check = &shell.BaseOnUBICheck{}
+var deprecatedBasedOnUbiCheck certification.Check = &shell.BaseOnUBICheck{}
 var deprecatedHasLicenseCheck certification.Check = &shell.HasLicenseCheck{}
 var deprecatedHasUniqueTagCheck certification.Check = &shell.HasUniqueTagCheck{}
 var deprecatedValidateOperatorBundle certification.Check = &shell.ValidateOperatorBundleCheck{}
@@ -156,6 +156,7 @@ var maxLayersCheck certification.Check = &containerpol.MaxLayersCheck{}
 var hasNoProhibitedCheck certification.Check = &containerpol.HasNoProhibitedPackagesCheck{}
 var hasRequiredLabelsCheck certification.Check = &containerpol.HasRequiredLabelsCheck{}
 var runAsRootCheck certification.Check = &containerpol.RunAsNonRootCheck{}
+var basedOnUbiCheck certification.Check = &containerpol.BasedOnUBICheck{}
 
 var operatorPolicy = map[string]certification.Check{
 	operatorPkgNameIsUniqueCheck.Name(): operatorPkgNameIsUniqueCheck,
@@ -173,13 +174,14 @@ var containerPolicy = map[string]certification.Check{
 	hasNoProhibitedCheck.Name():   hasNoProhibitedCheck,
 	hasRequiredLabelsCheck.Name(): hasRequiredLabelsCheck,
 	runAsRootCheck.Name():         runAsRootCheck,
+	basedOnUbiCheck.Name():        basedOnUbiCheck,
 }
 
 var oldContainerPolicy = map[string]certification.Check{
 	deprecatedRunAsNonRootCheck.Name():     deprecatedRunAsNonRootCheck,
 	deprecatedUnderLayerMaxCheck.Name():    deprecatedUnderLayerMaxCheck,
 	deprecatedHasRequiredLabelCheck.Name(): deprecatedHasRequiredLabelCheck,
-	basedOnUbiCheck.Name():                 basedOnUbiCheck,
+	deprecatedBasedOnUbiCheck.Name():       deprecatedBasedOnUbiCheck,
 	deprecatedHasLicenseCheck.Name():       deprecatedHasLicenseCheck,
 	deprecatedHasUniqueTagCheck.Name():     deprecatedHasUniqueTagCheck,
 	deprecatedHasNoProhibitedCheck.Name():  deprecatedHasNoProhibitedCheck,
diff --git a/certification/internal/policy/container/base_on_ubi.go b/certification/internal/policy/container/base_on_ubi.go
new file mode 100644
index 0000000..31c15af
--- /dev/null
+++ b/certification/internal/policy/container/base_on_ubi.go
@@ -0,0 +1,90 @@
+package container
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+
+	cranev1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/redhat-openshift-ecosystem/openshift-preflight/certification"
+	log "github.com/sirupsen/logrus"
+)
+
+// BasedOnUBICheck evaluates if the provided image is based on the Red Hat Universal Base Image
+// by inspecting the contents of the `/etc/os-release` and identifying if the ID is `rhel` and the
+// Name value is `Red Hat Enterprise Linux`
+type BasedOnUBICheck struct{}
+
+func (p *BasedOnUBICheck) Validate(imgRef certification.ImageReference) (bool, error) {
+	labels, err := p.getLabels(imgRef.ImageInfo)
+	if err != nil {
+		return false, err
+	}
+
+	osRelease, err := p.getOsReleaseContents(imgRef.ImageFSPath)
+	if err != nil {
+		log.Debugf("could not retrieve contents of os-release")
+		return false, err
+	}
+
+	return p.validate(labels, osRelease)
+}
+
+func (p *BasedOnUBICheck) getLabels(image cranev1.Image) (map[string]string, error) {
+	configFile, err := image.ConfigFile()
+	if err != nil {
+		return nil, err
+	}
+	return configFile.Config.Labels, nil
+}
+
+func (p *BasedOnUBICheck) getOsReleaseContents(path string) ([]string, error) {
+	osrelease, err := os.ReadFile(filepath.Join(path, "etc", "os-release"))
+	if err != nil {
+		log.Debug("could not open os-release file for reading")
+		return nil, err
+	}
+
+	return strings.Split(string(osrelease), "\n"), nil
+}
+
+func (p *BasedOnUBICheck) validate(labels map[string]string, osRelease []string) (bool, error) {
+	var hasRHELID, hasRHELName, hasUbiComponentLabel bool
+	for _, value := range osRelease {
+		if strings.HasPrefix(value, `ID="rhel"`) {
+			hasRHELID = true
+		} else if strings.HasPrefix(value, `NAME="Red Hat Enterprise Linux"`) {
+			hasRHELName = true
+		}
+	}
+
+	if component, exists := labels["com.redhat.component"]; exists {
+		hasUbiComponentLabel = strings.Contains(component, "ubi")
+	}
+
+	if hasRHELID && hasRHELName && hasUbiComponentLabel {
+		return true, nil
+	}
+
+	return false, nil
+}
+
+func (p *BasedOnUBICheck) Name() string {
+	return "BasedOnUbi"
+}
+
+func (p *BasedOnUBICheck) Metadata() certification.Metadata {
+	return certification.Metadata{
+		Description:      "Checking if the container's base image is based upon the Red Hat Universal Base Image (UBI)",
+		Level:            "best",
+		KnowledgeBaseURL: "https://connect.redhat.com/zones/containers/container-certification-policy-guide", // Placeholder
+		CheckURL:         "https://connect.redhat.com/zones/containers/container-certification-policy-guide",
+	}
+}
+
+func (p *BasedOnUBICheck) Help() certification.HelpText {
+	return certification.HelpText{
+		Message:    "Check BasedOnUbi encountered an error. Please review the preflight.log file for more information.",
+		Suggestion: "Change the FROM directive in your Dockerfile or Containerfile to FROM registry.access.redhat.com/ubi8/ubi",
+	}
+}


Test patch:
diff --git a/certification/internal/policy/container/base_on_ubi_test.go b/certification/internal/policy/container/base_on_ubi_test.go
new file mode 100644
index 0000000..1e29424
--- /dev/null
+++ b/certification/internal/policy/container/base_on_ubi_test.go
@@ -0,0 +1,143 @@
+package container
+
+import (
+	"os"
+	"path/filepath"
+
+	cranev1 "github.com/google/go-containerregistry/pkg/v1"
+	fakecranev1 "github.com/google/go-containerregistry/pkg/v1/fake"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	"github.com/redhat-openshift-ecosystem/openshift-preflight/certification"
+)
+
+func labelsForUbiCheck(label string, bad bool) map[string]string {
+	labels := map[string]string{
+		"name":                 "name",
+		"vendor":               "vendor",
+		"version":              "version",
+		"release":              "release",
+		"summary":              "summary",
+		"description":          "description",
+		"com.redhat.component": label,
+	}
+
+	if bad {
+		delete(labels, "com.redhat.component")
+	}
+
+	return labels
+}
+
+func goodLabelsConfigFile() (*cranev1.ConfigFile, error) {
+	return &cranev1.ConfigFile{
+		Config: cranev1.Config{
+			Labels: labelsForUbiCheck("ubi8-container", false),
+		},
+	}, nil
+}
+
+func missingUbiLableConfigFile() (*cranev1.ConfigFile, error) {
+	return &cranev1.ConfigFile{
+		Config: cranev1.Config{
+			Labels: labelsForUbiCheck("", true),
+		},
+	}, nil
+}
+
+func badLabelsConfigFile() (*cranev1.ConfigFile, error) {
+	return &cranev1.ConfigFile{
+		Config: cranev1.Config{
+			Labels: labelsForUbiCheck("doesnothavestring", false),
+		},
+	}, nil
+}
+
+var _ = Describe("BaseOnUBI", func() {
+	var (
+		basedOnUbiCheck BasedOnUBICheck
+		imageRef        certification.ImageReference
+	)
+
+	const osrelease = "os-release"
+
+	BeforeEach(func() {
+		fakeImage := fakecranev1.FakeImage{
+			ConfigFileStub: goodLabelsConfigFile,
+		}
+		imageRef.ImageInfo = &fakeImage
+		var err error
+		tmpDir, err := os.MkdirTemp("", "based-on-ubi-*")
+		Expect(err).ToNot(HaveOccurred())
+		err = os.Mkdir(filepath.Join(tmpDir, "etc"), 0755)
+		Expect(err).ToNot(HaveOccurred())
+		err = os.WriteFile(filepath.Join(tmpDir, "etc", osrelease), []byte(`ID="rhel"
+NAME="Red Hat Enterprise Linux"
+`), 0644)
+		Expect(err).ToNot(HaveOccurred())
+		imageRef.ImageFSPath = tmpDir
+	})
+	AfterEach(func() {
+		os.RemoveAll(imageRef.ImageFSPath)
+	})
+	Describe("Checking for UBI as a base", func() {
+		Context("When it is based on UBI", func() {
+			Context("and has the correct labels and os-release", func() {
+				It("should pass Validate", func() {
+					ok, err := basedOnUbiCheck.Validate(imageRef)
+					Expect(err).ToNot(HaveOccurred())
+					Expect(ok).To(BeTrue())
+				})
+			})
+		})
+		Context("When it is not based on UBI", func() {
+			Context("and has the correct labels but bad os-release", func() {
+				JustBeforeEach(func() {
+					err := os.WriteFile(filepath.Join(imageRef.ImageFSPath, "etc", osrelease), []byte("Not a good file"), 0644)
+					Expect(err).ToNot(HaveOccurred())
+				})
+				It("should not pass Validate", func() {
+					ok, err := basedOnUbiCheck.Validate(imageRef)
+					Expect(err).ToNot(HaveOccurred())
+					Expect(ok).To(BeFalse())
+				})
+			})
+			Context("and has does not have correct labels but good os-release", func() {
+				JustBeforeEach(func() {
+					fakeImage := fakecranev1.FakeImage{
+						ConfigFileStub: badLabelsConfigFile,
+					}
+					imageRef.ImageInfo = &fakeImage
+				})
+				It("should not pass Validate", func() {
+					ok, err := basedOnUbiCheck.Validate(imageRef)
+					Expect(err).ToNot(HaveOccurred())
+					Expect(ok).To(BeFalse())
+				})
+			})
+			Context("and it is missing the correct label", func() {
+				JustBeforeEach(func() {
+					fakeImage := fakecranev1.FakeImage{
+						ConfigFileStub: missingUbiLableConfigFile,
+					}
+					imageRef.ImageInfo = &fakeImage
+				})
+				It("should not pass Validate", func() {
+					ok, err := basedOnUbiCheck.Validate(imageRef)
+					Expect(err).ToNot(HaveOccurred())
+					Expect(ok).To(BeFalse())
+				})
+			})
+			Context("and os-release is missing", func() {
+				JustBeforeEach(func() {
+					os.Remove(filepath.Join(imageRef.ImageFSPath, "etc", osrelease))
+				})
+				It("should not pass Validate", func() {
+					ok, err := basedOnUbiCheck.Validate(imageRef)
+					Expect(err).To(HaveOccurred())
+					Expect(ok).To(BeFalse())
+				})
+			})
+		})
+	})
+})