diff --git a/api/v1/clusterextension_types.go b/api/v1/clusterextension_types.go index 343e8cbb4..6de62b0e1 100644 --- a/api/v1/clusterextension_types.go +++ b/api/v1/clusterextension_types.go @@ -106,6 +106,7 @@ type ClusterExtensionSpec struct { // a configuration schema the final manifests will be derived on a best-effort basis. More information on how // to configure the bundle should be found in its end-user documentation. // + // // +optional Config *ClusterExtensionConfig `json:"config,omitempty"` } diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md index c0da40c4b..b21e40452 100644 --- a/docs/api-reference/olmv1-api-reference.md +++ b/docs/api-reference/olmv1-api-reference.md @@ -343,7 +343,7 @@ _Appears in:_ | `serviceAccount` _[ServiceAccountReference](#serviceaccountreference)_ | serviceAccount is a reference to a ServiceAccount used to perform all interactions
with the cluster that are required to manage the extension.
The ServiceAccount must be configured with the necessary permissions to perform these interactions.
The ServiceAccount must exist in the namespace referenced in the spec.
serviceAccount is required. | | Required: \{\}
| | `source` _[SourceConfig](#sourceconfig)_ | source is a required field which selects the installation source of content
for this ClusterExtension. Selection is performed by setting the sourceType.

Catalog is currently the only implemented sourceType, and setting the
sourcetype to "Catalog" requires the catalog field to also be defined.

Below is a minimal example of a source definition (in yaml):

source:
sourceType: Catalog
catalog:
packageName: example-package | | Required: \{\}
| | `install` _[ClusterExtensionInstallConfig](#clusterextensioninstallconfig)_ | install is an optional field used to configure the installation options
for the ClusterExtension such as the pre-flight check configuration. | | | -| `config` _[ClusterExtensionConfig](#clusterextensionconfig)_ | config is an optional field used to specify bundle specific configuration
used to configure the bundle. Configuration is bundle specific and a bundle may provide
a configuration schema. When not specified, the default configuration of the resolved bundle will be used.

config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide
a configuration schema the final manifests will be derived on a best-effort basis. More information on how
to configure the bundle should be found in its end-user documentation. | | | +| `config` _[ClusterExtensionConfig](#clusterextensionconfig)_ | config is an optional field used to specify bundle specific configuration
used to configure the bundle. Configuration is bundle specific and a bundle may provide
a configuration schema. When not specified, the default configuration of the resolved bundle will be used.

config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide
a configuration schema the final manifests will be derived on a best-effort basis. More information on how
to configure the bundle should be found in its end-user documentation.

| | | #### ClusterExtensionStatus diff --git a/docs/draft/howto/single-ownnamespace-install.md b/docs/draft/howto/single-ownnamespace-install.md index 415294687..8e6f00fe4 100644 --- a/docs/draft/howto/single-ownnamespace-install.md +++ b/docs/draft/howto/single-ownnamespace-install.md @@ -1,7 +1,8 @@ ## Description !!! note -The `SingleOwnNamespaceInstallSupport` feature-gate is enabled by default. Use this guide to configure bundles that need Single or Own namespace install modes. +This feature is still in *alpha* the `SingleOwnNamespaceInstallSupport` feature-gate must be enabled to make use of it. +See the instructions below on how to enable it. --- @@ -30,6 +31,28 @@ include *installModes*. [![OwnNamespace Install Demo](https://asciinema.org/a/Rxx6WUwAU016bXFDW74XLcM5i.svg)](https://asciinema.org/a/Rxx6WUwAU016bXFDW74XLcM5i) +## Enabling the Feature-Gate + +!!! tip + +This guide assumes OLMv1 is already installed. If that is not the case, +you can follow the [getting started](../../getting-started/olmv1_getting_started.md) guide to install OLMv1. + +--- + +Patch the `operator-controller` `Deployment` adding `--feature-gates=SingleOwnNamespaceInstallSupport=true` to the +controller container arguments: + +```terminal title="Enable SingleOwnNamespaceInstallSupport feature-gate" +kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]' +``` + +Wait for `Deployment` rollout: + +```terminal title="Wait for Deployment rollout" +kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager +``` + ## Configuring the `ClusterExtension` A `ClusterExtension` can be configured to install bundle in `Single-` or `OwnNamespace` mode through the diff --git a/docs/draft/tutorials/explore-available-content-metas-endpoint.md b/docs/draft/tutorials/explore-available-content-metas-endpoint.md index 5d04b02df..f17271d3e 100644 --- a/docs/draft/tutorials/explore-available-content-metas-endpoint.md +++ b/docs/draft/tutorials/explore-available-content-metas-endpoint.md @@ -91,6 +91,9 @@ Then you can query the catalog by using `curl` commands and the `jq` CLI tool to ... ``` + !!! important + OLM 1.0 supports installing extensions that define webhooks. Targeting a single or specified set of namespaces requires enabling the `SingleOwnNamespaceInstallSupport` feature-gate. + 3. Return list of packages which support `AllNamespaces` install mode, do not use webhooks, and where the channel head version uses `olm.csv.metadata` format: ``` terminal diff --git a/docs/project/olmv1_limitations.md b/docs/project/olmv1_limitations.md index 54e174b4c..01ce9436d 100644 --- a/docs/project/olmv1_limitations.md +++ b/docs/project/olmv1_limitations.md @@ -8,7 +8,7 @@ hide: Currently, OLM v1 only supports installing operators packaged in [OLM v0 bundles](https://olm.operatorframework.io/docs/tasks/creating-operator-bundle/) , also known as `registry+v1` bundles. Additionally, the bundled operator, or cluster extension: -* **must** support installation via the `AllNamespaces`, `SingleNamespace`, or `OwnNamespace` install modes. +* **must** support installation via the `AllNamespaces` install mode * **must not** declare dependencies using any of the following file-based catalog properties: * `olm.gvk.required` * `olm.package.required` diff --git a/docs/tutorials/explore-available-content.md b/docs/tutorials/explore-available-content.md index 98bb7733c..36e3cf883 100644 --- a/docs/tutorials/explore-available-content.md +++ b/docs/tutorials/explore-available-content.md @@ -91,6 +91,9 @@ Then you can query the catalog by using `curl` commands and the `jq` CLI tool to ... ``` + !!! important + OLM 1.0 supports installing extensions that define webhooks. Targeting a single or specified set of namespaces requires enabling the `SingleOwnNamespaceInstallSupport` feature-gate. + 3. Return list of packages that support `AllNamespaces` install mode and do not use webhooks: ``` terminal diff --git a/hack/demo/own-namespace-demo-script.sh b/hack/demo/own-namespace-demo-script.sh index 86b3d2876..611c6dfb0 100755 --- a/hack/demo/own-namespace-demo-script.sh +++ b/hack/demo/own-namespace-demo-script.sh @@ -6,14 +6,16 @@ set -e trap 'echo "Demo ran into error"; trap - SIGTERM && kill -- -$$; exit 1' ERR SIGINT SIGTERM EXIT -# install standard CRDs -echo "Install standard CRDs..." -kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/standard.yaml" +# install experimental CRDs with config field support +kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/experimental.yaml" -# wait for standard CRDs to be available +# wait for experimental CRDs to be available kubectl wait --for condition=established --timeout=60s crd/clusterextensions.olm.operatorframework.io -# Ensure controller is healthy +# enable 'SingleOwnNamespaceInstallSupport' feature gate +kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]' + +# wait for operator-controller to become available kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager # create install namespace @@ -55,6 +57,17 @@ kubectl delete clusterextension argocd-operator --ignore-not-found=true kubectl delete namespace argocd-system --ignore-not-found=true kubectl delete clusterrolebinding argocd-installer-crb --ignore-not-found=true +# remove feature gate from deployment +echo "Removing feature gate from operator-controller..." +kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "remove", "path": "/spec/template/spec/containers/0/args", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]' || true + +# restore standard CRDs +echo "Restoring standard CRDs..." +kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/base.yaml" + +# wait for standard CRDs to be available +kubectl wait --for condition=established --timeout=60s crd/clusterextensions.olm.operatorframework.io + # wait for operator-controller to become available with standard config kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager diff --git a/hack/demo/single-namespace-demo-script.sh b/hack/demo/single-namespace-demo-script.sh index 885854dd9..970268415 100755 --- a/hack/demo/single-namespace-demo-script.sh +++ b/hack/demo/single-namespace-demo-script.sh @@ -6,14 +6,16 @@ set -e trap 'echo "Demo ran into error"; trap - SIGTERM && kill -- -$$; exit 1' ERR SIGINT SIGTERM EXIT -# install standard CRDs -echo "Install standard CRDs..." -kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/standard.yaml" +# install experimental CRDs with config field support +kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/experimental.yaml" -# wait for standard CRDs to be available +# wait for experimental CRDs to be available kubectl wait --for condition=established --timeout=60s crd/clusterextensions.olm.operatorframework.io -# Ensure controller is healthy +# enable 'SingleOwnNamespaceInstallSupport' feature gate +kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]' + +# wait for operator-controller to become available kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager # create install namespace @@ -58,6 +60,17 @@ kubectl delete clusterextension argocd-operator --ignore-not-found=true kubectl delete namespace argocd-system argocd --ignore-not-found=true kubectl delete clusterrolebinding argocd-installer-crb --ignore-not-found=true +# remove feature gate from deployment +echo "Removing feature gate from operator-controller..." +kubectl patch deployment -n olmv1-system operator-controller-controller-manager --type='json' -p='[{"op": "remove", "path": "/spec/template/spec/containers/0/args", "value": "--feature-gates=SingleOwnNamespaceInstallSupport=true"}]' || true + +# restore standard CRDs +echo "Restoring standard CRDs..." +kubectl apply -f "$(dirname "${BASH_SOURCE[0]}")/../../manifests/base.yaml" + +# wait for standard CRDs to be available +kubectl wait --for condition=established --timeout=60s crd/clusterextensions.olm.operatorframework.io + # wait for operator-controller to become available with standard config kubectl rollout status -n olmv1-system deployment/operator-controller-controller-manager diff --git a/helm/experimental.yaml b/helm/experimental.yaml index 1d2761714..b14b1b303 100644 --- a/helm/experimental.yaml +++ b/helm/experimental.yaml @@ -9,6 +9,7 @@ options: operatorController: features: enabled: + - SingleOwnNamespaceInstallSupport - PreflightPermissions - HelmChartSupport - BoxcutterRuntime diff --git a/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml b/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml index 61337bad6..a0983e41f 100644 --- a/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml +++ b/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml @@ -57,45 +57,6 @@ spec: description: spec is an optional field that defines the desired state of the ClusterExtension. properties: - config: - description: |- - config is an optional field used to specify bundle specific configuration - used to configure the bundle. Configuration is bundle specific and a bundle may provide - a configuration schema. When not specified, the default configuration of the resolved bundle will be used. - - config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide - a configuration schema the final manifests will be derived on a best-effort basis. More information on how - to configure the bundle should be found in its end-user documentation. - properties: - configType: - description: |- - configType is a required reference to the type of configuration source. - - Allowed values are "Inline" - - When this field is set to "Inline", the cluster extension configuration is defined inline within the - ClusterExtension resource. - enum: - - Inline - type: string - inline: - description: |- - inline contains JSON or YAML values specified directly in the - ClusterExtension. - - inline must be set if configType is 'Inline'. - inline accepts arbitrary JSON/YAML objects. - inline is validation at runtime against the schema provided by the bundle if a schema is provided. - type: object - x-kubernetes-preserve-unknown-fields: true - required: - - configType - type: object - x-kubernetes-validations: - - message: inline is required when configType is Inline, and forbidden - otherwise - rule: 'has(self.configType) && self.configType == ''Inline'' ?has(self.inline) - : !has(self.inline)' install: description: |- install is an optional field used to configure the installation options diff --git a/helm/tilt.yaml b/helm/tilt.yaml index 0fe3bec1f..aaed7c71f 100644 --- a/helm/tilt.yaml +++ b/helm/tilt.yaml @@ -14,6 +14,7 @@ options: operatorController: features: enabled: + - SingleOwnNamespaceInstallSupport - PreflightPermissions - HelmChartSupport disabled: diff --git a/internal/operator-controller/features/features.go b/internal/operator-controller/features/features.go index 87e827c66..4926ff853 100644 --- a/internal/operator-controller/features/features.go +++ b/internal/operator-controller/features/features.go @@ -33,8 +33,8 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature // registry+v1 cluster extensions with single or own namespaces modes // i.e. with a single watch namespace. SingleOwnNamespaceInstallSupport: { - Default: true, - PreRelease: featuregate.GA, + Default: false, + PreRelease: featuregate.Alpha, LockToDefault: false, }, diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml index 5b09dc949..1efa8b8d9 100644 --- a/manifests/experimental-e2e.yaml +++ b/manifests/experimental-e2e.yaml @@ -2188,6 +2188,7 @@ spec: - --health-probe-bind-address=:8081 - --metrics-bind-address=:8443 - --leader-elect + - --feature-gates=SingleOwnNamespaceInstallSupport=true - --feature-gates=PreflightPermissions=true - --feature-gates=HelmChartSupport=true - --feature-gates=BoxcutterRuntime=true diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml index 4ddd45077..664f8599c 100644 --- a/manifests/experimental.yaml +++ b/manifests/experimental.yaml @@ -2101,6 +2101,7 @@ spec: - --health-probe-bind-address=:8081 - --metrics-bind-address=:8443 - --leader-elect + - --feature-gates=SingleOwnNamespaceInstallSupport=true - --feature-gates=PreflightPermissions=true - --feature-gates=HelmChartSupport=true - --feature-gates=BoxcutterRuntime=true diff --git a/manifests/standard-e2e.yaml b/manifests/standard-e2e.yaml index 9c7a20f57..783beec51 100644 --- a/manifests/standard-e2e.yaml +++ b/manifests/standard-e2e.yaml @@ -648,45 +648,6 @@ spec: description: spec is an optional field that defines the desired state of the ClusterExtension. properties: - config: - description: |- - config is an optional field used to specify bundle specific configuration - used to configure the bundle. Configuration is bundle specific and a bundle may provide - a configuration schema. When not specified, the default configuration of the resolved bundle will be used. - - config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide - a configuration schema the final manifests will be derived on a best-effort basis. More information on how - to configure the bundle should be found in its end-user documentation. - properties: - configType: - description: |- - configType is a required reference to the type of configuration source. - - Allowed values are "Inline" - - When this field is set to "Inline", the cluster extension configuration is defined inline within the - ClusterExtension resource. - enum: - - Inline - type: string - inline: - description: |- - inline contains JSON or YAML values specified directly in the - ClusterExtension. - - inline must be set if configType is 'Inline'. - inline accepts arbitrary JSON/YAML objects. - inline is validation at runtime against the schema provided by the bundle if a schema is provided. - type: object - x-kubernetes-preserve-unknown-fields: true - required: - - configType - type: object - x-kubernetes-validations: - - message: inline is required when configType is Inline, and forbidden - otherwise - rule: 'has(self.configType) && self.configType == ''Inline'' ?has(self.inline) - : !has(self.inline)' install: description: |- install is an optional field used to configure the installation options diff --git a/manifests/standard.yaml b/manifests/standard.yaml index cf7eb0ee5..95e400c26 100644 --- a/manifests/standard.yaml +++ b/manifests/standard.yaml @@ -613,45 +613,6 @@ spec: description: spec is an optional field that defines the desired state of the ClusterExtension. properties: - config: - description: |- - config is an optional field used to specify bundle specific configuration - used to configure the bundle. Configuration is bundle specific and a bundle may provide - a configuration schema. When not specified, the default configuration of the resolved bundle will be used. - - config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide - a configuration schema the final manifests will be derived on a best-effort basis. More information on how - to configure the bundle should be found in its end-user documentation. - properties: - configType: - description: |- - configType is a required reference to the type of configuration source. - - Allowed values are "Inline" - - When this field is set to "Inline", the cluster extension configuration is defined inline within the - ClusterExtension resource. - enum: - - Inline - type: string - inline: - description: |- - inline contains JSON or YAML values specified directly in the - ClusterExtension. - - inline must be set if configType is 'Inline'. - inline accepts arbitrary JSON/YAML objects. - inline is validation at runtime against the schema provided by the bundle if a schema is provided. - type: object - x-kubernetes-preserve-unknown-fields: true - required: - - configType - type: object - x-kubernetes-validations: - - message: inline is required when configType is Inline, and forbidden - otherwise - rule: 'has(self.configType) && self.configType == ''Inline'' ?has(self.inline) - : !has(self.inline)' install: description: |- install is an optional field used to configure the installation options diff --git a/test/experimental-e2e/boxcutter_support_test.go b/test/experimental-e2e/boxcutter_support_test.go deleted file mode 100644 index b09d19ec8..000000000 --- a/test/experimental-e2e/boxcutter_support_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package experimental_e2e - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - apimeta "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - - ocv1 "github.com/operator-framework/operator-controller/api/v1" - utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils" - . "github.com/operator-framework/operator-controller/test/helpers" -) - -func TestClusterExtensionVersionUpdate(t *testing.T) { - t.Log("When a cluster extension is installed from a catalog") - t.Log("When resolving upgrade edges") - - clusterExtension, extensionCatalog, sa, ns := TestInit(t) - defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) - defer utils.CollectTestArtifacts(t, artifactName, c, cfg) - - t.Log("By creating an ClusterExtension at a specified version") - clusterExtension.Spec = ocv1.ClusterExtensionSpec{ - Source: ocv1.SourceConfig{ - SourceType: "Catalog", - Catalog: &ocv1.CatalogFilter{ - PackageName: "test", - Version: "1.0.0", - }, - }, - Namespace: ns.Name, - ServiceAccount: ocv1.ServiceAccountReference{ - Name: sa.Name, - }, - } - require.NoError(t, c.Create(context.Background(), clusterExtension)) - t.Log("By eventually reporting a successful resolution") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) - cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionTrue, cond.Status) - require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - }, pollDuration, pollInterval) - - t.Log("It allows to upgrade the ClusterExtension to a non-successor version") - t.Log("By forcing update of ClusterExtension resource to a non-successor version") - // 1.2.0 does not replace/skip/skipRange 1.0.0. - clusterExtension.Spec.Source.Catalog.Version = "1.2.0" - clusterExtension.Spec.Source.Catalog.UpgradeConstraintPolicy = ocv1.UpgradeConstraintPolicySelfCertified - require.NoError(t, c.Update(context.Background(), clusterExtension)) - t.Log("By eventually reporting a satisfiable resolution") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) - cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) - require.NotNil(ct, cond) - require.Equal(ct, metav1.ConditionTrue, cond.Status) - require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) - }, pollDuration, pollInterval) - t.Log("We should have two ClusterExtensionRevision resources") - require.EventuallyWithT(t, func(ct *assert.CollectT) { - cerList := &ocv1.ClusterExtensionRevisionList{} - require.NoError(ct, c.List(context.Background(), cerList)) - require.Len(ct, cerList.Items, 2) - }, pollDuration, pollInterval) -} diff --git a/test/experimental-e2e/experimental_test.go b/test/experimental-e2e/experimental_test.go deleted file mode 100644 index de329b588..000000000 --- a/test/experimental-e2e/experimental_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package experimental_e2e - -import ( - "os" - "testing" - "time" - - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/operator-framework/operator-controller/internal/operator-controller/scheme" - utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils" -) - -const ( - artifactName = "operator-controller-experimental-e2e" - pollDuration = time.Minute - pollInterval = time.Second -) - -var ( - cfg *rest.Config - c client.Client -) - -func TestMain(m *testing.M) { - cfg = ctrl.GetConfigOrDie() - - var err error - utilruntime.Must(apiextensionsv1.AddToScheme(scheme.Scheme)) - c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - utilruntime.Must(err) - - os.Exit(m.Run()) -} - -func TestNoop(t *testing.T) { - t.Log("Running experimental-e2e tests") - defer utils.CollectTestArtifacts(t, artifactName, c, cfg) -} diff --git a/test/e2e/single_namespace_support_test.go b/test/experimental-e2e/single_namespace_support_test.go similarity index 80% rename from test/e2e/single_namespace_support_test.go rename to test/experimental-e2e/single_namespace_support_test.go index 86ec78220..77a0cba42 100644 --- a/test/e2e/single_namespace_support_test.go +++ b/test/experimental-e2e/single_namespace_support_test.go @@ -1,10 +1,11 @@ -package e2e +package experimental_e2e import ( "context" "fmt" "os" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -15,12 +16,45 @@ import ( apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/rest" "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/scheme" utils "github.com/operator-framework/operator-controller/internal/shared/util/testutils" + . "github.com/operator-framework/operator-controller/test/helpers" ) +const ( + artifactName = "operator-controller-experimental-e2e" + pollDuration = time.Minute + pollInterval = time.Second +) + +var ( + cfg *rest.Config + c client.Client +) + +func TestMain(m *testing.M) { + cfg = ctrl.GetConfigOrDie() + + var err error + utilruntime.Must(apiextensionsv1.AddToScheme(scheme.Scheme)) + c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + utilruntime.Must(err) + + os.Exit(m.Run()) +} + +func TestNoop(t *testing.T) { + t.Log("Running experimental-e2e tests") + defer utils.CollectTestArtifacts(t, artifactName, c, cfg) +} + func TestClusterExtensionSingleNamespaceSupport(t *testing.T) { t.Log("Test support for cluster extension config") defer utils.CollectTestArtifacts(t, artifactName, c, cfg) @@ -346,3 +380,57 @@ func TestClusterExtensionOwnNamespaceSupport(t *testing.T) { require.Equal(ct, clusterExtension.Spec.Namespace, deployment.Spec.Template.GetAnnotations()["olm.targetNamespaces"]) }, pollDuration, pollInterval) } + +func TestClusterExtensionVersionUpdate(t *testing.T) { + t.Log("When a cluster extension is installed from a catalog") + t.Log("When resolving upgrade edges") + + clusterExtension, extensionCatalog, sa, ns := TestInit(t) + defer TestCleanup(t, extensionCatalog, clusterExtension, sa, ns) + defer utils.CollectTestArtifacts(t, artifactName, c, cfg) + + t.Log("By creating an ClusterExtension at a specified version") + clusterExtension.Spec = ocv1.ClusterExtensionSpec{ + Source: ocv1.SourceConfig{ + SourceType: "Catalog", + Catalog: &ocv1.CatalogFilter{ + PackageName: "test", + Version: "1.0.0", + }, + }, + Namespace: ns.Name, + ServiceAccount: ocv1.ServiceAccountReference{ + Name: sa.Name, + }, + } + require.NoError(t, c.Create(context.Background(), clusterExtension)) + t.Log("By eventually reporting a successful resolution") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + }, pollDuration, pollInterval) + + t.Log("It allows to upgrade the ClusterExtension to a non-successor version") + t.Log("By forcing update of ClusterExtension resource to a non-successor version") + // 1.2.0 does not replace/skip/skipRange 1.0.0. + clusterExtension.Spec.Source.Catalog.Version = "1.2.0" + clusterExtension.Spec.Source.Catalog.UpgradeConstraintPolicy = ocv1.UpgradeConstraintPolicySelfCertified + require.NoError(t, c.Update(context.Background(), clusterExtension)) + t.Log("By eventually reporting a satisfiable resolution") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + require.NoError(ct, c.Get(context.Background(), types.NamespacedName{Name: clusterExtension.Name}, clusterExtension)) + cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1.TypeProgressing) + require.NotNil(ct, cond) + require.Equal(ct, metav1.ConditionTrue, cond.Status) + require.Equal(ct, ocv1.ReasonSucceeded, cond.Reason) + }, pollDuration, pollInterval) + t.Log("We should have two ClusterExtensionRevision resources") + require.EventuallyWithT(t, func(ct *assert.CollectT) { + cerList := &ocv1.ClusterExtensionRevisionList{} + require.NoError(ct, c.List(context.Background(), cerList)) + require.Len(ct, cerList.Items, 2) + }, pollDuration, pollInterval) +}