From b7984ba3394f6fd1e85375bb9ada89d8b7197a06 Mon Sep 17 00:00:00 2001 From: Nico Schieder Date: Thu, 6 Mar 2025 15:17:37 +0100 Subject: [PATCH] Add Configuration interface Implements RFC "Arbitrary Configuration". --- Makefile | 2 +- api/v1/bundleconfig_types.go | 27 ++++++++++ api/v1/clusterextension_types.go | 7 +++ api/v1/zz_generated.deepcopy.go | 50 ++++++++++++++++++- ...peratorframework.io_clusterextensions.yaml | 8 +++ .../operator-controller/applier/helm_test.go | 9 +++- .../applier/watchnamespace.go | 28 ++++++++--- .../applier/watchnamespace_test.go | 39 +++++++++++---- 8 files changed, 149 insertions(+), 21 deletions(-) create mode 100644 api/v1/bundleconfig_types.go diff --git a/Makefile b/Makefile index c407ade2d..df3d4d964 100644 --- a/Makefile +++ b/Makefile @@ -139,7 +139,7 @@ manifests: $(CONTROLLER_GEN) #EXHELP Generate WebhookConfiguration, ClusterRole, $(CONTROLLER_GEN) crd paths="./api/v1/..." output:crd:artifacts:config=$(CRD_WORKING_DIR) mv $(CRD_WORKING_DIR)/olm.operatorframework.io_clusterextensions.yaml $(KUSTOMIZE_OPCON_CRDS_DIR) mv $(CRD_WORKING_DIR)/olm.operatorframework.io_clustercatalogs.yaml $(KUSTOMIZE_CATD_CRDS_DIR) - rmdir $(CRD_WORKING_DIR) + rm -rf $(CRD_WORKING_DIR) # Generate the remaining operator-controller manifests $(CONTROLLER_GEN) rbac:roleName=manager-role paths="./internal/operator-controller/..." output:rbac:artifacts:config=$(KUSTOMIZE_OPCON_RBAC_DIR) # Generate the remaining catalogd manifests diff --git a/api/v1/bundleconfig_types.go b/api/v1/bundleconfig_types.go new file mode 100644 index 000000000..1b5cca340 --- /dev/null +++ b/api/v1/bundleconfig_types.go @@ -0,0 +1,27 @@ +package v1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// BundleConfig ... +// +kubebuilder:object:root=true +type BundleConfig struct { + metav1.TypeMeta `json:",inline"` + // metadata is the standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ObjectMeta `json:"metadata"` + + // spec is the desired state of the BundleConfig. + // spec is required. + Spec BundleConfigSpec `json:"spec"` +} + +type BundleConfigSpec struct { + // watchNamespace configures what Namespace the Operator should watch. + // setting it to the same value as the install namespace corresponds to OwnNamespace mode, + // setting it to a different value corresponds to SingleNamespace mode. + WatchNamespace string `json:"watchNamespace,omitempty"` +} + +func init() { + SchemeBuilder.Register(&BundleConfig{}) +} diff --git a/api/v1/clusterextension_types.go b/api/v1/clusterextension_types.go index 0141f1a7a..78d6a64ed 100644 --- a/api/v1/clusterextension_types.go +++ b/api/v1/clusterextension_types.go @@ -18,6 +18,7 @@ package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" ) var ClusterExtensionKind = "ClusterExtension" @@ -92,6 +93,12 @@ type ClusterExtensionSpec struct { // // +optional Install *ClusterExtensionInstallConfig `json:"install,omitempty"` + + // config can be used to pass parameters to the underlying runtime. + // +optional + // +kubebuilder:validation:items:XEmbeddedResource + // +kubebuilder:validation:items:XPreserveUnknownFields + Config []runtime.RawExtension `json:"config,omitempty"` } const SourceTypeCatalog = "Catalog" diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 37694f61f..86b16a1aa 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -22,9 +22,50 @@ package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BundleConfig) DeepCopyInto(out *BundleConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleConfig. +func (in *BundleConfig) DeepCopy() *BundleConfig { + if in == nil { + return nil + } + out := new(BundleConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BundleConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BundleConfigSpec) DeepCopyInto(out *BundleConfigSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleConfigSpec. +func (in *BundleConfigSpec) DeepCopy() *BundleConfigSpec { + if in == nil { + return nil + } + out := new(BundleConfigSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BundleMetadata) DeepCopyInto(out *BundleMetadata) { *out = *in @@ -331,6 +372,13 @@ func (in *ClusterExtensionSpec) DeepCopyInto(out *ClusterExtensionSpec) { *out = new(ClusterExtensionInstallConfig) (*in).DeepCopyInto(*out) } + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = make([]runtime.RawExtension, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterExtensionSpec. diff --git a/config/base/operator-controller/crd/bases/olm.operatorframework.io_clusterextensions.yaml b/config/base/operator-controller/crd/bases/olm.operatorframework.io_clusterextensions.yaml index e54b68518..866232e82 100644 --- a/config/base/operator-controller/crd/bases/olm.operatorframework.io_clusterextensions.yaml +++ b/config/base/operator-controller/crd/bases/olm.operatorframework.io_clusterextensions.yaml @@ -56,6 +56,14 @@ spec: description: spec is an optional field that defines the desired state of the ClusterExtension. properties: + config: + description: config can be used to pass parameters to the underlying + runtime. + items: + type: object + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true + type: array install: description: |- install is an optional field used to configure the installation options diff --git a/internal/operator-controller/applier/helm_test.go b/internal/operator-controller/applier/helm_test.go index faaa41783..38feaee17 100644 --- a/internal/operator-controller/applier/helm_test.go +++ b/internal/operator-controller/applier/helm_test.go @@ -16,6 +16,7 @@ import ( "helm.sh/helm/v3/pkg/storage/driver" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" featuregatetesting "k8s.io/component-base/featuregate/testing" "sigs.k8s.io/controller-runtime/pkg/client" @@ -467,8 +468,12 @@ func TestApply_InstallationWithSingleOwnNamespaceInstallSupportEnabled(t *testin testExt := &ocv1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ Name: "testExt", - Annotations: map[string]string{ - applier.AnnotationClusterExtensionWatchNamespace: expectedWatchNamespace, + }, + Spec: ocv1.ClusterExtensionSpec{ + Config: []runtime.RawExtension{ + {Raw: []byte( + `{"apiVersion":"olm.operatorframework.io/v1","kind":"BundleConfig",` + + `"spec":{"watchNamespace":"` + expectedWatchNamespace + `"}}`)}, }, }, } diff --git a/internal/operator-controller/applier/watchnamespace.go b/internal/operator-controller/applier/watchnamespace.go index 193f456b3..7763481ff 100644 --- a/internal/operator-controller/applier/watchnamespace.go +++ b/internal/operator-controller/applier/watchnamespace.go @@ -1,6 +1,7 @@ package applier import ( + "encoding/json" "fmt" corev1 "k8s.io/api/core/v1" @@ -19,14 +20,27 @@ const ( // for registry+v1 bundles. This will go away once the ClusterExtension API is updated to include // (opaque) runtime configuration. func GetWatchNamespace(ext *ocv1.ClusterExtension) (string, error) { - if features.OperatorControllerFeatureGate.Enabled(features.SingleOwnNamespaceInstallSupport) { - if ext != nil && ext.Annotations[AnnotationClusterExtensionWatchNamespace] != "" { - watchNamespace := ext.Annotations[AnnotationClusterExtensionWatchNamespace] - if errs := validation.IsDNS1123Subdomain(watchNamespace); len(errs) > 0 { - return "", fmt.Errorf("invalid watch namespace '%s': namespace must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character", watchNamespace) - } - return ext.Annotations[AnnotationClusterExtensionWatchNamespace], nil + if !features.OperatorControllerFeatureGate.Enabled(features.SingleOwnNamespaceInstallSupport) || + ext == nil { + return corev1.NamespaceAll, nil + } + + for _, c := range ext.Spec.Config { + maybeConfig := &ocv1.BundleConfig{} + if err := json.Unmarshal(c.Raw, maybeConfig); err != nil { + return "", err + } + + if maybeConfig.GroupVersionKind() != ocv1.GroupVersion.WithKind("BundleConfig") { + continue } + + watchNamespace := maybeConfig.Spec.WatchNamespace + if errs := validation.IsDNS1123Subdomain(watchNamespace); len(errs) > 0 { + return "", fmt.Errorf("invalid watch namespace '%s': namespace must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character", watchNamespace) + } + return watchNamespace, nil } + return corev1.NamespaceAll, nil } diff --git a/internal/operator-controller/applier/watchnamespace_test.go b/internal/operator-controller/applier/watchnamespace_test.go index 90e018dc7..49248b976 100644 --- a/internal/operator-controller/applier/watchnamespace_test.go +++ b/internal/operator-controller/applier/watchnamespace_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" featuregatetesting "k8s.io/component-base/featuregate/testing" v1 "github.com/operator-framework/operator-controller/api/v1" @@ -17,11 +18,17 @@ func TestGetWatchNamespacesWhenFeatureGateIsDisabled(t *testing.T) { watchNamespace, err := applier.GetWatchNamespace(&v1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ Name: "extension", - Annotations: map[string]string{ - "olm.operatorframework.io/watch-namespace": "watch-namespace", + // Annotations: map[string]string{ + // "olm.operatorframework.io/watch-namespace": "watch-namespace", + // }, + }, + Spec: v1.ClusterExtensionSpec{ + Config: []runtime.RawExtension{ + {Raw: []byte( + `{"apiVersion":"olm.operatorframework.io/v1","kind":"BundleConfig",` + + `"spec":{"watchNamespace":"watch-namespace"}}`)}, }, }, - Spec: v1.ClusterExtensionSpec{}, }) require.NoError(t, err) t.Log("Check watchNamespace is '' even if the annotation is set") @@ -54,11 +61,14 @@ func TestGetWatchNamespace(t *testing.T) { csv: &v1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ Name: "extension", - Annotations: map[string]string{ - "olm.operatorframework.io/watch-namespace": "watch-namespace", + }, + Spec: v1.ClusterExtensionSpec{ + Config: []runtime.RawExtension{ + {Raw: []byte( + `{"apiVersion":"olm.operatorframework.io/v1","kind":"BundleConfig",` + + `"spec":{"watchNamespace":"watch-namespace"}}`)}, }, }, - Spec: v1.ClusterExtensionSpec{}, }, expectError: false, }, { @@ -67,11 +77,14 @@ func TestGetWatchNamespace(t *testing.T) { csv: &v1.ClusterExtension{ ObjectMeta: metav1.ObjectMeta{ Name: "extension", - Annotations: map[string]string{ - "olm.operatorframework.io/watch-namespace": "watch-namespace,watch-namespace2,watch-namespace3", + }, + Spec: v1.ClusterExtensionSpec{ + Config: []runtime.RawExtension{ + {Raw: []byte( + `{"apiVersion":"olm.operatorframework.io/v1","kind":"BundleConfig",` + + `"spec":{"watchNamespace":"watch-namespace,watch-namespace2,watch-namespace3"}}`)}, }, }, - Spec: v1.ClusterExtensionSpec{}, }, expectError: true, }, { @@ -84,7 +97,13 @@ func TestGetWatchNamespace(t *testing.T) { "olm.operatorframework.io/watch-namespace": "watch-namespace-", }, }, - Spec: v1.ClusterExtensionSpec{}, + Spec: v1.ClusterExtensionSpec{ + Config: []runtime.RawExtension{ + {Raw: []byte( + `{"apiVersion":"olm.operatorframework.io/v1","kind":"BundleConfig",` + + `"spec":{"watchNamespace":"watch-namespace-"}}`)}, + }, + }, }, expectError: true, },