Repository: redhat-openshift-ecosystem/openshift-preflight
Pull request #299 (2021-10-20 14:52:23)

Add optional values if target is OpenShift 4.9 or greater
Many deprecated APIs were removed in OpenShift 4.9 (k8s 1.22). There are
additional check in bundle validate to enforce this. This checks the target
version listed in the bundle, and enables additional checks if the target
is >= 4.9.

Fixes #295

Signed-off-by: Brad P. Crochet <brad@redhat.com>


Patch:
diff --git a/certification/internal/cli/operatorsdk.go b/certification/internal/cli/operatorsdk.go
index 971777e..4ef2b08 100644
--- a/certification/internal/cli/operatorsdk.go
+++ b/certification/internal/cli/operatorsdk.go
@@ -34,6 +34,7 @@ type OperatorSdkBundleValidateOptions struct {
 	LogLevel        string
 	ContainerEngine string
 	Selector        []string
+	OptionalValues  map[string]string
 	OutputFormat    string
 	Verbose         bool
 }
diff --git a/certification/internal/engine/operatorsdk.go b/certification/internal/engine/operatorsdk.go
index ecda594..8796d30 100644
--- a/certification/internal/engine/operatorsdk.go
+++ b/certification/internal/engine/operatorsdk.go
@@ -106,6 +106,11 @@ func (o operatorSdkEngine) BundleValidate(image string, opts cli.OperatorSdkBund
 			cmdArgs = append(cmdArgs, "--select-optional", fmt.Sprintf("name=%s", selector))
 		}
 	}
+	if opts.OptionalValues != nil {
+		for key, value := range opts.OptionalValues {
+			cmdArgs = append(cmdArgs, "--optional-values", fmt.Sprintf("%s=%s", key, value))
+		}
+	}
 	if opts.Verbose {
 		cmdArgs = append(cmdArgs, "--verbose")
 	}
diff --git a/certification/internal/policy/operator/default.go b/certification/internal/policy/operator/default.go
index 5376af7..5f799ad 100644
--- a/certification/internal/policy/operator/default.go
+++ b/certification/internal/policy/operator/default.go
@@ -15,4 +15,7 @@ const (
 
 	// secretName is the K8s secret name which stores the auth keys for the private registry access
 	secretName = "registry-auth-keys"
+
+	// versionsKey is the OpenShift versions in annotations.yaml that lists the versions allowed for an operator
+	versionsKey = "com.redhat.openshift.versions"
 )
diff --git a/certification/internal/policy/operator/validate_operator_bundle.go b/certification/internal/policy/operator/validate_operator_bundle.go
index 8395f82..8fcc7d0 100644
--- a/certification/internal/policy/operator/validate_operator_bundle.go
+++ b/certification/internal/policy/operator/validate_operator_bundle.go
@@ -1,6 +1,9 @@
 package operator
 
 import (
+	"strings"
+
+	"github.com/blang/semver"
 	"github.com/redhat-openshift-ecosystem/openshift-preflight/certification"
 	"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/internal/cli"
 	log "github.com/sirupsen/logrus"
@@ -18,6 +21,8 @@ func NewValidateOperatorBundleCheck(operatorSdkEngine *cli.OperatorSdkEngine) *V
 	}
 }
 
+const ocpVerV1beta1Unsupported = "4.9"
+
 func (p ValidateOperatorBundleCheck) Validate(bundleRef certification.ImageReference) (bool, error) {
 	report, err := p.getDataToValidate(bundleRef.ImageFSPath)
 	if err != nil {
@@ -37,6 +42,21 @@ func (p ValidateOperatorBundleCheck) getDataToValidate(imagePath string) (*cli.O
 		OutputFormat:    "json-alpha1",
 	}
 
+	annotations, err := getAnnotationsFromBundle(imagePath)
+	if err != nil {
+		log.Errorf("unable to get annotations.yaml from the bundle")
+		return nil, err
+	}
+
+	if versions, ok := annotations[versionsKey]; ok {
+		// Check that the label range contains >= 4.9
+		if isTarget49OrGreater(versions) {
+			log.Debug("OpenShift 4.9 detected in annotations. Running with additional checks enabled.")
+			opts.OptionalValues = make(map[string]string)
+			opts.OptionalValues["k8s-version"] = "1.22"
+		}
+	}
+
 	return p.OperatorSdkEngine.BundleValidate(imagePath, opts)
 }
 
@@ -58,6 +78,78 @@ func (p ValidateOperatorBundleCheck) validate(report *cli.OperatorSdkBundleValid
 	return report.Passed, nil
 }
 
+func isTarget49OrGreater(ocpLabelIndex string) bool {
+	semVerOCPV1beta1Unsupported, _ := semver.ParseTolerant(ocpVerV1beta1Unsupported)
+	// the OCP range informed cannot allow carry on to OCP 4.9+
+	beginsEqual := strings.HasPrefix(ocpLabelIndex, "=")
+	// It means that the OCP label is =OCP version
+	if beginsEqual {
+		version := cleanStringToGetTheVersionToParse(strings.Split(ocpLabelIndex, "=")[1])
+		verParsed, err := semver.ParseTolerant(version)
+		if err != nil {
+			log.Errorf("unable to parse the value (%s) on (%s)", version, ocpLabelIndex)
+			return false
+		}
+
+		if verParsed.GE(semVerOCPV1beta1Unsupported) {
+			return true
+		}
+		return false
+	}
+	indexRange := cleanStringToGetTheVersionToParse(ocpLabelIndex)
+	if len(indexRange) > 1 {
+		// Bare version
+		if !strings.Contains(indexRange, "-") {
+			verParsed, err := semver.ParseTolerant(indexRange)
+			if err != nil {
+				log.Error("unable to parse the version")
+				return false
+			}
+			if verParsed.GE(semVerOCPV1beta1Unsupported) {
+				return true
+			}
+		}
+
+		versions := strings.Split(indexRange, "-")
+		version := versions[0]
+		if len(versions) > 1 {
+			version = versions[1]
+			verParsed, err := semver.ParseTolerant(version)
+			if err != nil {
+				log.Error("unable to parse the version")
+				return false
+			}
+
+			if verParsed.GE(semVerOCPV1beta1Unsupported) {
+				return true
+			}
+			return false
+		}
+
+		verParsed, err := semver.ParseTolerant(version)
+		if err != nil {
+			log.Error("unable to parse the version")
+			return false
+		}
+
+		if semVerOCPV1beta1Unsupported.GE(verParsed) {
+			return true
+		}
+	}
+	return false
+}
+
+// cleanStringToGetTheVersionToParse will remove the expected characters for
+// we are able to parse the version informed.
+func cleanStringToGetTheVersionToParse(value string) string {
+	doubleQuote := "\""
+	singleQuote := "'"
+	value = strings.ReplaceAll(value, singleQuote, "")
+	value = strings.ReplaceAll(value, doubleQuote, "")
+	value = strings.ReplaceAll(value, "v", "")
+	return value
+}
+
 func (p ValidateOperatorBundleCheck) Name() string {
 	return "ValidateOperatorBundle"
 }
diff --git a/go.mod b/go.mod
index 1ac2da4..2073b79 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/redhat-openshift-ecosystem/openshift-preflight
 go 1.16
 
 require (
+	github.com/blang/semver v3.5.1+incompatible
 	github.com/docker/docker-credential-helpers v0.6.4 // indirect
 	github.com/fsnotify/fsnotify v1.5.1 // indirect
 	github.com/google/go-containerregistry v0.6.0


Test patch:
diff --git a/certification/internal/policy/operator/validate_operator_bundle_test.go b/certification/internal/policy/operator/validate_operator_bundle_test.go
index f75faf6..d2791f4 100644
--- a/certification/internal/policy/operator/validate_operator_bundle_test.go
+++ b/certification/internal/policy/operator/validate_operator_bundle_test.go
@@ -1,16 +1,31 @@
 package operator
 
 import (
+	"os"
+	"path/filepath"
+
 	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
 	. "github.com/onsi/gomega"
 	"github.com/redhat-openshift-ecosystem/openshift-preflight/certification"
 	"github.com/redhat-openshift-ecosystem/openshift-preflight/certification/internal/cli"
 )
 
 var _ = Describe("BundleValidateCheck", func() {
+	const (
+		metadataDir        = "metadata"
+		annotationFilename = "annotations.yaml"
+		annotations        = `annotations:
+  com.redhat.openshift.versions: "v4.6-v4.9"
+  operators.operatorframework.io.bundle.package.v1: testPackage
+  operators.operatorframework.io.bundle.channel.default.v1: testChannel
+`
+	)
+
 	var (
 		bundleValidateCheck ValidateOperatorBundleCheck
 		fakeEngine          cli.OperatorSdkEngine
+		imageRef            certification.ImageReference
 	)
 
 	BeforeEach(func() {
@@ -28,12 +43,25 @@ var _ = Describe("BundleValidateCheck", func() {
 		fakeEngine = FakeOperatorSdkEngine{
 			OperatorSdkBVReport: report,
 		}
+
+		// mock bundle directory
+		tmpDir, err := os.MkdirTemp("", "bundle-metadata-*")
+		Expect(err).ToNot(HaveOccurred())
+
+		err = os.Mkdir(filepath.Join(tmpDir, metadataDir), 0755)
+		Expect(err).ToNot(HaveOccurred())
+
+		err = os.WriteFile(filepath.Join(tmpDir, metadataDir, annotationFilename), []byte(annotations), 0644)
+		Expect(err).ToNot(HaveOccurred())
+
+		imageRef.ImageFSPath = tmpDir
+
 		bundleValidateCheck = *NewValidateOperatorBundleCheck(&fakeEngine)
 	})
 	Describe("Operator Bundle Validate", func() {
 		Context("When Operator Bundle Validate passes", func() {
 			It("Should pass Validate", func() {
-				ok, err := bundleValidateCheck.Validate(certification.ImageReference{ImageURI: "dummy/image"})
+				ok, err := bundleValidateCheck.Validate(imageRef)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(ok).To(BeTrue())
 			})
@@ -50,7 +78,7 @@ var _ = Describe("BundleValidateCheck", func() {
 				bundleValidateCheck = *NewValidateOperatorBundleCheck(&fakeEngine)
 			})
 			It("Should not pass Validate", func() {
-				ok, err := bundleValidateCheck.Validate(certification.ImageReference{ImageURI: "dummy/image"})
+				ok, err := bundleValidateCheck.Validate(imageRef)
 				Expect(err).ToNot(HaveOccurred())
 				Expect(ok).To(BeFalse())
 			})
@@ -69,4 +97,20 @@ var _ = Describe("BundleValidateCheck", func() {
 			})
 		})
 	})
+	DescribeTable("Image Registry validation",
+		func(versions string, expected bool) {
+			ok := isTarget49OrGreater(versions)
+			Expect(ok).To(Equal(expected))
+		},
+		Entry("range 4.6 to 4.8", "v4.6-v4.8", false),
+		Entry("exactly 4.8", "=v4.8", false),
+		Entry("exactly 4.9", "=v4.9", true),
+		Entry("range 4.6 to 4.9", "v4.6-v4.9", true),
+		Entry(">= 4.8", "v4.8", true),
+		Entry(">= 4.9", "v4.9", true),
+	)
+	AfterEach(func() {
+		err := os.RemoveAll(imageRef.ImageFSPath)
+		Expect(err).ToNot(HaveOccurred())
+	})
 })