• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NVIDIA / skyhook / 20494412809

24 Dec 2025 09:50PM UTC coverage: 81.314% (+6.0%) from 75.355%
20494412809

push

github

lockwobr
feat: added new tests to cover the new webhook setup

6384 of 7851 relevant lines covered (81.31%)

4.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

55.46
/operator/internal/controller/webhook_controller.go
1
/*
2
 * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
 * SPDX-License-Identifier: Apache-2.0
4
 *
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18

19
package controller
20

21
import (
22
        "bytes"
23
        "context"
24
        "fmt"
25
        "net/http"
26
        "reflect"
27
        "time"
28

29
        "sigs.k8s.io/controller-runtime/pkg/builder"
30
        "sigs.k8s.io/controller-runtime/pkg/predicate"
31

32
        "github.com/NVIDIA/skyhook/operator/api/v1alpha1"
33
        admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
34
        corev1 "k8s.io/api/core/v1"
35
        "k8s.io/apimachinery/pkg/api/errors"
36
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37
        "k8s.io/apimachinery/pkg/types"
38
        ctrl "sigs.k8s.io/controller-runtime"
39
        runtimecache "sigs.k8s.io/controller-runtime/pkg/cache"
40
        "sigs.k8s.io/controller-runtime/pkg/client"
41
        "sigs.k8s.io/controller-runtime/pkg/log"
42
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
43
)
44

45
const (
46
        // Webhook configuration names
47
        validatingWebhookConfigName = "skyhook-operator-validating-webhook"
48
        mutatingWebhookConfigName   = "skyhook-operator-mutating-webhook"
49

50
        // Webhook names
51
        skyhookValidatingWebhookName          = "validate-skyhook.nvidia.com"
52
        deploymentPolicyValidatingWebhookName = "validate-deploymentpolicy.nvidia.com"
53
        skyhookMutatingWebhookName            = "mutate-skyhook.nvidia.com"
54
        deploymentPolicyMutatingWebhookName   = "mutate-deploymentpolicy.nvidia.com"
55

56
        // Webhook paths
57
        skyhookValidatingPath          = "/validate-skyhook-nvidia-com-v1alpha1-skyhook"
58
        deploymentPolicyValidatingPath = "/validate-skyhook-nvidia-com-v1alpha1-deploymentpolicy"
59
        skyhookMutatingPath            = "/mutate-skyhook-nvidia-com-v1alpha1-skyhook"
60
        deploymentPolicyMutatingPath   = "/mutate-skyhook-nvidia-com-v1alpha1-deploymentpolicy"
61

62
        // Certificate management
63
        certRotationThreshold    = 168 * time.Hour      // 7 days
64
        certValidityDurationYear = 365 * 24 * time.Hour // 1 year
65
        expirationAnnotationKey  = v1alpha1.METADATA_PREFIX + "/expiration"
66
)
67

68
// This project used to use cert-manager to generate the webhook certificates.
69
// This removes the dependency on cert-manager and simplifies the deployment.
70
// This also removes the need to have a specific issuer, and just uses a self-signed cert.
71
type WebhookControllerOptions struct { // prefix these with WEBHOOK_
72
        SecretName  string `env:"WEBHOOK_SECRET_NAME, default=webhook-cert"`
73
        ServiceName string `env:"WEBHOOK_SERVICE_NAME, default=skyhook-operator-webhook-service"`
74
}
75

76
type WebhookController struct {
77
        client.Client
78
        cache     runtimecache.Cache
79
        namespace string
80
        certDir   string
81
        opts      WebhookControllerOptions
82
}
83

84
func NewWebhookController(client client.Client, cache runtimecache.Cache, namespace, certDir string, opts WebhookControllerOptions) (*WebhookController, error) {
×
85
        if err := ensureDummyCert(certDir); err != nil {
×
86
                return nil, err
×
87
        }
×
88

89
        return &WebhookController{
×
90
                Client:    client,
×
91
                cache:     cache,
×
92
                namespace: namespace,
×
93
                certDir:   certDir,
×
94
                opts:      opts,
×
95
        }, nil
×
96
}
97

98
// Start implements the Runnable interface to ensure certificates are set up before the webhook server starts
99
func (r *WebhookController) Start(ctx context.Context) error {
×
100
        logger := log.FromContext(ctx)
×
101
        logger.Info("Setting up webhook certificates")
×
102

×
103
        // wait for the cache to sync
×
104
        if cache := r.cache.WaitForCacheSync(ctx); !cache {
×
105
                return fmt.Errorf("failed to wait for cache to sync")
×
106
        }
×
107
        // starts the reconcile process off
108
        _, err := r.GetOrCreateWebhookCertSecret(ctx, r.opts.SecretName, r.namespace)
×
109
        if err != nil {
×
110
                if errors.IsAlreadyExists(err) {
×
111
                        return nil // ignore this special case, it just needs to exist
×
112
                }
×
113
                return err
×
114
        }
115

116
        logger.Info("Webhook certificates setup complete")
×
117
        return nil
×
118
}
119

120
// NeedLeaderElection implements the Runnable interface, runs only on leader
121
func (r *WebhookController) NeedLeaderElection() bool {
×
122
        return true
×
123
}
×
124

125
func (r *WebhookController) SetupWithManager(mgr ctrl.Manager) error {
×
126
        return ctrl.NewControllerManagedBy(mgr).
×
127
                For(&corev1.Secret{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(obj client.Object) bool {
×
128
                        return obj.GetNamespace() == r.namespace && obj.GetName() == r.opts.SecretName
×
129
                }))).
×
130
                Complete(r)
131
}
132

133
// permissions
134
//+kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=validatingwebhookconfigurations;mutatingwebhookconfigurations,verbs=get;list;watch;create;update;patch;delete
135
//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete
136

137
// Reconcile is the main function that reconciles the webhook controller
138
func (r *WebhookController) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
×
139
        logger := log.FromContext(ctx)
×
140
        logger.Info("Reconciling webhook controller")
×
141

×
142
        // if its deleted, skip reconciliation, this is for cleanup
×
143
        obj := &corev1.Secret{}
×
144
        if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
×
145
                // handle not found, etc.
×
146
                return ctrl.Result{}, client.IgnoreNotFound(err)
×
147
        }
×
148

149
        // If the object is being deleted, skip reconciliation
150
        if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
×
151
                // Optionally: handle finalizers here if you want
×
152
                return ctrl.Result{}, nil
×
153
        }
×
154

155
        // 1. Get or create/update the Secret with certs
156
        // 2. Get or create/update the webhook configurations
157

158
        // Example: check if secret exists
159
        secret, err := r.GetOrCreateWebhookCertSecret(ctx, r.opts.SecretName, r.namespace)
×
160
        if err != nil {
×
161
                return reconcile.Result{}, err
×
162
        }
×
163

164
        _, err = r.CheckOrUpdateWebhookCertSecret(ctx, secret)
×
165
        if err != nil {
×
166
                return reconcile.Result{}, err
×
167
        }
×
168

169
        _, err = r.CheckOrUpdateWebhookConfigurations(ctx, secret)
×
170
        if err != nil {
×
171
                return reconcile.Result{}, err
×
172
        }
×
173

174
        logger.Info("Reconciled webhook controller")
×
175
        return reconcile.Result{RequeueAfter: 24 * time.Hour}, nil // requeue for periodic rotation/check
×
176
}
177

178
// GetOrCreateWebhookCertSecret returns a new secret with the given name and the given CA and cert.
179
func (r *WebhookController) GetOrCreateWebhookCertSecret(ctx context.Context, secretName, namespace string) (*corev1.Secret, error) {
1✔
180

1✔
181
        // get the secret
1✔
182
        secret := &corev1.Secret{}
1✔
183
        err := r.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, secret)
1✔
184
        if err != nil {
2✔
185
                if errors.IsNotFound(err) {
2✔
186
                        // not found, create it
1✔
187
                        webhookCert, err := generateCert(r.opts.ServiceName, r.namespace, certValidityDurationYear)
1✔
188
                        if err != nil {
1✔
189
                                return nil, err
×
190
                        }
×
191

192
                        // Write cert and key to disk if CertDir is set
193
                        if r.certDir != "" {
2✔
194
                                _ = writeCertAndKey([]byte(webhookCert.TLSCert), []byte(webhookCert.TLSKey), r.certDir)
1✔
195
                        }
1✔
196

197
                        secret = webhookCert.ToSecret(secretName, namespace, r.opts.ServiceName)
1✔
198

1✔
199
                        if err := r.Create(ctx, secret); err != nil {
1✔
200
                                return nil, err
×
201
                        }
×
202

203
                        return secret, nil
1✔
204
                }
205
                return nil, err
×
206
        }
207

208
        // found, return it
209
        return secret, nil
×
210
}
211

212
// CheckOrUpdateWebhookCertSecret checks if the webhook secret is going to expire in the next 7 days or if the cert on disk is different from the secret
213
// if it is, it will generate a new cert and update the secret
214
func (r *WebhookController) CheckOrUpdateWebhookCertSecret(ctx context.Context, secret *corev1.Secret) (bool, error) {
1✔
215
        equal, err := compareCertOnDiskToSecret(r.certDir, secret)
1✔
216
        if err != nil {
1✔
217
                return false, err
×
218
        }
×
219

220
        // check if the secret is going to expire in the next 7 days or if the cert on disk is different from the secret
221
        if !equal || secret.Annotations[expirationAnnotationKey] < time.Now().Add(certRotationThreshold).Format(time.RFC3339) {
2✔
222
                // expired, generate a new cert
1✔
223
                webhookCert, err := generateCert(r.opts.ServiceName, r.namespace, certValidityDurationYear)
1✔
224
                if err != nil {
1✔
225
                        return false, err
×
226
                }
×
227

228
                // Write cert and key to disk if CertDir is set
229
                if r.certDir != "" {
2✔
230
                        _ = writeCertAndKey([]byte(webhookCert.TLSCert), []byte(webhookCert.TLSKey), r.certDir)
1✔
231
                }
1✔
232

233
                secret.Data["ca.crt"] = webhookCert.CABytes
1✔
234
                secret.Data["tls.crt"] = []byte(webhookCert.TLSCert)
1✔
235
                secret.Data["tls.key"] = []byte(webhookCert.TLSKey)
1✔
236
                secret.Annotations[expirationAnnotationKey] = webhookCert.Expiration.Format(time.RFC3339)
1✔
237

1✔
238
                return true, r.Update(ctx, secret)
1✔
239
        }
240

241
        return false, nil
×
242
}
243

244
// CheckOrUpdateWebhookConfigurations checks if the webhook configurations are need to be updated with the new cert
245
// if it is, it will update the webhook configurations
246
func (r *WebhookController) CheckOrUpdateWebhookConfigurations(ctx context.Context, secret *corev1.Secret) (bool, error) {
×
247
        caBundle := secret.Data["ca.crt"]
×
248

×
249
        validatingChanged, err := r.updateValidatingWebhookConfiguration(ctx, caBundle)
×
250
        if err != nil {
×
251
                return false, err
×
252
        }
×
253

254
        mutatingChanged, err := r.updateMutatingWebhookConfiguration(ctx, caBundle)
×
255
        if err != nil {
×
256
                return false, err
×
257
        }
×
258

259
        return validatingChanged || mutatingChanged, nil
×
260
}
261

262
// updateValidatingWebhookConfiguration updates the ValidatingWebhookConfiguration with the provided CABundle
263
func (r *WebhookController) updateValidatingWebhookConfiguration(ctx context.Context, caBundle []byte) (bool, error) {
×
264
        existingValidating := &admissionregistrationv1.ValidatingWebhookConfiguration{}
×
265
        if err := r.Get(ctx, types.NamespacedName{Name: validatingWebhookConfigName}, existingValidating); err != nil {
×
266
                if errors.IsNotFound(err) {
×
267
                        return false, fmt.Errorf("ValidatingWebhookConfiguration %q not found; creation is handled by the Helm chart. Ensure the chart is installed and webhooks are enabled: %w", validatingWebhookConfigName, err)
×
268
                }
×
269
                return false, fmt.Errorf("failed to get ValidatingWebhookConfiguration %q: %w", validatingWebhookConfigName, err)
×
270
        }
271

272
        needUpdate := false
×
273
        for i := range existingValidating.Webhooks {
×
274
                expectedRules := r.getValidatingWebhookRules(existingValidating.Webhooks[i].Name)
×
275
                if expectedRules == nil {
×
276
                        continue // Unknown webhook, skip
×
277
                }
278
                if validatingWebhookNeedsUpdate(&existingValidating.Webhooks[i], caBundle, expectedRules) {
×
279
                        needUpdate = true
×
280
                }
×
281
        }
282

283
        if needUpdate {
×
284
                if err := r.Update(ctx, existingValidating); err != nil {
×
285
                        return false, err
×
286
                }
×
287
                return true, nil
×
288
        }
289

290
        return false, nil
×
291
}
292

293
// updateMutatingWebhookConfiguration updates the MutatingWebhookConfiguration with the provided CABundle
294
func (r *WebhookController) updateMutatingWebhookConfiguration(ctx context.Context, caBundle []byte) (bool, error) {
×
295
        existingMutating := &admissionregistrationv1.MutatingWebhookConfiguration{}
×
296
        if err := r.Get(ctx, types.NamespacedName{Name: mutatingWebhookConfigName}, existingMutating); err != nil {
×
297
                if errors.IsNotFound(err) {
×
298
                        return false, fmt.Errorf("MutatingWebhookConfiguration %q not found; creation is handled by the Helm chart. Ensure the chart is installed and webhooks are enabled: %w", mutatingWebhookConfigName, err)
×
299
                }
×
300
                return false, fmt.Errorf("failed to get MutatingWebhookConfiguration %q: %w", mutatingWebhookConfigName, err)
×
301
        }
302

303
        needUpdate := false
×
304
        for i := range existingMutating.Webhooks {
×
305
                expectedRules := r.getMutatingWebhookRules(existingMutating.Webhooks[i].Name)
×
306
                if expectedRules == nil {
×
307
                        continue // Unknown webhook, skip
×
308
                }
309
                if mutatingWebhookNeedsUpdate(&existingMutating.Webhooks[i], caBundle, expectedRules) {
×
310
                        needUpdate = true
×
311
                }
×
312
        }
313

314
        if needUpdate {
×
315
                if err := r.Update(ctx, existingMutating); err != nil {
×
316
                        return false, err
×
317
                }
×
318
                return true, nil
×
319
        }
320

321
        return false, nil
×
322
}
323

324
// getValidatingWebhookRules returns the expected rules for a validating webhook by name
325
func (r *WebhookController) getValidatingWebhookRules(webhookName string) []admissionregistrationv1.RuleWithOperations {
×
326
        switch webhookName {
×
327
        case skyhookValidatingWebhookName:
×
328
                return skyhookRules()
×
329
        case deploymentPolicyValidatingWebhookName:
×
330
                return deploymentPolicyValidatingRules()
×
331
        default:
×
332
                return nil
×
333
        }
334
}
335

336
// getMutatingWebhookRules returns the expected rules for a mutating webhook by name
337
func (r *WebhookController) getMutatingWebhookRules(webhookName string) []admissionregistrationv1.RuleWithOperations {
×
338
        switch webhookName {
×
339
        case skyhookMutatingWebhookName:
×
340
                return skyhookRules()
×
341
        case deploymentPolicyMutatingWebhookName:
×
342
                return deploymentPolicyMutatingRules()
×
343
        default:
×
344
                return nil
×
345
        }
346
}
347

348
// webhookValidatingWebhookConfiguration returns a new validating webhook configuration.
349
func webhookValidatingWebhookConfiguration(namespace, serviceName string, secret *corev1.Secret) *admissionregistrationv1.ValidatingWebhookConfiguration {
1✔
350
        conf := admissionregistrationv1.ValidatingWebhookConfiguration{
1✔
351
                ObjectMeta: metav1.ObjectMeta{
1✔
352
                        Name: validatingWebhookConfigName,
1✔
353
                },
1✔
354
                Webhooks: []admissionregistrationv1.ValidatingWebhook{
1✔
355
                        {
1✔
356
                                Name:                    skyhookValidatingWebhookName,
1✔
357
                                ClientConfig:            webhookClient(serviceName, namespace, skyhookValidatingPath, secret),
1✔
358
                                FailurePolicy:           ptr(admissionregistrationv1.Fail),
1✔
359
                                Rules:                   skyhookRules(),
1✔
360
                                SideEffects:             ptr(admissionregistrationv1.SideEffectClassNone),
1✔
361
                                AdmissionReviewVersions: []string{"v1"},
1✔
362
                        },
1✔
363
                        {
1✔
364
                                Name:                    deploymentPolicyValidatingWebhookName,
1✔
365
                                ClientConfig:            webhookClient(serviceName, namespace, deploymentPolicyValidatingPath, secret),
1✔
366
                                FailurePolicy:           ptr(admissionregistrationv1.Fail),
1✔
367
                                Rules:                   deploymentPolicyValidatingRules(),
1✔
368
                                SideEffects:             ptr(admissionregistrationv1.SideEffectClassNone),
1✔
369
                                AdmissionReviewVersions: []string{"v1"},
1✔
370
                        },
1✔
371
                },
1✔
372
        }
1✔
373

1✔
374
        return &conf
1✔
375
}
1✔
376

377
// webhookMutatingWebhookConfiguration returns a new mutating webhook configuration.
378
func webhookMutatingWebhookConfiguration(namespace, serviceName string, secret *corev1.Secret) *admissionregistrationv1.MutatingWebhookConfiguration {
1✔
379
        conf := admissionregistrationv1.MutatingWebhookConfiguration{
1✔
380
                ObjectMeta: metav1.ObjectMeta{
1✔
381
                        Name: mutatingWebhookConfigName,
1✔
382
                },
1✔
383
                Webhooks: []admissionregistrationv1.MutatingWebhook{
1✔
384
                        {
1✔
385
                                Name:                    skyhookMutatingWebhookName,
1✔
386
                                ClientConfig:            webhookClient(serviceName, namespace, skyhookMutatingPath, secret),
1✔
387
                                FailurePolicy:           ptr(admissionregistrationv1.Fail),
1✔
388
                                Rules:                   skyhookRules(),
1✔
389
                                SideEffects:             ptr(admissionregistrationv1.SideEffectClassNone),
1✔
390
                                AdmissionReviewVersions: []string{"v1"},
1✔
391
                        },
1✔
392
                        {
1✔
393
                                Name:                    deploymentPolicyMutatingWebhookName,
1✔
394
                                ClientConfig:            webhookClient(serviceName, namespace, deploymentPolicyMutatingPath, secret),
1✔
395
                                FailurePolicy:           ptr(admissionregistrationv1.Fail),
1✔
396
                                Rules:                   deploymentPolicyMutatingRules(),
1✔
397
                                SideEffects:             ptr(admissionregistrationv1.SideEffectClassNone),
1✔
398
                                AdmissionReviewVersions: []string{"v1"},
1✔
399
                        },
1✔
400
                },
1✔
401
        }
1✔
402

1✔
403
        return &conf
1✔
404
}
1✔
405

406
func compareMutatingWebhookConfigurations(a, b *admissionregistrationv1.MutatingWebhookConfiguration) bool {
1✔
407
        if len(a.Webhooks) != len(b.Webhooks) {
2✔
408
                return true
1✔
409
        }
1✔
410
        for i := range a.Webhooks {
2✔
411
                if !bytes.Equal(a.Webhooks[i].ClientConfig.CABundle, b.Webhooks[i].ClientConfig.CABundle) {
2✔
412
                        return true
1✔
413
                }
1✔
414
        }
415
        return false
1✔
416
}
417

418
func compareValidatingWebhookConfigurations(a, b *admissionregistrationv1.ValidatingWebhookConfiguration) bool {
1✔
419
        if len(a.Webhooks) != len(b.Webhooks) {
2✔
420
                return true
1✔
421
        }
1✔
422
        for i := range a.Webhooks {
2✔
423
                if !bytes.Equal(a.Webhooks[i].ClientConfig.CABundle, b.Webhooks[i].ClientConfig.CABundle) {
2✔
424
                        return true
1✔
425
                }
1✔
426
        }
427
        return false
1✔
428
}
429

430
func webhookClient(serviceName, namespace, path string, secret *corev1.Secret) admissionregistrationv1.WebhookClientConfig {
1✔
431
        return admissionregistrationv1.WebhookClientConfig{
1✔
432
                Service: &admissionregistrationv1.ServiceReference{
1✔
433
                        Name:      serviceName,
1✔
434
                        Namespace: namespace,
1✔
435
                        Path:      ptr(path),
1✔
436
                },
1✔
437
                CABundle: secret.Data["ca.crt"],
1✔
438
        }
1✔
439
}
1✔
440

441
func skyhookRules() []admissionregistrationv1.RuleWithOperations {
1✔
442
        return []admissionregistrationv1.RuleWithOperations{
1✔
443
                {
1✔
444
                        Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
1✔
445
                        Rule: admissionregistrationv1.Rule{
1✔
446
                                APIGroups:   []string{v1alpha1.GroupVersion.Group},
1✔
447
                                APIVersions: []string{v1alpha1.GroupVersion.Version},
1✔
448
                                Resources:   []string{"skyhooks"},
1✔
449
                        },
1✔
450
                },
1✔
451
        }
1✔
452
}
1✔
453

454
// deploymentPolicyValidatingRules adds the delete operation to the mutating webhook rules, otherwise they are the same
455
func deploymentPolicyValidatingRules() []admissionregistrationv1.RuleWithOperations {
1✔
456
        mutrules := deploymentPolicyMutatingRules()
1✔
457
        oprs := mutrules[0].Operations
1✔
458
        newops := make([]admissionregistrationv1.OperationType, len(oprs))
1✔
459
        copy(newops, oprs)
1✔
460
        newops = append(newops, admissionregistrationv1.Delete)
1✔
461
        mutrules[0].Operations = newops
1✔
462
        return mutrules
1✔
463
}
1✔
464

465
func deploymentPolicyMutatingRules() []admissionregistrationv1.RuleWithOperations {
1✔
466
        return []admissionregistrationv1.RuleWithOperations{
1✔
467
                {
1✔
468
                        Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
1✔
469
                        Rule: admissionregistrationv1.Rule{
1✔
470
                                APIGroups:   []string{v1alpha1.GroupVersion.Group},
1✔
471
                                APIVersions: []string{v1alpha1.GroupVersion.Version},
1✔
472
                                Resources:   []string{"deploymentpolicies"},
1✔
473
                        },
1✔
474
                },
1✔
475
        }
1✔
476
}
1✔
477

478
// validatingWebhookNeedsUpdate checks if a validating webhook needs to be updated with new CABundle or Rules
479
// Returns true if updates were made to the webhook
480
func validatingWebhookNeedsUpdate(webhook *admissionregistrationv1.ValidatingWebhook, caBundle []byte, expectedRules []admissionregistrationv1.RuleWithOperations) bool {
1✔
481
        needUpdate := false
1✔
482

1✔
483
        // Check if CABundle needs to be set
1✔
484
        if len(webhook.ClientConfig.CABundle) == 0 {
1✔
485
                webhook.ClientConfig.CABundle = caBundle
×
486
                needUpdate = true
×
487
        }
×
488

489
        // Check if rules need to be updated
490
        if !reflect.DeepEqual(webhook.Rules, expectedRules) {
2✔
491
                webhook.Rules = expectedRules
1✔
492
                needUpdate = true
1✔
493
        }
1✔
494

495
        return needUpdate
1✔
496
}
497

498
// mutatingWebhookNeedsUpdate checks if a mutating webhook needs to be updated
499
func mutatingWebhookNeedsUpdate(webhook *admissionregistrationv1.MutatingWebhook, caBundle []byte, expectedRules []admissionregistrationv1.RuleWithOperations) bool {
1✔
500
        needUpdate := false
1✔
501

1✔
502
        // Check if CABundle needs to be set
1✔
503
        if len(webhook.ClientConfig.CABundle) == 0 {
2✔
504
                webhook.ClientConfig.CABundle = caBundle
1✔
505
                needUpdate = true
1✔
506
        }
1✔
507

508
        // Check if rules need to be updated
509
        if !reflect.DeepEqual(webhook.Rules, expectedRules) {
1✔
510
                webhook.Rules = expectedRules
×
511
                needUpdate = true
×
512
        }
×
513

514
        return needUpdate
1✔
515
}
516

517
// WebhookSecretReadyzCheck is a readyz check for the webhook secret, if it does not exist, it will return an error
518
// if it exists, it will wait for the secret to be ready, this makes sure that we don't start the operator
519
// if the webhook secret is not ready
520
func (r *WebhookController) WebhookSecretReadyzCheck(_ *http.Request) error {
1✔
521
        secret := &corev1.Secret{}
1✔
522
        err := r.Client.Get(context.Background(), types.NamespacedName{
1✔
523
                Name:      r.opts.SecretName,
1✔
524
                Namespace: r.namespace,
1✔
525
        }, secret)
1✔
526

1✔
527
        if err != nil {
2✔
528
                return err
1✔
529
        }
1✔
530

531
        equal, err := compareCertOnDiskToSecret(r.certDir, secret)
1✔
532
        if err != nil {
1✔
533
                return err
×
534
        }
×
535

536
        if !equal {
2✔
537
                return fmt.Errorf("webhook secret is not ready")
1✔
538
        }
1✔
539

540
        // check for the webhook configurations
541
        validatingWebhookName := webhookValidatingWebhookConfiguration(r.namespace, r.opts.ServiceName, secret).GetName()
1✔
542
        validatingWebhookConfiguration := &admissionregistrationv1.ValidatingWebhookConfiguration{}
1✔
543
        err = r.Get(context.Background(), types.NamespacedName{Name: validatingWebhookName}, validatingWebhookConfiguration)
1✔
544
        if err != nil {
2✔
545
                if errors.IsNotFound(err) {
2✔
546
                        return fmt.Errorf("ValidatingWebhookConfiguration %q not found. Either disable webhooks (not recommended) or reinstall the operator via the Helm chart to provision webhooks", validatingWebhookName)
1✔
547
                }
1✔
548
                return err
×
549
        }
550

551
        if !bytes.Equal(validatingWebhookConfiguration.Webhooks[0].ClientConfig.CABundle, secret.Data["ca.crt"]) {
2✔
552
                return fmt.Errorf("webhook secret is not ready, ca bundle is not equal to the validating webhook configuration")
1✔
553
        }
1✔
554

555
        mutatingWebhookConfiguration := webhookMutatingWebhookConfiguration(r.namespace, r.opts.ServiceName, secret)
1✔
556
        err = r.Get(context.Background(), types.NamespacedName{Name: mutatingWebhookConfiguration.Name}, mutatingWebhookConfiguration)
1✔
557
        if err != nil {
1✔
558
                if errors.IsNotFound(err) {
×
559
                        return fmt.Errorf("MutatingWebhookConfiguration %q not found. Either disable webhooks (not recommended) or reinstall the operator via the Helm chart to provision webhooks", mutatingWebhookConfiguration.Name)
×
560
                }
×
561
                return err
×
562
        }
563

564
        if !bytes.Equal(mutatingWebhookConfiguration.Webhooks[0].ClientConfig.CABundle, secret.Data["ca.crt"]) {
1✔
565
                return fmt.Errorf("webhook secret is not ready, ca bundle is not equal to the mutating webhook configuration")
×
566
        }
×
567

568
        return nil
1✔
569
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc