diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 59613489d..612547248 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 9101bb7f6..1de30e25b 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 000000000..5a1c72cc2 --- /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)) +} 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 000000000..24e8ecc12 --- /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) +}