Repository: operator-framework/operator-controller
Pull request #1969 (2025-05-15 13:55:43)

:sparkles: Add openshift-serviceca certificate provider
# Description

Adds openshift-serviceca certificate provider support for webhooks. Enabled via feature gate: `WebhookProviderOpenshiftServiceCA`

## Reviewer Checklist

- [ ] API Go Documentation
- [ ] Tests: Unit Tests (and E2E Tests, if appropriate)
- [ ] Comprehensive Commit Messages
- [ ] Links to related GitHub Issue(s)


netlify[bot]: ### <span aria-hidden="true">✅</span> Deploy Preview for *olmv1* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 158c06002654e3d0989c081993e0103ceaa9be49 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/projects/olmv1/deploys/6825f261d61659000887a1df |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1969--olmv1.netlify.app](https://deploy-preview-1969--olmv1.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTE5NjktLW9sbXYxLm5ldGxpZnkuYXBwIn0.gQGZ2RHYHlE07kUnVspprMLqj5a8a_d-TM4lzc6Eczc)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [olmv1 Preview](https://deploy-preview-1969--olmv1.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify project configuration](https://app.netlify.com/projects/olmv1/configuration/notifications#deploy-notifications)._

Patch:
diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go
index 5961348..6125472 100644
--- a/cmd/operator-controller/main.go
+++ b/cmd/operator-controller/main.go
@@ -431,6 +431,9 @@ func run() error {
 	if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderCertManager) {
 		certProvider = certproviders.CertManagerCertificateProvider{}
 		isWebhookSupportEnabled = true
+	} else if features.OperatorControllerFeatureGate.Enabled(features.WebhookProviderOpenshiftServiceCA) {
+		certProvider = certproviders.OpenshiftServiceCaCertificateProvider{}
+		isWebhookSupportEnabled = true
 	}
 
 	// now initialize the helmApplier, assigning the potentially nil preAuth
diff --git a/internal/operator-controller/features/features.go b/internal/operator-controller/features/features.go
index 9101bb7..1de30e2 100644
--- a/internal/operator-controller/features/features.go
+++ b/internal/operator-controller/features/features.go
@@ -11,10 +11,11 @@ import (
 const (
 	// Add new feature gates constants (strings)
 	// Ex: SomeFeature featuregate.Feature = "SomeFeature"
-	PreflightPermissions             featuregate.Feature = "PreflightPermissions"
-	SingleOwnNamespaceInstallSupport featuregate.Feature = "SingleOwnNamespaceInstallSupport"
-	SyntheticPermissions             featuregate.Feature = "SyntheticPermissions"
-	WebhookProviderCertManager       featuregate.Feature = "WebhookProviderCertManager"
+	PreflightPermissions              featuregate.Feature = "PreflightPermissions"
+	SingleOwnNamespaceInstallSupport  featuregate.Feature = "SingleOwnNamespaceInstallSupport"
+	SyntheticPermissions              featuregate.Feature = "SyntheticPermissions"
+	WebhookProviderCertManager        featuregate.Feature = "WebhookProviderCertManager"
+	WebhookProviderOpenshiftServiceCA featuregate.Feature = "WebhookProviderOpenshiftServiceCA"
 )
 
 var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
@@ -52,6 +53,16 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature
 		PreRelease:    featuregate.Alpha,
 		LockToDefault: false,
 	},
+
+	// WebhookProviderCertManager enables support for installing
+	// registry+v1 cluster extensions that include validating,
+	// mutating, and/or conversion webhooks with Openshift Service CA
+	// as the certificate provider.
+	WebhookProviderOpenshiftServiceCA: {
+		Default:       false,
+		PreRelease:    featuregate.Alpha,
+		LockToDefault: false,
+	},
 }
 
 var OperatorControllerFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate()
diff --git a/internal/operator-controller/rukpak/render/certproviders/openshift_serviceca.go b/internal/operator-controller/rukpak/render/certproviders/openshift_serviceca.go
new file mode 100644
index 0000000..5a1c72c
--- /dev/null
+++ b/internal/operator-controller/rukpak/render/certproviders/openshift_serviceca.go
@@ -0,0 +1,61 @@
+package certproviders
+
+import (
+	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
+	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
+	"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
+)
+
+const (
+	openshiftServiceCAServingCertNameAnnotation = "service.beta.openshift.io/serving-cert-secret-name"
+	openshiftServiceCAInjectCABundleAnnotation  = "service.beta.openshift.io/inject-cabundle"
+)
+
+var _ render.CertificateProvider = (*OpenshiftServiceCaCertificateProvider)(nil)
+
+type OpenshiftServiceCaCertificateProvider struct{}
+
+func (p OpenshiftServiceCaCertificateProvider) InjectCABundle(obj client.Object, cfg render.CertificateProvisionerConfig) error {
+	switch obj.(type) {
+	case *admissionregistrationv1.ValidatingWebhookConfiguration:
+		p.addInjectCABundleAnnotation(obj)
+	case *admissionregistrationv1.MutatingWebhookConfiguration:
+		p.addInjectCABundleAnnotation(obj)
+	case *apiextensionsv1.CustomResourceDefinition:
+		p.addInjectCABundleAnnotation(obj)
+	case *corev1.Service:
+		p.addServingCertSecretNameAnnotation(obj, cfg.CertName)
+	}
+	return nil
+}
+
+func (p OpenshiftServiceCaCertificateProvider) AdditionalObjects(_ render.CertificateProvisionerConfig) ([]unstructured.Unstructured, error) {
+	return nil, nil
+}
+
+func (p OpenshiftServiceCaCertificateProvider) GetCertSecretInfo(cfg render.CertificateProvisionerConfig) render.CertSecretInfo {
+	return render.CertSecretInfo{
+		SecretName:     cfg.CertName,
+		PrivateKeyKey:  "tls.key",
+		CertificateKey: "tls.crt",
+	}
+}
+
+func (p OpenshiftServiceCaCertificateProvider) addServingCertSecretNameAnnotation(obj client.Object, certName string) {
+	injectionAnnotation := map[string]string{
+		openshiftServiceCAServingCertNameAnnotation: certName,
+	}
+	obj.SetAnnotations(util.MergeMaps(obj.GetAnnotations(), injectionAnnotation))
+}
+
+func (p OpenshiftServiceCaCertificateProvider) addInjectCABundleAnnotation(obj client.Object) {
+	injectionAnnotation := map[string]string{
+		openshiftServiceCAInjectCABundleAnnotation: "true",
+	}
+	obj.SetAnnotations(util.MergeMaps(obj.GetAnnotations(), injectionAnnotation))
+}


Test patch:
diff --git a/internal/operator-controller/rukpak/render/certproviders/openshift_serviceca_test.go b/internal/operator-controller/rukpak/render/certproviders/openshift_serviceca_test.go
new file mode 100644
index 0000000..24e8ecc
--- /dev/null
+++ b/internal/operator-controller/rukpak/render/certproviders/openshift_serviceca_test.go
@@ -0,0 +1,130 @@
+package certproviders_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
+	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
+	"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/certproviders"
+)
+
+func Test_OpenshiftServiceCAProvider_InjectCABundle(t *testing.T) {
+	for _, tc := range []struct {
+		name        string
+		obj         client.Object
+		cfg         render.CertificateProvisionerConfig
+		expectedObj client.Object
+	}{
+		{
+			name: "injects inject-cabundle annotation in validating webhook configuration",
+			obj:  &admissionregistrationv1.ValidatingWebhookConfiguration{},
+			cfg: render.CertificateProvisionerConfig{
+				WebhookServiceName: "webhook-service",
+				Namespace:          "namespace",
+				CertName:           "cert-name",
+			},
+			expectedObj: &admissionregistrationv1.ValidatingWebhookConfiguration{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"service.beta.openshift.io/inject-cabundle": "true",
+					},
+				},
+			},
+		},
+		{
+			name: "injects inject-cabundle annotation in mutating webhook configuration",
+			obj:  &admissionregistrationv1.MutatingWebhookConfiguration{},
+			cfg: render.CertificateProvisionerConfig{
+				WebhookServiceName: "webhook-service",
+				Namespace:          "namespace",
+				CertName:           "cert-name",
+			},
+			expectedObj: &admissionregistrationv1.MutatingWebhookConfiguration{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"service.beta.openshift.io/inject-cabundle": "true",
+					},
+				},
+			},
+		},
+		{
+			name: "injects inject-cabundle annotation in custom resource definition",
+			obj:  &apiextensionsv1.CustomResourceDefinition{},
+			cfg: render.CertificateProvisionerConfig{
+				WebhookServiceName: "webhook-service",
+				Namespace:          "namespace",
+				CertName:           "cert-name",
+			},
+			expectedObj: &apiextensionsv1.CustomResourceDefinition{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"service.beta.openshift.io/inject-cabundle": "true",
+					},
+				},
+			},
+		},
+		{
+			name: "injects serving-cert-secret-name annotation in service resource referencing the certificate name",
+			obj:  &corev1.Service{},
+			cfg: render.CertificateProvisionerConfig{
+				WebhookServiceName: "webhook-service",
+				Namespace:          "namespace",
+				CertName:           "cert-name",
+			},
+			expectedObj: &corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Annotations: map[string]string{
+						"service.beta.openshift.io/serving-cert-secret-name": "cert-name",
+					},
+				},
+			},
+		},
+		{
+			name: "ignores other objects",
+			obj:  &corev1.Secret{},
+			cfg: render.CertificateProvisionerConfig{
+				WebhookServiceName: "webhook-service",
+				Namespace:          "namespace",
+				CertName:           "cert-name",
+			},
+			expectedObj: &corev1.Secret{},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			certProvider := certproviders.OpenshiftServiceCaCertificateProvider{}
+			require.NoError(t, certProvider.InjectCABundle(tc.obj, tc.cfg))
+			require.Equal(t, tc.expectedObj, tc.obj)
+		})
+	}
+}
+
+func Test_OpenshiftServiceCAProvider_AdditionalObjects(t *testing.T) {
+	certProvider := certproviders.OpenshiftServiceCaCertificateProvider{}
+	objs, err := certProvider.AdditionalObjects(render.CertificateProvisionerConfig{
+		WebhookServiceName: "webhook-service",
+		Namespace:          "namespace",
+		CertName:           "cert-name",
+	})
+	require.NoError(t, err)
+	require.Nil(t, objs)
+}
+
+func Test_OpenshiftServiceCAProvider_GetCertSecretInfo(t *testing.T) {
+	certProvider := certproviders.OpenshiftServiceCaCertificateProvider{}
+	certInfo := certProvider.GetCertSecretInfo(render.CertificateProvisionerConfig{
+		WebhookServiceName: "webhook-service",
+		Namespace:          "namespace",
+		CertName:           "cert-name",
+	})
+	require.Equal(t, render.CertSecretInfo{
+		SecretName:     "cert-name",
+		PrivateKeyKey:  "tls.key",
+		CertificateKey: "tls.crt",
+	}, certInfo)
+}