44 "context"
55 "encoding/json"
66 "fmt"
7+ "os"
8+ "strings"
9+ "time"
710
811 apierrors "k8s.io/apimachinery/pkg/api/errors"
912 "k8s.io/apimachinery/pkg/runtime"
@@ -12,13 +15,15 @@ import (
1215 "sigs.k8s.io/controller-runtime/pkg/webhook"
1316 "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
1417
18+ "github.com/google/go-containerregistry/pkg/authn"
1519 "github.com/google/go-containerregistry/pkg/name"
20+ gcrremote "github.com/google/go-containerregistry/pkg/v1/remote"
1621
1722 "github.com/redhat-et/GKM/pkg/utils"
1823)
1924
2025var (
21- gkmcachelog = logf .Log .WithName ("webhook-ns" )
26+ gkmcacheLog = logf .Log .WithName ("webhook-ns" )
2227 _ webhook.CustomDefaulter = & GKMCache {}
2328 _ webhook.CustomValidator = & GKMCache {}
2429)
@@ -41,39 +46,58 @@ func (w *GKMCache) SetupWebhookWithManager(mgr ctrl.Manager) error {
4146
4247// Default implements the defaulting logic (mutating webhook)
4348func (w * GKMCache ) Default (ctx context.Context , obj runtime.Object ) error {
44- gkmcachelog .V (1 ).Info ("Mutating Webhook called" , "object" , obj )
49+ gkmcacheLog .V (1 ).Info ("Mutating Webhook called" , "object" , obj )
4550
4651 cache , ok := obj .(* GKMCache )
4752 if ! ok {
4853 return apierrors .NewBadRequest (fmt .Sprintf ("expected GKMCache, got %T" , obj ))
4954 }
50- gkmcachelog .V (1 ).Info ("Decoded GKMCache object" , "name" , cache .Name , "namespace" , cache .Namespace )
55+ gkmcacheLog .V (1 ).Info ("Decoded GKMCache object" , "name" , cache .Name , "namespace" , cache .Namespace )
5156
5257 if cache .Annotations == nil {
5358 cache .Annotations = map [string ]string {}
5459 }
5560
5661 if cache .Spec .Image == "" {
57- gkmcachelog .Info ("spec.image is empty, skipping" )
62+ gkmcacheLog .Info ("spec.image is empty, skipping" )
5863 return nil
5964 }
6065
61- // First check if the image already contains a digest (e.g., from Kyverno mutation)
66+ // Resolve & verify image -> digest
67+ cctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
68+ defer cancel ()
69+
70+ kyvernoEnabled := isKyvernoVerificationEnabled ()
6271 var digest string
63- if extractedDigest := extractDigestFromImage (cache .Spec .Image ); extractedDigest != "" {
64- gkmcachelog .Info ("Image already contains digest (likely from Kyverno)" , "image" , cache .Spec .Image , "digest" , extractedDigest )
65- digest = extractedDigest
66- }
67- resolvedDigest , digestFound := cache .Annotations [utils .GMKCacheAnnotationResolvedDigest ]
68- if digestFound {
69- // Digest hasn't changed so just return
70- if digest == resolvedDigest {
71- return nil
72+ var err error
73+ if kyvernoEnabled {
74+ // First check if the image already contains a digest (e.g., from Kyverno mutation)
75+ if extractedDigest := extractDigestFromImage (cache .Spec .Image ); extractedDigest != "" {
76+ gkmcacheLog .Info ("Image already contains digest (likely from Kyverno)" , "image" , cache .Spec .Image , "digest" , extractedDigest )
77+ digest = extractedDigest
78+ }
79+ resolvedDigest , digestFound := cache .Annotations [utils .GMKCacheAnnotationResolvedDigest ]
80+ if digestFound && digest != "" {
81+ // Digest hasn't changed so just return
82+ if digest == resolvedDigest {
83+ return nil
84+ }
85+ }
86+ } else {
87+ gkmcacheLog .V (1 ).Info ("Resolving image digest (Kyverno verification disabled)" , "image" , cache .Spec .Image )
88+ digest , err = resolveImageDigest (cctx , cache .Spec .Image )
89+ if err != nil {
90+ gkmcacheLog .Error (err , "failed to resolve image digest" )
91+ return apierrors .NewBadRequest (fmt .Sprintf (
92+ "image digest resolution failed for '%s': %s" ,
93+ cache .Spec .Image , err .Error (),
94+ ))
7295 }
7396 }
97+
7498 cache .Annotations [utils .GMKCacheAnnotationResolvedDigest ] = digest
7599
76- gkmcachelog .Info ("added/updated resolvedDigest" , "image" , cache .Spec .Image , "digest" , digest )
100+ gkmcacheLog .Info ("added/updated resolvedDigest" , "image" , cache .Spec .Image , "digest" , digest )
77101 return nil
78102}
79103
@@ -96,21 +120,23 @@ func (w *GKMCache) ValidateCreate(ctx context.Context, obj runtime.Object) (admi
96120 return nil , fmt .Errorf ("%s must be set by mutating webhook" , utils .GMKCacheAnnotationResolvedDigest )
97121 }
98122
99- if _ , exists := cache .Annotations [utils .KyvernoVerifyImagesAnnotation ]; ! exists {
100- return nil , fmt .Errorf ("%s must be set by kyverno" , utils .KyvernoVerifyImagesAnnotation )
101- }
123+ if isKyvernoVerificationEnabled () {
124+ if _ , exists := cache .Annotations [utils .KyvernoVerifyImagesAnnotation ]; ! exists {
125+ return nil , fmt .Errorf ("%s must be set by kyverno" , utils .KyvernoVerifyImagesAnnotation )
126+ }
102127
103- // Check Kyverno verification status if present
104- if err := verifyKyvernoAnnotation (cache .Annotations ); err != nil {
105- return nil , fmt .Errorf ("kyverno verification failed: %w" , err )
128+ // Check Kyverno verification status if present
129+ if err := verifyKyvernoAnnotation (cache .Annotations ); err != nil {
130+ return nil , fmt .Errorf ("kyverno verification failed: %w" , err )
131+ }
106132 }
107133
108134 return nil , nil
109135}
110136
111137// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
112138func (w * GKMCache ) ValidateUpdate (ctx context.Context , oldObj , newObj runtime.Object ) (admission.Warnings , error ) {
113- gkmcachelog .V (1 ).Info ("Validating Webhook called" , "oldObj" , oldObj , "newObj" , newObj )
139+ gkmcacheLog .V (1 ).Info ("Validating Webhook called" , "oldObj" , oldObj , "newObj" , newObj )
114140 oldCache , ok1 := oldObj .(* GKMCache )
115141 newCache , ok2 := newObj .(* GKMCache )
116142 if ! ok1 || ! ok2 {
@@ -139,6 +165,17 @@ func (w *GKMCache) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Ob
139165 return nil , fmt .Errorf ("%s must be set by mutating webhook when spec.image changes" , utils .GMKCacheAnnotationResolvedDigest )
140166 }
141167
168+ // Validate Kyverno verification if enabled
169+ if isKyvernoVerificationEnabled () {
170+ if _ , exists := newCache .Annotations [utils .KyvernoVerifyImagesAnnotation ]; ! exists {
171+ return nil , fmt .Errorf ("%s must be set by kyverno" , utils .KyvernoVerifyImagesAnnotation )
172+ }
173+
174+ if err := verifyKyvernoAnnotation (newCache .Annotations ); err != nil {
175+ return nil , fmt .Errorf ("kyverno verification failed: %w" , err )
176+ }
177+ }
178+
142179 return nil , nil
143180}
144181
@@ -149,7 +186,7 @@ func (w *GKMCache) ValidateDelete(_ context.Context, obj runtime.Object) (admiss
149186 return nil , apierrors .NewBadRequest (fmt .Sprintf ("expected GKMCache, got %T" , obj ))
150187 }
151188
152- clustergkmcacheLog .Info ("validating GKMCache delete" , "name" , cache .Name )
189+ gkmcacheLog .Info ("validating GKMCache delete" , "name" , cache .Name )
153190
154191 // Add delete validation logic here if needed.
155192 return nil , nil
@@ -198,3 +235,51 @@ func verifyKyvernoAnnotation(annotations map[string]string) error {
198235
199236 return nil
200237}
238+
239+ // isKyvernoVerificationEnabled checks if Kyverno verification is enabled.
240+ // It reads from the KYVERNO_VERIFICATION_ENABLED environment variable.
241+ // Defaults to true (enabled) if not set or invalid.
242+ func isKyvernoVerificationEnabled () bool {
243+ envValue := os .Getenv (utils .EnvKyvernoEnabled )
244+ if envValue == "" {
245+ // Default to enabled
246+ return true
247+ }
248+ // Parse the value - accept "true", "1", "yes" as enabled
249+ // Everything else (including "false", "0", "no") is disabled
250+ switch strings .ToLower (envValue ) {
251+ case "true" , "1" , "yes" :
252+ return true
253+ case "false" , "0" , "no" :
254+ return false
255+ default :
256+ // Invalid value, default to enabled and log a warning
257+ gkmcacheLog .Info ("Invalid value for KYVERNO_VERIFICATION_ENABLED, defaulting to enabled" , "value" , envValue )
258+ return true
259+ }
260+ }
261+
262+ // resolveImageDigest resolves an image reference to its digest without verifying signatures.
263+ // This is used when Kyverno verification is disabled (development/testing mode).
264+ // It returns the image digest string (sha256:...) if successful.
265+ func resolveImageDigest (ctx context.Context , imageRef string ) (string , error ) {
266+ // Parse the image reference (tag or digest).
267+ ref , err := name .ParseReference (imageRef )
268+ if err != nil {
269+ return "" , fmt .Errorf ("parse image reference: %w" , err )
270+ }
271+
272+ // Registry access options (authn.DefaultKeychain covers most cases).
273+ remoteOpts := []gcrremote.Option {
274+ gcrremote .WithAuthFromKeychain (authn .DefaultKeychain ),
275+ gcrremote .WithContext (ctx ),
276+ }
277+
278+ // Get the image descriptor to retrieve the digest
279+ descriptor , err := gcrremote .Get (ref , remoteOpts ... )
280+ if err != nil {
281+ return "" , fmt .Errorf ("fetch image descriptor: %w" , err )
282+ }
283+
284+ return descriptor .Digest .String (), nil
285+ }
0 commit comments