diff --git a/test/e2e/features/uninstall.feature b/test/e2e/features/uninstall.feature new file mode 100644 index 000000000..75374f667 --- /dev/null +++ b/test/e2e/features/uninstall.feature @@ -0,0 +1,66 @@ +Feature: Uninstall ClusterExtension + + As an OLM user I would like to uninstall a cluster extension. + + Background: + Given OLM is available + And ClusterCatalog "test" serves bundles + And ServiceAccount "olm-sa" with needed permissions is available in ${TEST_NAMESPACE} + And ClusterExtension is applied + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME} + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + source: + sourceType: Catalog + catalog: + packageName: test + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": test-catalog + """ + And bundle "test-operator.1.2.0" is installed in version "1.2.0" + # Ensure the bundle resources exist before initiating the deletion process and checking + # that they no longer exist to avoid false positives (i.e. if they never existed, checking that they don't exist + # will succeed) + And resource "networkpolicy/test-operator-network-policy" exists + And resource "configmap/test-configmap" exists + And resource "deployment/test-operator" exists + # Note: The names of these resources are derived from the permissions contained in the clusterroles + # If those permissions change due to changes in the bundle, the names of these resources will also change + # causing a failure here + And resource "clusterrole/testoperator.v1.2.-37mym6pni2xxmai9n7fmhtbn9i348lx7o619rmf3ypio" exists + And resource "clusterrole/testoperator.v1.2.0-t88i5epjh8oxp4klplhjyrsekwcp92b27w03ayr1ku5" exists + And resource "clusterrolebinding/testoperator.v1.2.-37mym6pni2xxmai9n7fmhtbn9i348lx7o619rmf3ypio" exists + And resource "clusterrolebinding/testoperator.v1.2.0-t88i5epjh8oxp4klplhjyrsekwcp92b27w03ayr1ku5" exists + + Scenario: Uninstall ClusterExtension + When resource "clusterextension/${NAME}" is removed + Then resource "networkpolicy/test-operator-network-policy" is eventually not found + And resource "configmap/test-configmap" is eventually not found + And resource "deployment/test-operator" is eventually not found + And resource "clusterrole/testoperator.v1.2.-37mym6pni2xxmai9n7fmhtbn9i348lx7o619rmf3ypio" is eventually not found + And resource "clusterrole/testoperator.v1.2.0-t88i5epjh8oxp4klplhjyrsekwcp92b27w03ayr1ku5" is eventually not found + And resource "clusterrolebinding/testoperator.v1.2.-37mym6pni2xxmai9n7fmhtbn9i348lx7o619rmf3ypio" is eventually not found + And resource "clusterrolebinding/testoperator.v1.2.0-t88i5epjh8oxp4klplhjyrsekwcp92b27w03ayr1ku5" is eventually not found + + Scenario: ClusterExtension resources are cleaned up even if the ServiceAccount is no longer present + # This scenario is especially important for GitOps where the ClusterExtension and related resources (namespace, + # service account, etc.) are deleted together + When resource "serviceaccount/olm-sa" is removed + # Ensure service account is gone before checking to ensure resources are cleaned up whether the service account + # and its permissions are present on the cluster or not + And resource "serviceaccount/olm-sa" is eventually not found + And resource "clusterextension/${NAME}" is removed + Then resource "networkpolicy/test-operator-network-policy" is eventually not found + And resource "configmap/test-configmap" is eventually not found + And resource "deployment/test-operator" is eventually not found + And resource "clusterrole/testoperator.v1.2.-37mym6pni2xxmai9n7fmhtbn9i348lx7o619rmf3ypio" is eventually not found + And resource "clusterrole/testoperator.v1.2.0-t88i5epjh8oxp4klplhjyrsekwcp92b27w03ayr1ku5" is eventually not found + And resource "clusterrolebinding/testoperator.v1.2.-37mym6pni2xxmai9n7fmhtbn9i348lx7o619rmf3ypio" is eventually not found + And resource "clusterrolebinding/testoperator.v1.2.0-t88i5epjh8oxp4klplhjyrsekwcp92b27w03ayr1ku5" is eventually not found diff --git a/test/e2e/steps/steps.go b/test/e2e/steps/steps.go index 9ae772ce8..52bba3be8 100644 --- a/test/e2e/steps/steps.go +++ b/test/e2e/steps/steps.go @@ -68,6 +68,7 @@ func RegisterSteps(sc *godog.ScenarioContext) { sc.Step(`^(?i)resource "([^"]+)" is installed$`, ResourceAvailable) sc.Step(`^(?i)resource "([^"]+)" is available$`, ResourceAvailable) sc.Step(`^(?i)resource "([^"]+)" is removed$`, ResourceRemoved) + sc.Step(`^(?i)resource "([^"]+)" is eventually not found$`, ResourceEventuallyNotFound) sc.Step(`^(?i)resource "([^"]+)" exists$`, ResourceAvailable) sc.Step(`^(?i)resource is applied$`, ResourceIsApplied) sc.Step(`^(?i)resource "deployment/test-operator" reports as (not ready|ready)$`, MarkTestOperatorNotReady) @@ -398,12 +399,12 @@ func ClusterExtensionRevisionIsArchived(ctx context.Context, revisionName string func ResourceAvailable(ctx context.Context, resource string) error { sc := scenarioCtx(ctx) resource = substituteScenarioVars(resource, sc) - rtype, name, found := strings.Cut(resource, "/") + kind, name, found := strings.Cut(resource, "/") if !found { - return fmt.Errorf("resource %s is not in the format /", resource) + return fmt.Errorf("resource %s is not in the format /", resource) } waitFor(ctx, func() bool { - _, err := k8sClient("get", rtype, name, "-n", sc.namespace) + _, err := k8sClient("get", kind, name, "-n", sc.namespace) return err == nil }) return nil @@ -411,11 +412,12 @@ func ResourceAvailable(ctx context.Context, resource string) error { func ResourceRemoved(ctx context.Context, resource string) error { sc := scenarioCtx(ctx) - rtype, name, found := strings.Cut(resource, "/") + resource = substituteScenarioVars(resource, sc) + kind, name, found := strings.Cut(resource, "/") if !found { - return fmt.Errorf("resource %s is not in the format /", resource) + return fmt.Errorf("resource %s is not in the format /", resource) } - yaml, err := k8sClient("get", rtype, name, "-n", sc.namespace, "-o", "yaml") + yaml, err := k8sClient("get", kind, name, "-n", sc.namespace, "-o", "yaml") if err != nil { return err } @@ -424,23 +426,38 @@ func ResourceRemoved(ctx context.Context, resource string) error { return err } sc.removedResources = append(sc.removedResources, *obj) - _, err = k8sClient("delete", rtype, name, "-n", sc.namespace) + _, err = k8sClient("delete", kind, name, "-n", sc.namespace) return err } +func ResourceEventuallyNotFound(ctx context.Context, resource string) error { + sc := scenarioCtx(ctx) + resource = substituteScenarioVars(resource, sc) + kind, name, found := strings.Cut(resource, "/") + if !found { + return fmt.Errorf("resource %s is not in the format /", resource) + } + + require.Eventually(godog.T(ctx), func() bool { + obj, err := k8sClient("get", kind, name, "-n", sc.namespace, "--ignore-not-found", "-o", "yaml") + return err == nil && obj == "" + }, timeout, tick) + return nil +} + func ResourceMatches(ctx context.Context, resource string, requiredContentTemplate *godog.DocString) error { sc := scenarioCtx(ctx) resource = substituteScenarioVars(resource, sc) - rtype, name, found := strings.Cut(resource, "/") + kind, name, found := strings.Cut(resource, "/") if !found { - return fmt.Errorf("resource %s is not in the format /", resource) + return fmt.Errorf("resource %s is not in the format /", resource) } requiredContent, err := toUnstructured(substituteScenarioVars(requiredContentTemplate.Content, sc)) if err != nil { return fmt.Errorf("failed to parse required resource yaml: %v", err) } waitFor(ctx, func() bool { - objJson, err := k8sClient("get", rtype, name, "-n", sc.namespace, "-o", "json") + objJson, err := k8sClient("get", kind, name, "-n", sc.namespace, "-o", "json") if err != nil { return false } @@ -468,12 +485,13 @@ func ResourceMatches(ctx context.Context, resource string, requiredContentTempla func ResourceRestored(ctx context.Context, resource string) error { sc := scenarioCtx(ctx) - rtype, name, found := strings.Cut(resource, "/") + resource = substituteScenarioVars(resource, sc) + kind, name, found := strings.Cut(resource, "/") if !found { - return fmt.Errorf("resource %s is not in the format /", resource) + return fmt.Errorf("resource %s is not in the format /", resource) } waitFor(ctx, func() bool { - yaml, err := k8sClient("get", rtype, name, "-n", sc.namespace, "-o", "yaml") + yaml, err := k8sClient("get", kind, name, "-n", sc.namespace, "-o", "yaml") if err != nil { return false } @@ -486,7 +504,7 @@ func ResourceRestored(ctx context.Context, resource string) error { for i, removed := range sc.removedResources { rct := removed.GetCreationTimestamp() if removed.GetName() == obj.GetName() && removed.GetKind() == obj.GetKind() && rct.Before(&ct) { - switch rtype { + switch kind { case "configmap": if !reflect.DeepEqual(removed.Object["data"], obj.Object["data"]) { return false