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

NVIDIA / skyhook / 21848362404

10 Feb 2026 01:46AM UTC coverage: 80.713% (-0.1%) from 80.83%
21848362404

push

github

lockwobr
fix: resolve webhook caBundle deadlock during helm upgrade

During helm upgrade, the webhook configurations' caBundle field was
reset to empty, causing new pods to fail readiness checks while the
old leader pod never detected the change (only watched the cert
Secret, with a 24h requeue). This created a deadlock where no pod
could fix the caBundle.

- Watch ValidatingWebhookConfiguration and MutatingWebhookConfiguration
  so the leader detects caBundle changes immediately
- Use bytes.Equal for caBundle comparison instead of len==0 so stale
  values are corrected, not just empty ones
- Remove caBundle from Helm webhook templates so upgrades stop
  resetting operator-managed values

4 of 15 new or added lines in 1 file covered. (26.67%)

4 existing lines in 1 file now uncovered.

6817 of 8446 relevant lines covered (80.71%)

4.27 hits per line

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

54.62
/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/handler"
31
        "sigs.k8s.io/controller-runtime/pkg/predicate"
32

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

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

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

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

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

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

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

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

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

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

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

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

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

126
func (r *WebhookController) SetupWithManager(mgr ctrl.Manager) error {
×
127
        return ctrl.NewControllerManagedBy(mgr).
×
128
                For(&corev1.Secret{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(obj client.Object) bool {
×
129
                        return obj.GetNamespace() == r.namespace && obj.GetName() == r.opts.SecretName
×
130
                }))).
×
131
                // Watch webhook configurations so the leader detects external changes (e.g. Helm upgrade
132
                // resetting caBundle) and fixes them immediately instead of waiting for the 24h requeue.
133
                Watches(&admissionregistrationv1.ValidatingWebhookConfiguration{},
134
                        handler.EnqueueRequestsFromMapFunc(r.webhookConfigToSecret),
NEW
135
                        builder.WithPredicates(predicate.NewPredicateFuncs(func(obj client.Object) bool {
×
NEW
136
                                return obj.GetName() == validatingWebhookConfigName
×
NEW
137
                        }))).
×
138
                Watches(&admissionregistrationv1.MutatingWebhookConfiguration{},
139
                        handler.EnqueueRequestsFromMapFunc(r.webhookConfigToSecret),
NEW
140
                        builder.WithPredicates(predicate.NewPredicateFuncs(func(obj client.Object) bool {
×
NEW
141
                                return obj.GetName() == mutatingWebhookConfigName
×
NEW
142
                        }))).
×
143
                Complete(r)
144
}
145

146
// webhookConfigToSecret maps webhook config change events back to the cert Secret,
147
// so the existing Reconcile() can verify and fix the caBundle.
NEW
148
func (r *WebhookController) webhookConfigToSecret(_ context.Context, _ client.Object) []reconcile.Request {
×
NEW
149
        return []reconcile.Request{{
×
NEW
150
                NamespacedName: types.NamespacedName{Name: r.opts.SecretName, Namespace: r.namespace},
×
NEW
151
        }}
×
NEW
152
}
×
153

154
// permissions
155
//+kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=validatingwebhookconfigurations;mutatingwebhookconfigurations,verbs=get;list;watch;create;update;patch;delete
156
//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete
157

158
// Reconcile is the main function that reconciles the webhook controller
159
func (r *WebhookController) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
×
160
        logger := log.FromContext(ctx)
×
161
        logger.Info("Reconciling webhook controller")
×
162

×
163
        // if its deleted, skip reconciliation, this is for cleanup
×
164
        obj := &corev1.Secret{}
×
165
        if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
×
166
                // handle not found, etc.
×
167
                return ctrl.Result{}, client.IgnoreNotFound(err)
×
168
        }
×
169

170
        // If the object is being deleted, skip reconciliation
171
        if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
×
172
                // Optionally: handle finalizers here if you want
×
173
                return ctrl.Result{}, nil
×
174
        }
×
175

176
        // 1. Get or create/update the Secret with certs
177
        // 2. Get or create/update the webhook configurations
178

179
        // Example: check if secret exists
180
        secret, err := r.GetOrCreateWebhookCertSecret(ctx, r.opts.SecretName, r.namespace)
×
181
        if err != nil {
×
182
                return reconcile.Result{}, err
×
183
        }
×
184

185
        _, err = r.CheckOrUpdateWebhookCertSecret(ctx, secret)
×
186
        if err != nil {
×
187
                return reconcile.Result{}, err
×
188
        }
×
189

190
        _, err = r.CheckOrUpdateWebhookConfigurations(ctx, secret)
×
191
        if err != nil {
×
192
                return reconcile.Result{}, err
×
193
        }
×
194

195
        logger.Info("Reconciled webhook controller")
×
196
        return reconcile.Result{RequeueAfter: 24 * time.Hour}, nil // requeue for periodic rotation/check
×
197
}
198

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

1✔
202
        // get the secret
1✔
203
        secret := &corev1.Secret{}
1✔
204
        err := r.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, secret)
1✔
205
        if err != nil {
2✔
206
                if errors.IsNotFound(err) {
2✔
207
                        // not found, create it
1✔
208
                        webhookCert, err := generateCert(r.opts.ServiceName, r.namespace, certValidityDurationYear)
1✔
209
                        if err != nil {
1✔
210
                                return nil, err
×
211
                        }
×
212

213
                        // Write cert and key to disk if CertDir is set
214
                        if r.certDir != "" {
2✔
215
                                _ = writeCertAndKey([]byte(webhookCert.TLSCert), []byte(webhookCert.TLSKey), r.certDir)
1✔
216
                        }
1✔
217

218
                        secret = webhookCert.ToSecret(secretName, namespace, r.opts.ServiceName)
1✔
219

1✔
220
                        if err := r.Create(ctx, secret); err != nil {
1✔
221
                                return nil, err
×
222
                        }
×
223

224
                        return secret, nil
1✔
225
                }
226
                return nil, err
×
227
        }
228

229
        // found, return it
230
        return secret, nil
×
231
}
232

233
// 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
234
// if it is, it will generate a new cert and update the secret
235
func (r *WebhookController) CheckOrUpdateWebhookCertSecret(ctx context.Context, secret *corev1.Secret) (bool, error) {
1✔
236
        equal, err := compareCertOnDiskToSecret(r.certDir, secret)
1✔
237
        if err != nil {
1✔
238
                return false, err
×
239
        }
×
240

241
        // check if the secret is going to expire in the next 7 days or if the cert on disk is different from the secret
242
        if !equal || secret.Annotations[expirationAnnotationKey] < time.Now().Add(certRotationThreshold).Format(time.RFC3339) {
2✔
243
                // expired, generate a new cert
1✔
244
                webhookCert, err := generateCert(r.opts.ServiceName, r.namespace, certValidityDurationYear)
1✔
245
                if err != nil {
1✔
246
                        return false, err
×
247
                }
×
248

249
                // Write cert and key to disk if CertDir is set
250
                if r.certDir != "" {
2✔
251
                        _ = writeCertAndKey([]byte(webhookCert.TLSCert), []byte(webhookCert.TLSKey), r.certDir)
1✔
252
                }
1✔
253

254
                secret.Data["ca.crt"] = webhookCert.CABytes
1✔
255
                secret.Data["tls.crt"] = []byte(webhookCert.TLSCert)
1✔
256
                secret.Data["tls.key"] = []byte(webhookCert.TLSKey)
1✔
257
                secret.Annotations[expirationAnnotationKey] = webhookCert.Expiration.Format(time.RFC3339)
1✔
258

1✔
259
                return true, r.Update(ctx, secret)
1✔
260
        }
261

262
        return false, nil
×
263
}
264

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

×
270
        validatingChanged, err := r.updateValidatingWebhookConfiguration(ctx, caBundle)
×
271
        if err != nil {
×
272
                return false, err
×
273
        }
×
274

275
        mutatingChanged, err := r.updateMutatingWebhookConfiguration(ctx, caBundle)
×
276
        if err != nil {
×
277
                return false, err
×
278
        }
×
279

280
        return validatingChanged || mutatingChanged, nil
×
281
}
282

283
// updateValidatingWebhookConfiguration updates the ValidatingWebhookConfiguration with the provided CABundle
284
func (r *WebhookController) updateValidatingWebhookConfiguration(ctx context.Context, caBundle []byte) (bool, error) {
×
285
        existingValidating := &admissionregistrationv1.ValidatingWebhookConfiguration{}
×
286
        if err := r.Get(ctx, types.NamespacedName{Name: validatingWebhookConfigName}, existingValidating); err != nil {
×
287
                if errors.IsNotFound(err) {
×
288
                        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)
×
289
                }
×
290
                return false, fmt.Errorf("failed to get ValidatingWebhookConfiguration %q: %w", validatingWebhookConfigName, err)
×
291
        }
292

293
        needUpdate := false
×
294
        for i := range existingValidating.Webhooks {
×
295
                expectedRules := r.getValidatingWebhookRules(existingValidating.Webhooks[i].Name)
×
296
                if expectedRules == nil {
×
297
                        continue // Unknown webhook, skip
×
298
                }
299
                if validatingWebhookNeedsUpdate(&existingValidating.Webhooks[i], caBundle, expectedRules) {
×
300
                        needUpdate = true
×
301
                }
×
302
        }
303

304
        if needUpdate {
×
305
                if err := r.Update(ctx, existingValidating); err != nil {
×
306
                        return false, err
×
307
                }
×
308
                return true, nil
×
309
        }
310

311
        return false, nil
×
312
}
313

314
// updateMutatingWebhookConfiguration updates the MutatingWebhookConfiguration with the provided CABundle
315
func (r *WebhookController) updateMutatingWebhookConfiguration(ctx context.Context, caBundle []byte) (bool, error) {
×
316
        existingMutating := &admissionregistrationv1.MutatingWebhookConfiguration{}
×
317
        if err := r.Get(ctx, types.NamespacedName{Name: mutatingWebhookConfigName}, existingMutating); err != nil {
×
318
                if errors.IsNotFound(err) {
×
319
                        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)
×
320
                }
×
321
                return false, fmt.Errorf("failed to get MutatingWebhookConfiguration %q: %w", mutatingWebhookConfigName, err)
×
322
        }
323

324
        needUpdate := false
×
325
        for i := range existingMutating.Webhooks {
×
326
                expectedRules := r.getMutatingWebhookRules(existingMutating.Webhooks[i].Name)
×
327
                if expectedRules == nil {
×
328
                        continue // Unknown webhook, skip
×
329
                }
330
                if mutatingWebhookNeedsUpdate(&existingMutating.Webhooks[i], caBundle, expectedRules) {
×
331
                        needUpdate = true
×
332
                }
×
333
        }
334

335
        if needUpdate {
×
336
                if err := r.Update(ctx, existingMutating); err != nil {
×
337
                        return false, err
×
338
                }
×
339
                return true, nil
×
340
        }
341

342
        return false, nil
×
343
}
344

345
// getValidatingWebhookRules returns the expected rules for a validating webhook by name
346
func (r *WebhookController) getValidatingWebhookRules(webhookName string) []admissionregistrationv1.RuleWithOperations {
×
347
        switch webhookName {
×
348
        case skyhookValidatingWebhookName:
×
349
                return skyhookRules()
×
350
        case deploymentPolicyValidatingWebhookName:
×
351
                return deploymentPolicyValidatingRules()
×
352
        default:
×
353
                return nil
×
354
        }
355
}
356

357
// getMutatingWebhookRules returns the expected rules for a mutating webhook by name
358
func (r *WebhookController) getMutatingWebhookRules(webhookName string) []admissionregistrationv1.RuleWithOperations {
×
359
        switch webhookName {
×
360
        case skyhookMutatingWebhookName:
×
361
                return skyhookRules()
×
362
        case deploymentPolicyMutatingWebhookName:
×
363
                return deploymentPolicyMutatingRules()
×
364
        default:
×
365
                return nil
×
366
        }
367
}
368

369
// webhookValidatingWebhookConfiguration returns a new validating webhook configuration.
370
func webhookValidatingWebhookConfiguration(namespace, serviceName string, secret *corev1.Secret) *admissionregistrationv1.ValidatingWebhookConfiguration {
1✔
371
        conf := admissionregistrationv1.ValidatingWebhookConfiguration{
1✔
372
                ObjectMeta: metav1.ObjectMeta{
1✔
373
                        Name: validatingWebhookConfigName,
1✔
374
                },
1✔
375
                Webhooks: []admissionregistrationv1.ValidatingWebhook{
1✔
376
                        {
1✔
377
                                Name:                    skyhookValidatingWebhookName,
1✔
378
                                ClientConfig:            webhookClient(serviceName, namespace, skyhookValidatingPath, secret),
1✔
379
                                FailurePolicy:           ptr(admissionregistrationv1.Fail),
1✔
380
                                Rules:                   skyhookRules(),
1✔
381
                                SideEffects:             ptr(admissionregistrationv1.SideEffectClassNone),
1✔
382
                                AdmissionReviewVersions: []string{"v1"},
1✔
383
                        },
1✔
384
                        {
1✔
385
                                Name:                    deploymentPolicyValidatingWebhookName,
1✔
386
                                ClientConfig:            webhookClient(serviceName, namespace, deploymentPolicyValidatingPath, secret),
1✔
387
                                FailurePolicy:           ptr(admissionregistrationv1.Fail),
1✔
388
                                Rules:                   deploymentPolicyValidatingRules(),
1✔
389
                                SideEffects:             ptr(admissionregistrationv1.SideEffectClassNone),
1✔
390
                                AdmissionReviewVersions: []string{"v1"},
1✔
391
                        },
1✔
392
                },
1✔
393
        }
1✔
394

1✔
395
        return &conf
1✔
396
}
1✔
397

398
// webhookMutatingWebhookConfiguration returns a new mutating webhook configuration.
399
func webhookMutatingWebhookConfiguration(namespace, serviceName string, secret *corev1.Secret) *admissionregistrationv1.MutatingWebhookConfiguration {
1✔
400
        conf := admissionregistrationv1.MutatingWebhookConfiguration{
1✔
401
                ObjectMeta: metav1.ObjectMeta{
1✔
402
                        Name: mutatingWebhookConfigName,
1✔
403
                },
1✔
404
                Webhooks: []admissionregistrationv1.MutatingWebhook{
1✔
405
                        {
1✔
406
                                Name:                    skyhookMutatingWebhookName,
1✔
407
                                ClientConfig:            webhookClient(serviceName, namespace, skyhookMutatingPath, secret),
1✔
408
                                FailurePolicy:           ptr(admissionregistrationv1.Fail),
1✔
409
                                Rules:                   skyhookRules(),
1✔
410
                                SideEffects:             ptr(admissionregistrationv1.SideEffectClassNone),
1✔
411
                                AdmissionReviewVersions: []string{"v1"},
1✔
412
                        },
1✔
413
                        {
1✔
414
                                Name:                    deploymentPolicyMutatingWebhookName,
1✔
415
                                ClientConfig:            webhookClient(serviceName, namespace, deploymentPolicyMutatingPath, secret),
1✔
416
                                FailurePolicy:           ptr(admissionregistrationv1.Fail),
1✔
417
                                Rules:                   deploymentPolicyMutatingRules(),
1✔
418
                                SideEffects:             ptr(admissionregistrationv1.SideEffectClassNone),
1✔
419
                                AdmissionReviewVersions: []string{"v1"},
1✔
420
                        },
1✔
421
                },
1✔
422
        }
1✔
423

1✔
424
        return &conf
1✔
425
}
1✔
426

427
func compareMutatingWebhookConfigurations(a, b *admissionregistrationv1.MutatingWebhookConfiguration) bool {
1✔
428
        if len(a.Webhooks) != len(b.Webhooks) {
2✔
429
                return true
1✔
430
        }
1✔
431
        for i := range a.Webhooks {
2✔
432
                if !bytes.Equal(a.Webhooks[i].ClientConfig.CABundle, b.Webhooks[i].ClientConfig.CABundle) {
2✔
433
                        return true
1✔
434
                }
1✔
435
        }
436
        return false
1✔
437
}
438

439
func compareValidatingWebhookConfigurations(a, b *admissionregistrationv1.ValidatingWebhookConfiguration) bool {
1✔
440
        if len(a.Webhooks) != len(b.Webhooks) {
2✔
441
                return true
1✔
442
        }
1✔
443
        for i := range a.Webhooks {
2✔
444
                if !bytes.Equal(a.Webhooks[i].ClientConfig.CABundle, b.Webhooks[i].ClientConfig.CABundle) {
2✔
445
                        return true
1✔
446
                }
1✔
447
        }
448
        return false
1✔
449
}
450

451
func webhookClient(serviceName, namespace, path string, secret *corev1.Secret) admissionregistrationv1.WebhookClientConfig {
1✔
452
        return admissionregistrationv1.WebhookClientConfig{
1✔
453
                Service: &admissionregistrationv1.ServiceReference{
1✔
454
                        Name:      serviceName,
1✔
455
                        Namespace: namespace,
1✔
456
                        Path:      ptr(path),
1✔
457
                },
1✔
458
                CABundle: secret.Data["ca.crt"],
1✔
459
        }
1✔
460
}
1✔
461

462
func skyhookRules() []admissionregistrationv1.RuleWithOperations {
1✔
463
        return []admissionregistrationv1.RuleWithOperations{
1✔
464
                {
1✔
465
                        Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
1✔
466
                        Rule: admissionregistrationv1.Rule{
1✔
467
                                APIGroups:   []string{v1alpha1.GroupVersion.Group},
1✔
468
                                APIVersions: []string{v1alpha1.GroupVersion.Version},
1✔
469
                                Resources:   []string{"skyhooks"},
1✔
470
                        },
1✔
471
                },
1✔
472
        }
1✔
473
}
1✔
474

475
// deploymentPolicyValidatingRules adds the delete operation to the mutating webhook rules, otherwise they are the same
476
func deploymentPolicyValidatingRules() []admissionregistrationv1.RuleWithOperations {
1✔
477
        mutrules := deploymentPolicyMutatingRules()
1✔
478
        oprs := mutrules[0].Operations
1✔
479
        newops := make([]admissionregistrationv1.OperationType, len(oprs))
1✔
480
        copy(newops, oprs)
1✔
481
        newops = append(newops, admissionregistrationv1.Delete)
1✔
482
        mutrules[0].Operations = newops
1✔
483
        return mutrules
1✔
484
}
1✔
485

486
func deploymentPolicyMutatingRules() []admissionregistrationv1.RuleWithOperations {
1✔
487
        return []admissionregistrationv1.RuleWithOperations{
1✔
488
                {
1✔
489
                        Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
1✔
490
                        Rule: admissionregistrationv1.Rule{
1✔
491
                                APIGroups:   []string{v1alpha1.GroupVersion.Group},
1✔
492
                                APIVersions: []string{v1alpha1.GroupVersion.Version},
1✔
493
                                Resources:   []string{"deploymentpolicies"},
1✔
494
                        },
1✔
495
                },
1✔
496
        }
1✔
497
}
1✔
498

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

1✔
504
        // Check if CABundle needs to be set or updated (catches both empty and stale values)
1✔
505
        if !bytes.Equal(webhook.ClientConfig.CABundle, caBundle) {
2✔
506
                webhook.ClientConfig.CABundle = caBundle
1✔
507
                needUpdate = true
1✔
508
        }
1✔
509

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

516
        return needUpdate
1✔
517
}
518

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

1✔
523
        // Check if CABundle needs to be set or updated (catches both empty and stale values)
1✔
524
        if !bytes.Equal(webhook.ClientConfig.CABundle, caBundle) {
2✔
525
                webhook.ClientConfig.CABundle = caBundle
1✔
526
                needUpdate = true
1✔
527
        }
1✔
528

529
        // Check if rules need to be updated
530
        if !reflect.DeepEqual(webhook.Rules, expectedRules) {
1✔
531
                webhook.Rules = expectedRules
×
532
                needUpdate = true
×
533
        }
×
534

535
        return needUpdate
1✔
536
}
537

538
// WebhookSecretReadyzCheck is a readyz check for the webhook secret, if it does not exist, it will return an error
539
// if it exists, it will wait for the secret to be ready, this makes sure that we don't start the operator
540
// if the webhook secret is not ready
541
func (r *WebhookController) WebhookSecretReadyzCheck(_ *http.Request) error {
1✔
542
        secret := &corev1.Secret{}
1✔
543
        err := r.Client.Get(context.Background(), types.NamespacedName{
1✔
544
                Name:      r.opts.SecretName,
1✔
545
                Namespace: r.namespace,
1✔
546
        }, secret)
1✔
547

1✔
548
        if err != nil {
2✔
549
                return err
1✔
550
        }
1✔
551

552
        equal, err := compareCertOnDiskToSecret(r.certDir, secret)
1✔
553
        if err != nil {
1✔
554
                return err
×
555
        }
×
556

557
        if !equal {
2✔
558
                return fmt.Errorf("webhook secret is not ready")
1✔
559
        }
1✔
560

561
        // check for the webhook configurations
562
        validatingWebhookName := webhookValidatingWebhookConfiguration(r.namespace, r.opts.ServiceName, secret).GetName()
1✔
563
        validatingWebhookConfiguration := &admissionregistrationv1.ValidatingWebhookConfiguration{}
1✔
564
        err = r.Get(context.Background(), types.NamespacedName{Name: validatingWebhookName}, validatingWebhookConfiguration)
1✔
565
        if err != nil {
2✔
566
                if errors.IsNotFound(err) {
2✔
567
                        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✔
568
                }
1✔
569
                return err
×
570
        }
571

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

576
        mutatingWebhookConfiguration := webhookMutatingWebhookConfiguration(r.namespace, r.opts.ServiceName, secret)
1✔
577
        err = r.Get(context.Background(), types.NamespacedName{Name: mutatingWebhookConfiguration.Name}, mutatingWebhookConfiguration)
1✔
578
        if err != nil {
1✔
579
                if errors.IsNotFound(err) {
×
580
                        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)
×
581
                }
×
582
                return err
×
583
        }
584

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

589
        return nil
1✔
590
}
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

© 2026 Coveralls, Inc