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

NVIDIA / skyhook / 21881083720

10 Feb 2026 08:23PM UTC coverage: 80.713%. First build
21881083720

Pull #164

github

web-flow
Merge 8e570d367 into 97eea0078
Pull Request #164: fix: resolve webhook caBundle deadlock during helm upgrade

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

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