@@ -25,6 +25,8 @@ import (
2525 "strings"
2626 "time"
2727
28+ lib "github.com/aerospike/aerospike-management-lib"
29+
2830 "github.com/go-logr/logr"
2931 admissionv1 "k8s.io/api/admission/v1"
3032 corev1 "k8s.io/api/core/v1"
@@ -36,7 +38,6 @@ import (
3638 logf "sigs.k8s.io/controller-runtime/pkg/log"
3739
3840 asdbv1 "github.com/aerospike/aerospike-kubernetes-operator/v4/api/v1"
39- lib "github.com/aerospike/aerospike-management-lib"
4041)
4142
4243const (
@@ -54,167 +55,251 @@ type EvictionWebhook struct {
5455 Log logr.Logger
5556}
5657
58+ // isAerospikePod checks if the given pod is an Aerospike pod
59+ func (ew * EvictionWebhook ) isAerospikePod (pod * corev1.Pod ) bool {
60+ labels := pod .GetLabels ()
61+ if labels == nil {
62+ return false
63+ }
64+
65+ // Check for Aerospike-specific labels
66+ appLabel , hasAppLabel := labels [asdbv1 .AerospikeAppLabel ]
67+ _ , hasCustomResourceLabel := labels [asdbv1 .AerospikeCustomResourceLabel ]
68+
69+ // Pod is considered an Aerospike pod if it has both required labels
70+ return hasAppLabel && appLabel == asdbv1 .AerospikeAppLabelValue && hasCustomResourceLabel
71+ }
72+
73+ // setEvictionBlockedAnnotation sets an annotation on the pod indicating eviction was blocked
74+ func (ew * EvictionWebhook ) setEvictionBlockedAnnotation (ctx context.Context , pod * corev1.Pod ) error {
75+ // Create a patch to add the annotation
76+ patch := client .MergeFrom (pod .DeepCopy ())
77+
78+ if pod .Annotations == nil {
79+ pod .Annotations = make (map [string ]string )
80+ }
81+
82+ pod .Annotations [EvictionBlockedAnnotation ] = time .Now ().Format (time .RFC3339 )
83+
84+ return ew .Client .Patch (ctx , pod , patch )
85+ }
86+
87+ // SetupEvictionWebhookWithManager registers the eviction webhook with the manager
88+ func SetupEvictionWebhookWithManager (mgr ctrl.Manager ) error {
89+ ew := & EvictionWebhook {
90+ Client : mgr .GetClient (),
91+ Log : logf .Log .WithName ("eviction-webhook" ),
92+ }
93+
94+ // Register the webhook using the webhook server with direct HTTP handler
95+ webhookServer := mgr .GetWebhookServer ()
96+ webhookServer .Register ("/validate-eviction" , http .HandlerFunc (ew .Handle ))
97+
98+ return nil
99+ }
100+
57101//nolint:lll // for readability
58102// +kubebuilder:webhook:path=/validate-eviction,mutating=false,failurePolicy=ignore,sideEffects=None,groups="",resources=pods/eviction, verbs=create,versions=v1,name=veviction.kb.io,admissionReviewVersions={v1}
59103
60- // Handle handles eviction requests and blocks eviction of Aerospike pods
61104func (ew * EvictionWebhook ) Handle (w http.ResponseWriter , r * http.Request ) {
62- var enableSafePodEviction = "ENABLE_SAFE_POD_EVICTION"
63-
64105 log := ew .Log .WithValues ("method" , r .Method , "url" , r .URL .Path )
65106 log .Info ("Received eviction webhook request" )
66107
67- // Parse the admission review
68- var admissionReview admissionv1. AdmissionReview
69- if err := json . NewDecoder ( r . Body ). Decode ( & admissionReview ); err != nil {
70- log .Error (err , "Failed to decode admission review" )
71- http . Error (w , "Failed to decode admission review" , http . StatusBadRequest )
108+ // Parse admission review
109+ admissionReview , err := ew . parseAdmissionReview ( r )
110+ if err != nil {
111+ log .Error (err , "Failed to parse admission review" )
112+ ew . sendErrorResponse (w , http . StatusBadRequest , "Failed to parse admission review" )
72113
73114 return
74115 }
75116
76- // Create response
117+ // Create base response
77118 response := admissionv1.AdmissionResponse {
78119 UID : admissionReview .Request .UID ,
79120 Allowed : true ,
80121 }
81122
82- enable , found := os .LookupEnv (enableSafePodEviction )
83- if ! found || ! strings .EqualFold (enable , "true" ) {
84- log .V (1 ).Info ("Safe pod eviction is disabled via environment variable" , "env" , enableSafePodEviction )
123+ // Check if webhook is enabled
124+ if ! ew .isWebhookEnabled () {
125+ log .V (1 ).Info ("Safe pod eviction is disabled via environment variable" )
126+ ew .sendResponse (w , admissionReview , & response )
127+
85128 return
86129 }
87130
88- shouldEvaluate := true
131+ // Check namespace filtering
132+ if ! ew .shouldEvaluateNamespace (admissionReview .Request .Namespace ) {
133+ log .V (1 ).Info ("Namespace not in watch list, allowing eviction" , "namespace" , admissionReview .Request .Namespace )
134+ ew .sendResponse (w , admissionReview , & response )
135+
136+ return
137+ }
138+
139+ // Process eviction request
140+ evictionResult := ew .processEvictionRequest (admissionReview , log )
141+ if evictionResult != nil {
142+ response = * evictionResult
143+ }
144+
145+ // Send final response
146+ ew .sendResponse (w , admissionReview , & response )
147+ log .Info ("Eviction webhook request processed" , "allowed" , response .Allowed )
148+ }
149+
150+ // parseAdmissionReview parses the admission review from the request
151+ func (ew * EvictionWebhook ) parseAdmissionReview (r * http.Request ) (* admissionv1.AdmissionReview , error ) {
152+ var admissionReview admissionv1.AdmissionReview
153+ if err := json .NewDecoder (r .Body ).Decode (& admissionReview ); err != nil {
154+ return nil , fmt .Errorf ("failed to decode admission review: %w" , err )
155+ }
156+
157+ if admissionReview .Request == nil {
158+ return nil , fmt .Errorf ("admission review request is nil" )
159+ }
160+
161+ return & admissionReview , nil
162+ }
89163
164+ // isWebhookEnabled checks if the webhook is enabled via environment variable
165+ func (ew * EvictionWebhook ) isWebhookEnabled () bool {
166+ enable , found := os .LookupEnv ("ENABLE_SAFE_POD_EVICTION" )
167+ return found && strings .EqualFold (enable , "true" )
168+ }
169+
170+ // shouldEvaluateNamespace checks if the namespace should be evaluated
171+ func (ew * EvictionWebhook ) shouldEvaluateNamespace (namespace string ) bool {
90172 watchNs , err := asdbv1 .GetWatchNamespace ()
91173 if err != nil {
92- response .Allowed = false
93- response .Result = & metav1.Status {
94- Status : metav1 .StatusFailure ,
95- Message : fmt .Sprintf ("Failed to get watch namespaces: %v" , err ),
96- Code : http .StatusInternalServerError ,
97- }
174+ ew .Log .Error (err , "Failed to get watch namespaces" )
175+ return false
176+ }
98177
99- shouldEvaluate = false
100- } else if watchNs != "" {
101- nsList := strings .Split (watchNs , "," )
102- if ! lib .ContainsString (nsList , admissionReview .Request .Namespace ) {
103- shouldEvaluate = false
104- }
178+ if watchNs == "" {
179+ return true // No namespace filtering
105180 }
106181
107- if shouldEvaluate {
108- // Decode the eviction object
109- var eviction policyv1.Eviction
110- if err := json .Unmarshal (admissionReview .Request .Object .Raw , & eviction ); err != nil {
111- log .Error (err , "Failed to unmarshal eviction object" )
182+ nsList := strings .Split (watchNs , "," )
183+
184+ return lib .ContainsString (nsList , namespace )
185+ }
112186
113- response .Allowed = false
114- response .Result = & metav1.Status {
187+ // processEvictionRequest processes the eviction request and returns the response
188+ func (ew * EvictionWebhook ) processEvictionRequest (admissionReview * admissionv1.AdmissionReview ,
189+ log logr.Logger ) * admissionv1.AdmissionResponse {
190+ // Parse eviction object
191+ eviction , err := ew .parseEvictionObject (admissionReview .Request .Object .Raw )
192+ if err != nil {
193+ log .Error (err , "Failed to parse eviction object" )
194+
195+ return & admissionv1.AdmissionResponse {
196+ UID : admissionReview .Request .UID ,
197+ Allowed : false ,
198+ Result : & metav1.Status {
115199 Status : metav1 .StatusFailure ,
116- Message : fmt .Sprintf ("Failed to unmarshal eviction object: %v" , err ),
200+ Message : fmt .Sprintf ("Failed to parse eviction object: %v" , err ),
117201 Code : http .StatusBadRequest ,
118- }
119- } else {
120- // Get the pod that is being evicted
121- pod := & corev1.Pod {}
122- podKey := types.NamespacedName {
123- Name : eviction .Name ,
124- Namespace : admissionReview .Request .Namespace ,
125- }
126-
127- if err := ew .Client .Get (context .Background (), podKey , pod ); err != nil {
128- log .Error (err , "Failed to get pod for eviction" , "pod" , podKey )
129-
130- response .Allowed = false
131- response .Result = & metav1.Status {
132- Status : metav1 .StatusFailure ,
133- Message : fmt .Sprintf ("Failed to get pod %s/%s: %v" , admissionReview .Request .Namespace , eviction .Name , err ),
134- Code : http .StatusInternalServerError ,
135- }
136- } else {
137- // Check if this is an Aerospike pod
138- if ew .isAerospikePod (pod ) {
139- log .Info ("Blocking eviction of Aerospike pod" , "pod" , podKey )
140-
141- // Set annotation on the pod to indicate eviction was blocked
142- if err := ew .setEvictionBlockedAnnotation (context .Background (), pod ); err != nil {
143- log .Error (err , "Failed to set eviction blocked annotation" , "pod" , podKey )
144- // Continue with blocking eviction even if annotation fails
145- }
146-
147- response .Allowed = false
148- response .Result = & metav1.Status {
149- Status : metav1 .StatusFailure ,
150- Message : fmt .Sprintf ("Eviction of Aerospike pod %s/%s is blocked by admission webhook" ,
151- admissionReview .Request .Namespace , eviction .Name ),
152- Reason : EvictionBlockedReason ,
153- Code : http .StatusForbidden ,
154- }
155- } else {
156- log .V (1 ).Info ("Allowing eviction of non-Aerospike pod" , "pod" , podKey )
157- }
158- }
202+ },
159203 }
160204 }
161205
162- // Create response admission review
163- admissionReview .Response = & response
164- admissionReview .Request = nil // Clear request to reduce response size
206+ // Get pod information
207+ pod , err := ew .getPodForEviction (eviction , admissionReview .Request .Namespace )
208+ if err != nil {
209+ log .Error (err , "Failed to get pod for eviction" , "pod" , eviction .Name )
165210
166- // Send response
167- w .Header ().Set ("Content-Type" , "application/json" )
211+ return & admissionv1.AdmissionResponse {
212+ UID : admissionReview .Request .UID ,
213+ Allowed : false ,
214+ Result : & metav1.Status {
215+ Status : metav1 .StatusFailure ,
216+ Message : fmt .Sprintf ("Failed to get pod %s/%s: %v" , admissionReview .Request .Namespace , eviction .Name , err ),
217+ Code : http .StatusInternalServerError ,
218+ },
219+ }
220+ }
168221
169- if err := json .NewEncoder (w ).Encode (admissionReview ); err != nil {
170- log .Error (err , "Failed to encode admission review response" )
171- http .Error (w , "Failed to encode response" , http .StatusInternalServerError )
222+ // Check if this is an Aerospike pod
223+ if ! ew .isAerospikePod (pod ) {
224+ log .V (1 ).Info ("Allowing eviction of non-Aerospike pod" , "pod" , eviction .Name )
225+ return nil // Allow eviction
226+ }
172227
173- return
228+ // Block Aerospike pod eviction
229+ log .Info ("Blocking eviction of Aerospike pod" , "pod" , eviction .Name )
230+
231+ // Set annotation asynchronously (non-blocking)
232+ go ew .setEvictionBlockedAnnotationAsync (pod )
233+
234+ return & admissionv1.AdmissionResponse {
235+ UID : admissionReview .Request .UID ,
236+ Allowed : false ,
237+ Result : & metav1.Status {
238+ Status : metav1 .StatusFailure ,
239+ Message : fmt .Sprintf ("Eviction of Aerospike pod %s/%s is blocked by admission webhook" ,
240+ admissionReview .Request .Namespace , eviction .Name ),
241+ Reason : EvictionBlockedReason ,
242+ Code : http .StatusForbidden ,
243+ },
174244 }
245+ }
175246
176- log .Info ("Eviction webhook request processed" , "allowed" , response .Allowed )
247+ // parseEvictionObject parses the eviction object from raw bytes
248+ func (ew * EvictionWebhook ) parseEvictionObject (raw []byte ) (* policyv1.Eviction , error ) {
249+ var eviction policyv1.Eviction
250+ if err := json .Unmarshal (raw , & eviction ); err != nil {
251+ return nil , fmt .Errorf ("failed to unmarshal eviction object: %w" , err )
252+ }
253+
254+ return & eviction , nil
177255}
178256
179- // isAerospikePod checks if the given pod is an Aerospike pod
180- func (ew * EvictionWebhook ) isAerospikePod ( pod * corev1.Pod ) bool {
181- labels := pod . GetLabels ()
182- if labels == nil {
183- return false
257+ // getPodForEviction retrieves the pod that is being evicted
258+ func (ew * EvictionWebhook ) getPodForEviction ( eviction * policyv1. Eviction , namespace string ) ( * corev1.Pod , error ) {
259+ podKey := types. NamespacedName {
260+ Name : eviction . Name ,
261+ Namespace : namespace ,
184262 }
185263
186- // Check for Aerospike-specific labels
187- appLabel , hasAppLabel := labels [asdbv1 .AerospikeAppLabel ]
188- _ , hasCustomResourceLabel := labels [asdbv1 .AerospikeCustomResourceLabel ]
264+ pod := & corev1.Pod {}
189265
190- // Pod is considered an Aerospike pod if it has both required labels
191- return hasAppLabel && appLabel == asdbv1 .AerospikeAppLabelValue && hasCustomResourceLabel
266+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
267+ defer cancel ()
268+
269+ if err := ew .Client .Get (ctx , podKey , pod ); err != nil {
270+ return nil , fmt .Errorf ("failed to get pod %s: %w" , podKey .String (), err )
271+ }
272+
273+ return pod , nil
192274}
193275
194- // setEvictionBlockedAnnotation sets an annotation on the pod indicating eviction was blocked
195- func (ew * EvictionWebhook ) setEvictionBlockedAnnotation ( ctx context. Context , pod * corev1.Pod ) error {
196- // Create a patch to add the annotation
197- patch := client . MergeFrom ( pod . DeepCopy () )
276+ // setEvictionBlockedAnnotationAsync sets the eviction blocked annotation asynchronously
277+ func (ew * EvictionWebhook ) setEvictionBlockedAnnotationAsync ( pod * corev1.Pod ) {
278+ ctx , cancel := context . WithTimeout ( context . Background (), 2 * time . Second )
279+ defer cancel ( )
198280
199- if pod .Annotations == nil {
200- pod .Annotations = make (map [string ]string )
281+ if err := ew .setEvictionBlockedAnnotation (ctx , pod ); err != nil {
282+ ew .Log .V (1 ).Info ("Failed to set eviction blocked annotation" ,
283+ "pod" , pod .Name , "error" , err )
201284 }
285+ }
202286
203- pod .Annotations [EvictionBlockedAnnotation ] = time .Now ().Format (time .RFC3339 )
287+ // sendResponse sends the admission review response
288+ func (ew * EvictionWebhook ) sendResponse (w http.ResponseWriter , admissionReview * admissionv1.AdmissionReview ,
289+ response * admissionv1.AdmissionResponse ) {
290+ admissionReview .Response = response
291+ admissionReview .Request = nil // Clear request to reduce response size
204292
205- return ew .Client .Patch (ctx , pod , patch )
206- }
293+ w .Header ().Set ("Content-Type" , "application/json" )
207294
208- // SetupEvictionWebhookWithManager registers the eviction webhook with the manager
209- func SetupEvictionWebhookWithManager (mgr ctrl.Manager ) error {
210- ew := & EvictionWebhook {
211- Client : mgr .GetClient (),
212- Log : logf .Log .WithName ("eviction-webhook" ),
295+ if err := json .NewEncoder (w ).Encode (admissionReview ); err != nil {
296+ ew .Log .Error (err , "Failed to encode admission review response" )
297+ http .Error (w , "Failed to encode response" , http .StatusInternalServerError )
213298 }
299+ }
214300
215- // Register the webhook using the webhook server with direct HTTP handler
216- webhookServer := mgr .GetWebhookServer ()
217- webhookServer .Register ("/validate-eviction" , http .HandlerFunc (ew .Handle ))
218-
219- return nil
301+ // sendErrorResponse sends an error response
302+ func (ew * EvictionWebhook ) sendErrorResponse (w http.ResponseWriter , statusCode int , message string ) {
303+ w .Header ().Set ("Content-Type" , "application/json" )
304+ http .Error (w , message , statusCode )
220305}
0 commit comments