Skip to content

Commit 7332db6

Browse files
Merge pull request #17 from PillaiManish/eso-56
ESO-56: Implement the functionality to ensure ValidatingWebhookConfiguration resources stay in desired state
2 parents eaf731a + 969e707 commit 7332db6

File tree

9 files changed

+509
-5
lines changed

9 files changed

+509
-5
lines changed

pkg/controller/constants.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,21 @@ const (
2626

2727
// certificateCRDName is the name of the Certificate CRD provided by cert-manager project.
2828
certificateCRDName = "certificates"
29+
30+
// certManagerInjectCAFromAnnotation is the annotation key added to external-secrets resource once
31+
// if certManager field is enabled in webhook config
32+
// after successful reconciliation by the controller.
33+
certManagerInjectCAFromAnnotation = "cert-manager.io/inject-ca-from"
34+
35+
// certManagerInjectCAFromAnnotationValue is the annotation value added to external-secrets resource once
36+
// if certManager field is enabled in webhook config
37+
// after successful reconciliation by the controller.
38+
certManagerInjectCAFromAnnotationValue = "external-secrets/external-secrets-webhook"
39+
)
40+
41+
// asset names are the files present in the root `bindata/` dir, which are then loaded to
42+
// and made available by the pkg/operator/assets package.
43+
const (
44+
validatingWebhookExternalSecretCRDAssetName = "external-secrets/resources/validatingwebhookconfiguration_externalsecret-validate.yml"
45+
validatingWebhookSecretStoreCRDAssetName = "external-secrets/resources/validatingwebhookconfiguration_secretstore-validate.yml"
2946
)

pkg/controller/install_external_secrets.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,10 @@ import (
55
)
66

77
func (r *ExternalSecretsReconciler) reconcileExternalSecretsDeployment(externalsecrets *operatorv1alpha1.ExternalSecrets, recon bool) error {
8+
if err := r.createOrApplyValidatingWebhookConfiguration(externalsecrets, recon); err != nil {
9+
r.log.Error(err, "failed to reconcile validating webhook resource")
10+
return err
11+
}
12+
813
return nil
914
}

pkg/controller/suite_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626
. "github.com/onsi/ginkgo/v2"
2727
. "github.com/onsi/gomega"
2828

29-
"k8s.io/client-go/kubernetes/scheme"
3029
"k8s.io/client-go/rest"
3130
"sigs.k8s.io/controller-runtime/pkg/client"
3231
"sigs.k8s.io/controller-runtime/pkg/envtest"
@@ -77,12 +76,12 @@ var _ = BeforeSuite(func() {
7776
Expect(err).NotTo(HaveOccurred())
7877
Expect(cfg).NotTo(BeNil())
7978

80-
err = operatorv1alpha1.AddToScheme(scheme.Scheme)
79+
err = operatorv1alpha1.AddToScheme(scheme)
8180
Expect(err).NotTo(HaveOccurred())
8281

8382
// +kubebuilder:scaffold:scheme
8483

85-
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
84+
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
8685
Expect(err).NotTo(HaveOccurred())
8786
Expect(k8sClient).NotTo(BeNil())
8887

pkg/controller/test_utils.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package controller
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/go-logr/logr/testr"
7+
operatorv1alpha1 "github.com/openshift/external-secrets-operator/api/v1alpha1"
8+
"github.com/openshift/external-secrets-operator/pkg/operator/assets"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"testing"
11+
12+
webhook "k8s.io/api/admissionregistration/v1"
13+
"k8s.io/apimachinery/pkg/runtime"
14+
"k8s.io/client-go/tools/record"
15+
)
16+
17+
var (
18+
testError = fmt.Errorf("test client error")
19+
)
20+
21+
const (
22+
// testResourcesName is the name for ExternalSecrets test CR.
23+
testResourcesName = "externalsecrets-test-resource"
24+
)
25+
26+
func testReconciler(t *testing.T) *ExternalSecretsReconciler {
27+
return &ExternalSecretsReconciler{
28+
ctx: context.Background(),
29+
eventRecorder: record.NewFakeRecorder(100),
30+
log: testr.New(t),
31+
Scheme: runtime.NewScheme(),
32+
}
33+
}
34+
35+
func testValidatingWebhookConfiguration(testValidateWebhookConfigurationFile string) *webhook.ValidatingWebhookConfiguration {
36+
validateWebhook := decodeValidatingWebhookConfigurationObjBytes(assets.MustAsset(testValidateWebhookConfigurationFile))
37+
return validateWebhook
38+
}
39+
40+
// testExternalSecrets returns a sample ExternalSecrets object.
41+
func testExternalSecrets() *operatorv1alpha1.ExternalSecrets {
42+
return &operatorv1alpha1.ExternalSecrets{
43+
ObjectMeta: metav1.ObjectMeta{
44+
Name: testResourcesName,
45+
},
46+
}
47+
}

pkg/controller/utils.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,16 @@ import (
2323
)
2424

2525
var (
26-
codecs = serializer.NewCodecFactory(runtime.NewScheme())
26+
scheme = runtime.NewScheme()
27+
codecs = serializer.NewCodecFactory(scheme)
2728
)
2829

30+
func init() {
31+
if err := webhook.AddToScheme(scheme); err != nil {
32+
panic(err)
33+
}
34+
}
35+
2936
// addFinalizer adds finalizer to externalsecrets.openshift.operator.io resource.
3037
func (r *ExternalSecretsReconciler) addFinalizer(ctx context.Context, externalsecrets *operatorv1alpha1.ExternalSecrets) error {
3138
namespacedName := types.NamespacedName{Name: externalsecrets.Name, Namespace: externalsecrets.Namespace}
@@ -199,7 +206,7 @@ func decodeServiceAccountObjBytes(objBytes []byte) *corev1.ServiceAccount {
199206
}
200207

201208
func decodeValidatingWebhookConfigurationObjBytes(objBytes []byte) *webhook.ValidatingWebhookConfiguration {
202-
obj, err := runtime.Decode(codecs.UniversalDecoder(corev1.SchemeGroupVersion), objBytes)
209+
obj, err := runtime.Decode(codecs.UniversalDecoder(webhook.SchemeGroupVersion), objBytes)
203210
if err != nil {
204211
panic(err)
205212
}
@@ -229,6 +236,8 @@ func hasObjectChanged(desired, fetched client.Object) bool {
229236
rbacRoleBindingSubjectsModified[*rbacv1.RoleBinding](desired.(*rbacv1.RoleBinding), fetched.(*rbacv1.RoleBinding))
230237
case *corev1.Service:
231238
objectModified = serviceSpecModified(desired.(*corev1.Service), fetched.(*corev1.Service))
239+
case *webhook.ValidatingWebhookConfiguration:
240+
objectModified = validatingWebHookSpecModified(desired.(*webhook.ValidatingWebhookConfiguration), fetched.(*webhook.ValidatingWebhookConfiguration))
232241
default:
233242
panic(fmt.Sprintf("unsupported object type: %T", desired))
234243
}
@@ -335,3 +344,46 @@ func rbacRoleBindingSubjectsModified[Object *rbacv1.RoleBinding | *rbacv1.Cluste
335344
panic(fmt.Sprintf("unsupported object type %v", typ))
336345
}
337346
}
347+
348+
func validatingWebHookSpecModified(desired, fetched *webhook.ValidatingWebhookConfiguration) bool {
349+
if len(desired.Webhooks) != len(fetched.Webhooks) {
350+
return true
351+
}
352+
353+
fetchedWebhooksMap := make(map[string]webhook.ValidatingWebhook)
354+
for _, wh := range fetched.Webhooks {
355+
fetchedWebhooksMap[wh.Name] = wh
356+
}
357+
358+
for _, desiredWh := range desired.Webhooks {
359+
fetchedWh, ok := fetchedWebhooksMap[desiredWh.Name]
360+
if !ok {
361+
return true
362+
}
363+
364+
if !reflect.DeepEqual(desiredWh.SideEffects, fetchedWh.SideEffects) ||
365+
!reflect.DeepEqual(desiredWh.TimeoutSeconds, fetchedWh.TimeoutSeconds) ||
366+
!reflect.DeepEqual(desiredWh.FailurePolicy, fetchedWh.FailurePolicy) ||
367+
!reflect.DeepEqual(desiredWh.MatchPolicy, fetchedWh.MatchPolicy) ||
368+
!reflect.DeepEqual(desiredWh.NamespaceSelector, fetchedWh.NamespaceSelector) ||
369+
!reflect.DeepEqual(desiredWh.ObjectSelector, fetchedWh.ObjectSelector) ||
370+
!reflect.DeepEqual(desiredWh.MatchConditions, fetchedWh.MatchConditions) ||
371+
!reflect.DeepEqual(desiredWh.AdmissionReviewVersions, fetchedWh.AdmissionReviewVersions) ||
372+
!reflect.DeepEqual(desiredWh.ClientConfig, fetchedWh.ClientConfig) ||
373+
!reflect.DeepEqual(desiredWh.Rules, fetchedWh.Rules) {
374+
return true
375+
}
376+
377+
}
378+
379+
return false
380+
}
381+
382+
// parseBool is for parsing a string value as a boolean value. This is very specific to the values
383+
// read from CR which allows only `true` or `false` as values.
384+
func parseBool(val string) bool {
385+
if val == "true" {
386+
return true
387+
}
388+
return false
389+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package controller
2+
3+
import (
4+
"fmt"
5+
6+
webhook "k8s.io/api/admissionregistration/v1"
7+
corev1 "k8s.io/api/core/v1"
8+
"k8s.io/apimachinery/pkg/types"
9+
10+
operatorv1alpha1 "github.com/openshift/external-secrets-operator/api/v1alpha1"
11+
"github.com/openshift/external-secrets-operator/pkg/operator/assets"
12+
)
13+
14+
func (r *ExternalSecretsReconciler) createOrApplyValidatingWebhookConfiguration(externalsecrets *operatorv1alpha1.ExternalSecrets, recon bool) error {
15+
desiredWebhooks, err := r.getValidatingWebhookObjects(externalsecrets)
16+
if err != nil {
17+
return fmt.Errorf("failed to generate validatingWebhook resource for creation: %w", err)
18+
}
19+
20+
for _, desired := range desiredWebhooks {
21+
validatingWebhookName := desired.GetName()
22+
r.log.V(4).Info("reconciling validatingWebhook resource", "name", validatingWebhookName)
23+
fetched := &webhook.ValidatingWebhookConfiguration{}
24+
key := types.NamespacedName{
25+
Name: desired.GetName(),
26+
}
27+
exist, err := r.Exists(r.ctx, key, fetched)
28+
if err != nil {
29+
return FromClientError(err, "failed to check %s validatingWebhook resource already exists", validatingWebhookName)
30+
}
31+
32+
if exist && recon {
33+
r.eventRecorder.Eventf(externalsecrets, corev1.EventTypeWarning, "ResourceAlreadyExists", "%s validatingWebhook resource already exists, maybe from previous installation", validatingWebhookName)
34+
}
35+
if exist && hasObjectChanged(desired, fetched) {
36+
r.log.V(1).Info("validatingWebhook has been modified", "updating to desired state", "name", validatingWebhookName)
37+
if err := r.UpdateWithRetry(r.ctx, desired); err != nil {
38+
return FromClientError(err, "failed to update %s validatingWebhook resource with desired state", validatingWebhookName)
39+
}
40+
r.eventRecorder.Eventf(externalsecrets, corev1.EventTypeNormal, "Reconciled", "validatingWebhook resource %s reconciled back to desired state", validatingWebhookName)
41+
} else {
42+
r.log.V(4).Info("validatingWebhook resource already exists and is in expected state", "name", validatingWebhookName)
43+
}
44+
45+
if !exist {
46+
if err := r.Create(r.ctx, desired); err != nil {
47+
return FromClientError(err, "failed to create validatingWebhook resource %s", validatingWebhookName)
48+
}
49+
r.eventRecorder.Eventf(externalsecrets, corev1.EventTypeNormal, "Reconciled", "validatingWebhook resource %s created", validatingWebhookName)
50+
}
51+
}
52+
return nil
53+
54+
}
55+
56+
func (r *ExternalSecretsReconciler) getValidatingWebhookObjects(externalsecrets *operatorv1alpha1.ExternalSecrets) ([]*webhook.ValidatingWebhookConfiguration, error) {
57+
var webhooks []*webhook.ValidatingWebhookConfiguration
58+
59+
for _, assetName := range []string{validatingWebhookExternalSecretCRDAssetName, validatingWebhookSecretStoreCRDAssetName} {
60+
61+
validatingWebhook := decodeValidatingWebhookConfigurationObjBytes(assets.MustAsset(assetName))
62+
63+
if err := updateValidatingWebhookAnnotation(externalsecrets, validatingWebhook); err != nil {
64+
return nil, fmt.Errorf("failed to update validatingWebhook resource for %s external secrets: %s", externalsecrets.GetName(), err.Error())
65+
}
66+
67+
webhooks = append(webhooks, validatingWebhook)
68+
}
69+
70+
return webhooks, nil
71+
}
72+
73+
func updateValidatingWebhookAnnotation(externalsecrets *operatorv1alpha1.ExternalSecrets, webhook *webhook.ValidatingWebhookConfiguration) error {
74+
if externalsecrets != nil &&
75+
externalsecrets.Spec.ExternalSecretsConfig != nil &&
76+
externalsecrets.Spec.ExternalSecretsConfig.WebhookConfig != nil &&
77+
externalsecrets.Spec.ExternalSecretsConfig.WebhookConfig.CertManagerConfig != nil {
78+
if parseBool(externalsecrets.Spec.ExternalSecretsConfig.WebhookConfig.CertManagerConfig.AddInjectorAnnotations) {
79+
if webhook.Annotations == nil {
80+
webhook.Annotations = map[string]string{}
81+
}
82+
webhook.Annotations[certManagerInjectCAFromAnnotation] = certManagerInjectCAFromAnnotationValue
83+
}
84+
}
85+
return nil
86+
}

0 commit comments

Comments
 (0)