Skip to content

Commit 02fc47b

Browse files
committed
fix lint
1 parent d4802fe commit 02fc47b

File tree

2 files changed

+200
-115
lines changed

2 files changed

+200
-115
lines changed

.github/workflows/webhook-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
- master
1212

1313
jobs:
14-
setup-kind:
14+
test:
1515
runs-on: ubuntu-latest
1616
steps:
1717
- name: Checkout sources

internal/webhook/general/eviction_webhook.go

Lines changed: 199 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -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

4243
const (
@@ -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
61104
func (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

Comments
 (0)