@@ -18,6 +18,7 @@ import (
1818 "context"
1919 "errors"
2020 "fmt"
21+ "sync"
2122
2223 apiextensionsv1alpha1 "github.com/openshift/api/apiextensions/v1alpha1"
2324 apiextensionshelpers "k8s.io/apiextensions-apiserver/pkg/apihelpers"
@@ -28,6 +29,7 @@ import (
2829 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2930 "k8s.io/apimachinery/pkg/runtime"
3031 "k8s.io/apimachinery/pkg/runtime/serializer"
32+ "k8s.io/apimachinery/pkg/types"
3133 ctrl "sigs.k8s.io/controller-runtime"
3234 "sigs.k8s.io/controller-runtime/pkg/builder"
3335 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -45,19 +47,40 @@ const (
4547 WebhookPrefix = "/compatibility-requirement-object-mutation/"
4648)
4749
50+ type structuralSchemaCacheKey struct {
51+ // The name of the CompatibilityRequirement.
52+ compatibilityRequirementName string
53+ // The API version of the schema we are caching.
54+ version string
55+ }
56+
57+ type structuralSchemaCacheValue struct {
58+ schema * structuralschema.Structural
59+
60+ // The UID of the CompatibilityRequirement.
61+ uid types.UID
62+
63+ // The generation of the CompatibilityRequirement.
64+ generation int64
65+ }
66+
4867// validator implements the admission.Handler to have a custom Handle function which is able to
4968// validate arbitrary objects against CompatibilityRequirements by leveraging unstructured.
5069type validator struct {
5170 client client.Reader
5271 decoder admission.Decoder
5372 universalDeserializer runtime.Decoder
73+
74+ structuralSchemaCacheLock sync.RWMutex
75+ structuralSchemaCache map [structuralSchemaCacheKey ]structuralSchemaCacheValue
5476}
5577
5678// NewValidator returns a partially initialized ObjectValidator.
5779func NewValidator () * validator {
5880 return & validator {
5981 // This decoder is only used to decode to unstructured and for CompatibilityRequirements.
60- decoder : admission .NewDecoder (runtime .NewScheme ()),
82+ decoder : admission .NewDecoder (runtime .NewScheme ()),
83+ structuralSchemaCache : make (map [structuralSchemaCacheKey ]structuralSchemaCacheValue ),
6184 }
6285}
6386
@@ -83,7 +106,7 @@ func (v *validator) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts
83106
84107// handleObjectPruning handles the pruning of an object.
85108func (v * validator ) handleObjectPruning (ctx context.Context , compatibilityRequirementName string , obj * unstructured.Unstructured ) error {
86- schema , err := v .getCopmatibilityRequirementStructuralSchema (ctx , compatibilityRequirementName , obj .GroupVersionKind ().Version )
109+ schema , err := v .getStructuralSchema (ctx , compatibilityRequirementName , obj .GroupVersionKind ().Version )
87110 if err != nil {
88111 return fmt .Errorf ("failed to get schema for CompatibilityRequirement %q: %w" , compatibilityRequirementName , err )
89112 }
@@ -93,12 +116,72 @@ func (v *validator) handleObjectPruning(ctx context.Context, compatibilityRequir
93116 return nil
94117}
95118
96- func (v * validator ) getCopmatibilityRequirementStructuralSchema (ctx context.Context , compatibilityRequirementName string , version string ) (* structuralschema.Structural , error ) {
119+ func (v * validator ) getStructuralSchema (ctx context.Context , compatibilityRequirementName string , version string ) (* structuralschema.Structural , error ) {
97120 compatibilityRequirement := & apiextensionsv1alpha1.CompatibilityRequirement {}
98121 if err := v .client .Get (ctx , client.ObjectKey {Name : compatibilityRequirementName }, compatibilityRequirement ); err != nil {
99122 return nil , fmt .Errorf ("failed to get CompatibilityRequirement %q: %w" , compatibilityRequirementName , err )
100123 }
101124
125+ cacheKey := getStructuralSchemaCacheKey (compatibilityRequirement , version )
126+
127+ schema , ok := v .getStructuralSchemaFromCache (compatibilityRequirement , cacheKey )
128+ if ok {
129+ return schema , nil
130+ }
131+
132+ v .structuralSchemaCacheLock .Lock ()
133+ defer v .structuralSchemaCacheLock .Unlock ()
134+
135+ // Check the cache again under the write lock in case another thread populated the cache
136+ // while we were waiting for the write lock.
137+ schemaValue , ok := v .structuralSchemaCache [cacheKey ]
138+ if ok && isCacheEntryValid (compatibilityRequirement , schemaValue ) {
139+ return schemaValue .schema , nil
140+ }
141+
142+ schema , err := v .getCompatibilityRequirementStructuralSchema (ctx , compatibilityRequirement , version )
143+ if err != nil {
144+ return nil , fmt .Errorf ("failed to get structural schema: %w" , err )
145+ }
146+
147+ v .storeStructuralSchemaInCache (compatibilityRequirement , cacheKey , schema )
148+
149+ return schema , nil
150+ }
151+
152+ func getStructuralSchemaCacheKey (compatibilityRequirement * apiextensionsv1alpha1.CompatibilityRequirement , version string ) structuralSchemaCacheKey {
153+ return structuralSchemaCacheKey {
154+ compatibilityRequirementName : compatibilityRequirement .Name ,
155+ version : version ,
156+ }
157+ }
158+
159+ func isCacheEntryValid (compatibilityRequirement * apiextensionsv1alpha1.CompatibilityRequirement , schema structuralSchemaCacheValue ) bool {
160+ return compatibilityRequirement .Generation == schema .generation && compatibilityRequirement .UID == schema .uid
161+ }
162+
163+ func (v * validator ) getStructuralSchemaFromCache (compatibilityRequirement * apiextensionsv1alpha1.CompatibilityRequirement , cacheKey structuralSchemaCacheKey ) (* structuralschema.Structural , bool ) {
164+ v .structuralSchemaCacheLock .RLock ()
165+ defer v .structuralSchemaCacheLock .RUnlock ()
166+
167+ schema , ok := v .structuralSchemaCache [cacheKey ]
168+ if ! ok || ! isCacheEntryValid (compatibilityRequirement , schema ) {
169+ return nil , false
170+ }
171+
172+ return schema .schema , true
173+ }
174+
175+ func (v * validator ) storeStructuralSchemaInCache (compatibilityRequirement * apiextensionsv1alpha1.CompatibilityRequirement , cacheKey structuralSchemaCacheKey , schema * structuralschema.Structural ) {
176+ // No locking here as we take the lock when constructing the new schema.
177+ v .structuralSchemaCache [cacheKey ] = structuralSchemaCacheValue {
178+ schema : schema ,
179+ uid : compatibilityRequirement .UID ,
180+ generation : compatibilityRequirement .Generation ,
181+ }
182+ }
183+
184+ func (v * validator ) getCompatibilityRequirementStructuralSchema (ctx context.Context , compatibilityRequirement * apiextensionsv1alpha1.CompatibilityRequirement , version string ) (* structuralschema.Structural , error ) {
102185 // Extract the CRD so we can use the schema.
103186 // Use a universal deserializer as it correctly handles YAML and JSON decoding based on the expected key formatting for CRDs.
104187 // N.B. DO NOT switch this to a YAML library - they do not correctly handle the OpenAPIV3Schema casing within the CRD version schema.
0 commit comments