Repository: operator-framework/operator-lifecycle-manager
Pull request #1508 (2020-05-07 18:49:25)

Bug 1818851: feat(catalogs): add spec validation for sourcetypes
Backport of error messaging changes from https://github.com/operator-framework/operator-lifecycle-manager/pull/1428.

openshift-ci-robot: @benluddy: No Bugzilla bug is referenced in the title of this pull request.
To reference a bug, add 'Bug XXX:' to the title of this pull request and request another bug refresh with <code>/bugzilla refresh</code>.

<details>

In response to [this](https://github.com/operator-framework/operator-lifecycle-manager/pull/1508):

>feat(catalogs): add spec validation for sourcetypes


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
openshift-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/operator-framework/operator-lifecycle-manager/pull/1508#" title="Author self-approved">benluddy</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **ecordell**
You can assign the PR to them by writing `/assign @ecordell` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=operator-framework%2Foperator-lifecycle-manager).

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

- **[OWNERS](https://github.com/operator-framework/operator-lifecycle-manager/blob/release-4.3/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["ecordell"]} -->
openshift-ci-robot: @benluddy: This pull request references [Bugzilla bug 1818851](https://bugzilla.redhat.com/show_bug.cgi?id=1818851), which is valid. The bug has been moved to the POST state. The bug has been updated to refer to the pull request using the external bug tracker.

<details><summary>6 validation(s) were run on this bug</summary>

* bug is open, matching expected state (open)
* bug target release (4.3.z) matches configured target release for branch (4.3.z)
* bug is in the state ASSIGNED, which is one of the valid states (NEW, ASSIGNED, ON_DEV, POST, POST)
* dependent bug [Bugzilla bug 1818850](https://bugzilla.redhat.com/show_bug.cgi?id=1818850) is in the state RELEASE_PENDING, which is one of the valid states (VERIFIED, RELEASE_PENDING, CLOSED (ERRATA))
* dependent [Bugzilla bug 1818850](https://bugzilla.redhat.com/show_bug.cgi?id=1818850) targets the "4.4.0" release, which is one of the valid target releases: 4.4.0, 4.4.z
* bug has dependents</details>

<details>

In response to [this](https://github.com/operator-framework/operator-lifecycle-manager/pull/1508):

>[release-4.3] Bug 1818851: feat(catalogs): add spec validation for sourcetypes


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
openshift-ci-robot: @benluddy: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ci/prow/verify | e3fdf01da0b33096bf93ce5cbe5cc0fcc46005f1 | [link](https://prow.svc.ci.openshift.org/view/gcs/origin-ci-test/pr-logs/pull/operator-framework_operator-lifecycle-manager/1508/pull-ci-operator-framework-operator-lifecycle-manager-release-4.3-verify/110) | `/test verify`

[Full PR test history](https://prow.svc.ci.openshift.org/pr-history?org=operator-framework&repo=operator-lifecycle-manager&pr=1508). [Your PR dashboard](https://prow.svc.ci.openshift.org/pr?query=is:pr+state:open+author:benluddy). Please help us cut down on flakes by [linking to](https://github.com/kubernetes/community/blob/master/contributors/devel/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/operator-framework/operator-lifecycle-manager/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

Patch:
diff --git a/deploy/chart/templates/0000_50_olm_06-catalogsource.crd.yaml b/deploy/chart/templates/0000_50_olm_06-catalogsource.crd.yaml
index 470739bb8..9213749f6 100644
--- a/deploy/chart/templates/0000_50_olm_06-catalogsource.crd.yaml
+++ b/deploy/chart/templates/0000_50_olm_06-catalogsource.crd.yaml
@@ -146,10 +146,10 @@ spec:
                   type: string
             message:
               description: A human readable message indicating details about why the
-                ClusterServiceVersion is in this condition.
+                CatalogSource is in this condition.
               type: string
             reason:
-              description: Reason is the reason the Subscription was transitioned
+              description: Reason is the reason the CatalogSource was transitioned
                 to its current state.
               type: string
             registryService:
diff --git a/pkg/api/apis/operators/v1alpha1/catalogsource_types.go b/pkg/api/apis/operators/v1alpha1/catalogsource_types.go
index bb1b86afe..0876308b1 100644
--- a/pkg/api/apis/operators/v1alpha1/catalogsource_types.go
+++ b/pkg/api/apis/operators/v1alpha1/catalogsource_types.go
@@ -28,7 +28,11 @@ const (
 )
 
 const (
-	CatalogSourceConfigMapError      ConditionReason = "ConfigMapError"
+	// CatalogSourceSpecInvalidError denotes when fields on the spec of the CatalogSource are not valid.
+	CatalogSourceSpecInvalidError ConditionReason = "SpecInvalidError"
+	// CatalogSourceConfigMapError denotes when there is an issue extracting manifests from the specified ConfigMap.
+	CatalogSourceConfigMapError ConditionReason = "ConfigMapError"
+	// CatalogSourceRegistryServerError denotes when there is an issue querying the specified registry server.
 	CatalogSourceRegistryServerError ConditionReason = "RegistryServerError"
 )
 
@@ -85,10 +89,10 @@ type GRPCConnectionState struct {
 }
 
 type CatalogSourceStatus struct {
-	// A human readable message indicating details about why the ClusterServiceVersion is in this condition.
+	// A human readable message indicating details about why the CatalogSource is in this condition.
 	// +optional
 	Message string `json:"message,omitempty"`
-	// Reason is the reason the Subscription was transitioned to its current state.
+	// Reason is the reason the CatalogSource was transitioned to its current state.
 	// +optional
 	Reason ConditionReason `json:"reason,omitempty"`
 
diff --git a/pkg/controller/operators/catalog/operator.go b/pkg/controller/operators/catalog/operator.go
index 8a4171f21..f333f1044 100644
--- a/pkg/controller/operators/catalog/operator.go
+++ b/pkg/controller/operators/catalog/operator.go
@@ -461,6 +461,35 @@ func (o *Operator) handleCatSrcDeletion(obj interface{}) {
 	o.logger.WithField("source", sourceKey).Info("removed client for deleted catalogsource")
 }
 
+func validateSourceType(logger *logrus.Entry, in *v1alpha1.CatalogSource) (out *v1alpha1.CatalogSource, continueSync bool, _ error) {
+	out = in
+	var err error
+	switch sourceType := out.Spec.SourceType; sourceType {
+	case v1alpha1.SourceTypeInternal, v1alpha1.SourceTypeConfigmap:
+		if out.Spec.ConfigMap == "" {
+			err = fmt.Errorf("configmap name unset: must be set for sourcetype: %s", sourceType)
+		}
+	case v1alpha1.SourceTypeGrpc:
+		if out.Spec.Image == "" && out.Spec.Address == "" {
+			err = fmt.Errorf("image and address unset: at least one must be set for sourcetype: %s", sourceType)
+		}
+	default:
+		err = fmt.Errorf("unknown sourcetype: %s", sourceType)
+	}
+	if err != nil {
+		out.SetError(v1alpha1.CatalogSourceSpecInvalidError, err)
+		return
+	}
+
+	// The sourceType is valid, clear (all) status if there's existing invalid spec reason
+	if out.Status.Reason == v1alpha1.CatalogSourceSpecInvalidError {
+		out.Status = v1alpha1.CatalogSourceStatus{}
+	}
+	continueSync = true
+
+	return
+}
+
 func (o *Operator) syncConfigMap(logger *logrus.Entry, in *v1alpha1.CatalogSource) (out *v1alpha1.CatalogSource, continueSync bool, syncError error) {
 	out = in
 	if !(in.Spec.SourceType == v1alpha1.SourceTypeInternal || in.Spec.SourceType == v1alpha1.SourceTypeConfigmap) {
@@ -678,6 +707,7 @@ func (o *Operator) syncCatalogSources(obj interface{}) (syncError error) {
 	}
 
 	chain := []CatalogSourceSyncFunc{
+		validateSourceType,
 		o.syncConfigMap,
 		o.syncRegistryServer,
 		o.syncConnection,
@@ -688,8 +718,7 @@ func (o *Operator) syncCatalogSources(obj interface{}) (syncError error) {
 
 	out, syncError := syncFunc(in, chain)
 
-	if equalFunc(&in.Status, &out.Status) {
-		logger.Debug("no change in status, skipping status update")
+	if equalFunc(&catsrc.Status, &out.Status) {
 		return
 	}
 
diff --git a/pkg/controller/operators/catalog/subscription/reconciler.go b/pkg/controller/operators/catalog/subscription/reconciler.go
index 28fff7d2c..d716e6dd0 100644
--- a/pkg/controller/operators/catalog/subscription/reconciler.go
+++ b/pkg/controller/operators/catalog/subscription/reconciler.go
@@ -182,6 +182,12 @@ func (c *catalogHealthReconciler) health(now *metav1.Time, catalog *v1alpha1.Cat
 // healthy returns true if the given catalog is healthy, false otherwise, and any error encountered
 // while checking the catalog's registry server.
 func (c *catalogHealthReconciler) healthy(catalog *v1alpha1.CatalogSource) (bool, error) {
+	if catalog.Status.Reason == v1alpha1.CatalogSourceSpecInvalidError {
+		// The catalog's spec is bad, mark unhealthy
+		return false, nil
+	}
+
+	// Check connection health
 	rec := c.registryReconcilerFactory.ReconcilerForSource(catalog)
 	if rec == nil {
 		return false, fmt.Errorf("could not get reconciler for catalog: %#v", catalog)


Test patch:
diff --git a/pkg/controller/operators/catalog/operator_test.go b/pkg/controller/operators/catalog/operator_test.go
index ca4c324aa..66228028e 100644
--- a/pkg/controller/operators/catalog/operator_test.go
+++ b/pkg/controller/operators/catalog/operator_test.go
@@ -565,23 +565,13 @@ func TestSyncCatalogSources(t *testing.T) {
 					UID:       types.UID("catalog-uid"),
 				},
 				Spec: v1alpha1.CatalogSourceSpec{
-					ConfigMap:  "cool-configmap",
 					SourceType: "nope",
 				},
 			},
-			k8sObjs: []runtime.Object{
-				&corev1.ConfigMap{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:            "cool-configmap",
-						Namespace:       "cool-namespace",
-						UID:             types.UID("configmap-uid"),
-						ResourceVersion: "resource-version",
-					},
-					Data: fakeConfigMapData(),
-				},
+			expectedStatus: &v1alpha1.CatalogSourceStatus{
+				Message: "unknown sourcetype: nope",
+				Reason:  v1alpha1.CatalogSourceSpecInvalidError,
 			},
-			expectedStatus: nil,
-			expectedError:  fmt.Errorf("no reconciler for source type nope"),
 		},
 		{
 			testName:      "CatalogSourceWithBackingConfigMap",
@@ -723,6 +713,66 @@ func TestSyncCatalogSources(t *testing.T) {
 				pod(*grpcCatalog),
 			},
 		},
+		{
+			testName:  "CatalogSourceWithGrpcType/EnsuresImageOrAddressIsSet",
+			namespace: "cool-namespace",
+			catalogSource: &v1alpha1.CatalogSource{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "invalid-spec-catalog",
+					Namespace: "cool-namespace",
+					UID:       types.UID("catalog-uid"),
+					Labels:    map[string]string{"olm.catalogSource": "invalid-spec-catalog"},
+				},
+				Spec: v1alpha1.CatalogSourceSpec{
+					SourceType: v1alpha1.SourceTypeGrpc,
+				},
+			},
+			expectedStatus: &v1alpha1.CatalogSourceStatus{
+				Message: fmt.Sprintf("image and address unset: at least one must be set for sourcetype: %s", v1alpha1.SourceTypeGrpc),
+				Reason:  v1alpha1.CatalogSourceSpecInvalidError,
+			},
+			expectedError: nil,
+		},
+		{
+			testName:  "CatalogSourceWithInternalType/EnsuresConfigMapIsSet",
+			namespace: "cool-namespace",
+			catalogSource: &v1alpha1.CatalogSource{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "invalid-spec-catalog",
+					Namespace: "cool-namespace",
+					UID:       types.UID("catalog-uid"),
+					Labels:    map[string]string{"olm.catalogSource": "invalid-spec-catalog"},
+				},
+				Spec: v1alpha1.CatalogSourceSpec{
+					SourceType: v1alpha1.SourceTypeInternal,
+				},
+			},
+			expectedStatus: &v1alpha1.CatalogSourceStatus{
+				Message: fmt.Sprintf("configmap name unset: must be set for sourcetype: %s", v1alpha1.SourceTypeInternal),
+				Reason:  v1alpha1.CatalogSourceSpecInvalidError,
+			},
+			expectedError: nil,
+		},
+		{
+			testName:  "CatalogSourceWithConfigMapType/EnsuresConfigMapIsSet",
+			namespace: "cool-namespace",
+			catalogSource: &v1alpha1.CatalogSource{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "invalid-spec-catalog",
+					Namespace: "cool-namespace",
+					UID:       types.UID("catalog-uid"),
+					Labels:    map[string]string{"olm.catalogSource": "invalid-spec-catalog"},
+				},
+				Spec: v1alpha1.CatalogSourceSpec{
+					SourceType: v1alpha1.SourceTypeConfigmap,
+				},
+			},
+			expectedStatus: &v1alpha1.CatalogSourceStatus{
+				Message: fmt.Sprintf("configmap name unset: must be set for sourcetype: %s", v1alpha1.SourceTypeConfigmap),
+				Reason:  v1alpha1.CatalogSourceSpecInvalidError,
+			},
+			expectedError: nil,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.testName, func(t *testing.T) {
diff --git a/pkg/controller/operators/catalog/subscription/reconciler_test.go b/pkg/controller/operators/catalog/subscription/reconciler_test.go
index 215d7356f..953e1f2d8 100644
--- a/pkg/controller/operators/catalog/subscription/reconciler_test.go
+++ b/pkg/controller/operators/catalog/subscription/reconciler_test.go
@@ -2,6 +2,7 @@ package subscription
 
 import (
 	"context"
+	"fmt"
 	"testing"
 	"time"
 
@@ -100,6 +101,76 @@ func TestCatalogHealthReconcile(t *testing.T) {
 				),
 			},
 		},
+		{
+			description: "ExistsToUnhealthy/InvalidCatalogSpec",
+			fields: fields{
+				config: &fakeReconcilerConfig{
+					now:                    nowFunc,
+					globalCatalogNamespace: "global",
+					existingObjs: existingObjs{
+						clientObjs: []runtime.Object{
+							func() *v1alpha1.CatalogSource {
+								cs := catalogSource("ns", "cs-0")
+								cs.Spec = v1alpha1.CatalogSourceSpec{
+									SourceType: v1alpha1.SourceTypeGrpc,
+								}
+								cs.Status = v1alpha1.CatalogSourceStatus{
+									Reason:  v1alpha1.CatalogSourceSpecInvalidError,
+									Message: fmt.Sprintf("image and address unset: at least one must be set for sourcetype: %s", v1alpha1.SourceTypeGrpc),
+								}
+
+								return cs
+							}(),
+							&v1alpha1.Subscription{
+								ObjectMeta: metav1.ObjectMeta{
+									Name:      "sub",
+									Namespace: "ns",
+								},
+								Spec: &v1alpha1.SubscriptionSpec{
+									CatalogSourceNamespace: "ns",
+									CatalogSource:          "cs-0",
+								},
+							},
+						},
+					},
+				},
+			},
+			args: args{
+				in: newSubscriptionExistsState(
+					&v1alpha1.Subscription{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      "sub",
+							Namespace: "ns",
+						},
+						Spec: &v1alpha1.SubscriptionSpec{
+							CatalogSourceNamespace: "ns",
+							CatalogSource:          "cs-0",
+						},
+					},
+				),
+			},
+			want: want{
+				out: newCatalogUnhealthyState(&v1alpha1.Subscription{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "sub",
+						Namespace: "ns",
+					},
+					Spec: &v1alpha1.SubscriptionSpec{
+						CatalogSourceNamespace: "ns",
+						CatalogSource:          "cs-0",
+					},
+					Status: v1alpha1.SubscriptionStatus{
+						CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{
+							catalogHealth("ns", "cs-0", &now, false),
+						},
+						Conditions: []v1alpha1.SubscriptionCondition{
+							catalogUnhealthyCondition(corev1.ConditionTrue, v1alpha1.CatalogSourcesAdded, "targeted catalogsource ns/cs-0 unhealthy", &now),
+						},
+						LastUpdated: now,
+					},
+				}),
+			},
+		},
 		{
 			description: "ExistsToUnhealthy/Catalogs/NoChanges",
 			fields: fields{
@@ -342,6 +413,94 @@ func TestCatalogHealthReconcile(t *testing.T) {
 				}),
 			},
 		},
+		{
+			description: "HealthyToUnhealthy/InvalidCatalogSpec",
+			fields: fields{
+				config: &fakeReconcilerConfig{
+					now:                    nowFunc,
+					globalCatalogNamespace: "global",
+					existingObjs: existingObjs{
+						clientObjs: []runtime.Object{
+							func() *v1alpha1.CatalogSource {
+								cs := catalogSource("ns", "cs-0")
+								cs.Spec = v1alpha1.CatalogSourceSpec{
+									SourceType: v1alpha1.SourceTypeGrpc,
+								}
+								cs.Status = v1alpha1.CatalogSourceStatus{
+									Reason:  v1alpha1.CatalogSourceSpecInvalidError,
+									Message: fmt.Sprintf("image and address unset: at least one must be set for sourcetype: %s", v1alpha1.SourceTypeGrpc),
+								}
+
+								return cs
+							}(),
+							&v1alpha1.Subscription{
+								ObjectMeta: metav1.ObjectMeta{
+									Name:      "sub",
+									Namespace: "ns",
+								},
+								Spec: &v1alpha1.SubscriptionSpec{
+									CatalogSourceNamespace: "ns",
+									CatalogSource:          "cs-0",
+								},
+								Status: v1alpha1.SubscriptionStatus{
+									CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{
+										catalogHealth("ns", "cs-0", &earlier, true),
+									},
+									Conditions: []v1alpha1.SubscriptionCondition{
+										catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier),
+									},
+									LastUpdated: earlier,
+								},
+							},
+						},
+					},
+				},
+			},
+			args: args{
+				in: newSubscriptionExistsState(
+					&v1alpha1.Subscription{
+						ObjectMeta: metav1.ObjectMeta{
+							Name:      "sub",
+							Namespace: "ns",
+						},
+						Spec: &v1alpha1.SubscriptionSpec{
+							CatalogSourceNamespace: "ns",
+							CatalogSource:          "cs-0",
+						},
+						Status: v1alpha1.SubscriptionStatus{
+							CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{
+								catalogHealth("ns", "cs-0", &earlier, true),
+							},
+							Conditions: []v1alpha1.SubscriptionCondition{
+								catalogUnhealthyCondition(corev1.ConditionFalse, v1alpha1.AllCatalogSourcesHealthy, "all available catalogsources are healthy", &earlier),
+							},
+							LastUpdated: earlier,
+						},
+					},
+				),
+			},
+			want: want{
+				out: newCatalogUnhealthyState(&v1alpha1.Subscription{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "sub",
+						Namespace: "ns",
+					},
+					Spec: &v1alpha1.SubscriptionSpec{
+						CatalogSourceNamespace: "ns",
+						CatalogSource:          "cs-0",
+					},
+					Status: v1alpha1.SubscriptionStatus{
+						CatalogHealth: []v1alpha1.SubscriptionCatalogHealth{
+							catalogHealth("ns", "cs-0", &now, false),
+						},
+						Conditions: []v1alpha1.SubscriptionCondition{
+							catalogUnhealthyCondition(corev1.ConditionTrue, v1alpha1.CatalogSourcesUpdated, "targeted catalogsource ns/cs-0 unhealthy", &now),
+						},
+						LastUpdated: now,
+					},
+				}),
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.description, func(t *testing.T) {
diff --git a/test/e2e/subscription_e2e_test.go b/test/e2e/subscription_e2e_test.go
index c3190fa71..e290dc1e4 100644
--- a/test/e2e/subscription_e2e_test.go
+++ b/test/e2e/subscription_e2e_test.go
@@ -11,14 +11,6 @@ import (
 	"github.com/blang/semver"
 	"github.com/ghodss/yaml"
 	configv1client "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"
-	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
-	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
-	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install"
-	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry"
-	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver"
-	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/comparison"
-	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
-	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/version"
 	"github.com/stretchr/testify/require"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -27,6 +19,15 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/tools/clientcmd"
+
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/install"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/comparison"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/operatorclient"
+	"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/version"
 )
 
 var doubleInstance = int32(2)