From 6dbbb7dc1eb7bbf1e2544dc64f2543f9b82bd6a8 Mon Sep 17 00:00:00 2001 From: Lucas Rodriguez Date: Sun, 19 May 2024 22:51:28 -0500 Subject: [PATCH] works on my machine --- src/internal/agent/hooks/pods.go | 18 +-- src/internal/agent/hooks/pods_test.go | 143 ++++++++++++++++++ src/internal/agent/hooks/test_utils.go | 45 ++++++ .../{admission.go => admission/handler.go} | 24 +-- src/internal/agent/http/server.go | 11 +- 5 files changed, 219 insertions(+), 22 deletions(-) create mode 100644 src/internal/agent/hooks/pods_test.go create mode 100644 src/internal/agent/hooks/test_utils.go rename src/internal/agent/http/{admission.go => admission/handler.go} (80%) diff --git a/src/internal/agent/hooks/pods.go b/src/internal/agent/hooks/pods.go index a7b6911991..8c48383d6d 100644 --- a/src/internal/agent/hooks/pods.go +++ b/src/internal/agent/hooks/pods.go @@ -11,20 +11,24 @@ import ( "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/config/lang" "github.com/defenseunicorns/zarf/src/internal/agent/operations" - "github.com/defenseunicorns/zarf/src/internal/agent/state" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/transform" + "github.com/defenseunicorns/zarf/src/types" v1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" ) // NewPodMutationHook creates a new instance of pods mutation hook. -func NewPodMutationHook() operations.Hook { +func NewPodMutationHook(zarfState *types.ZarfState) operations.Hook { message.Debug("hooks.NewMutationHook()") return operations.Hook{ - Create: mutatePod, - Update: mutatePod, + Create: func(r *v1.AdmissionRequest) (*operations.Result, error) { + return mutatePod(r, zarfState) + }, + Update: func(r *v1.AdmissionRequest) (*operations.Result, error) { + return mutatePod(r, zarfState) + }, } } @@ -38,7 +42,7 @@ func parsePod(object []byte) (*corev1.Pod, error) { return &pod, nil } -func mutatePod(r *v1.AdmissionRequest) (*operations.Result, error) { +func mutatePod(r *v1.AdmissionRequest, zarfState *types.ZarfState) (*operations.Result, error) { message.Debugf("hooks.mutatePod()(*v1.AdmissionRequest) - %#v , %s/%s: %#v", r.Kind, r.Namespace, r.Name, r.Operation) var patchOperations []operations.PatchOperation @@ -59,10 +63,6 @@ func mutatePod(r *v1.AdmissionRequest) (*operations.Result, error) { zarfSecret := []corev1.LocalObjectReference{{Name: config.ZarfImagePullSecretName}} patchOperations = append(patchOperations, operations.ReplacePatchOperation("/spec/imagePullSecrets", zarfSecret)) - zarfState, err := state.GetZarfStateFromAgentPod() - if err != nil { - return nil, fmt.Errorf(lang.AgentErrGetState, err) - } containerRegistryURL := zarfState.RegistryInfo.Address // update the image host for each init container diff --git a/src/internal/agent/hooks/pods_test.go b/src/internal/agent/hooks/pods_test.go new file mode 100644 index 0000000000..9bd2b027f8 --- /dev/null +++ b/src/internal/agent/hooks/pods_test.go @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package hooks + +import ( + "encoding/json" + "testing" + + "github.com/defenseunicorns/zarf/src/config" + "github.com/defenseunicorns/zarf/src/internal/agent/http/admission" + "github.com/defenseunicorns/zarf/src/internal/agent/operations" + "github.com/defenseunicorns/zarf/src/types" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// createPodAdmissionRequest creates an admission request for a pod. +func createPodAdmissionRequest(t *testing.T, op v1.Operation, pod *corev1.Pod) *v1.AdmissionRequest { + t.Helper() + raw, err := json.Marshal(pod) + require.NoError(t, err) + return &v1.AdmissionRequest{ + Operation: op, + Object: runtime.RawExtension{ + Raw: raw, + }, + } +} + +// TestPodMutationWebhook tests the pod mutation webhook. +func TestPodMutationWebhook(t *testing.T) { + t.Parallel() + + handler := admission.NewHandler().Serve(NewPodMutationHook(&types.ZarfState{ + RegistryInfo: types.RegistryInfo{ + Address: "127.0.0.1:31999", + }, + })) + + tests := []struct { + name string + admissionReq *v1.AdmissionRequest + expectedPatch []operations.PatchOperation + }{ + { + name: "pod with label should be mutated", + admissionReq: createPodAdmissionRequest(t, v1.Create, &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"should-be": "mutated"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Image: "nginx"}}, + InitContainers: []corev1.Container{{Image: "busybox"}}, + EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Image: "alpine", + }, + }, + }, + }, + }), + expectedPatch: []operations.PatchOperation{ + operations.ReplacePatchOperation( + "/spec/imagePullSecrets", + []corev1.LocalObjectReference{{Name: config.ZarfImagePullSecretName}}, + ), + operations.ReplacePatchOperation( + "/spec/initContainers/0/image", + "127.0.0.1:31999/library/busybox:latest-zarf-2140033595", + ), + operations.ReplacePatchOperation( + "/spec/ephemeralContainers/0/image", + "127.0.0.1:31999/library/alpine:latest-zarf-1117969859", + ), + operations.ReplacePatchOperation( + "/spec/containers/0/image", + "127.0.0.1:31999/library/nginx:latest-zarf-3793515731", + ), + operations.ReplacePatchOperation( + "/metadata/labels/zarf-agent", + "patched", + ), + }, + }, + { + name: "pod with zarf-agent patched label", + admissionReq: createPodAdmissionRequest(t, v1.Create, &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"zarf-agent": "patched"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Image: "nginx"}}, + }, + }), + expectedPatch: nil, + }, + { + name: "pod with no labels", + admissionReq: createPodAdmissionRequest(t, v1.Create, &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Labels: nil, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Image: "nginx"}}, + }, + }), + expectedPatch: []operations.PatchOperation{ + operations.ReplacePatchOperation( + "/spec/imagePullSecrets", + []corev1.LocalObjectReference{{Name: config.ZarfImagePullSecretName}}, + ), + operations.ReplacePatchOperation( + "/spec/containers/0/image", + "127.0.0.1:31999/library/nginx:latest-zarf-3793515731", + ), + operations.AddPatchOperation( + "/metadata/labels", + map[string]string{"zarf-agent": "patched"}, + ), + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + resp := sendAdmissionRequest(t, tt.admissionReq, handler) + if tt.expectedPatch != nil { + expectedPatchJSON, err := json.Marshal(tt.expectedPatch) + require.NoError(t, err) + require.JSONEq(t, string(expectedPatchJSON), string(resp.Patch)) + } else { + require.Empty(t, string(resp.Patch)) + } + }) + } +} diff --git a/src/internal/agent/hooks/test_utils.go b/src/internal/agent/hooks/test_utils.go new file mode 100644 index 0000000000..48ee80e8db --- /dev/null +++ b/src/internal/agent/hooks/test_utils.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package hooks + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + v1 "k8s.io/api/admission/v1" +) + +// sendAdmissionRequest sends an admission request to the handler and returns the response. +func sendAdmissionRequest(t *testing.T, admissionReq *v1.AdmissionRequest, handler http.HandlerFunc) *v1.AdmissionResponse { + t.Helper() + + b, err := json.Marshal(&v1.AdmissionReview{ + Request: admissionReq, + }) + require.NoError(t, err) + + // Note: The URL ("/test") doesn't matter here because we are directly invoking the handler. + // The handler processes the request based on the HTTP method and body content, not the URL path. + req := httptest.NewRequest(http.MethodPost, "/test", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + require.Equal(t, http.StatusOK, rr.Code) + + var admissionReview v1.AdmissionReview + err = json.NewDecoder(rr.Body).Decode(&admissionReview) + require.NoError(t, err) + + resp := admissionReview.Response + require.NotNil(t, resp) + require.True(t, resp.Allowed) + + return resp +} diff --git a/src/internal/agent/http/admission.go b/src/internal/agent/http/admission/handler.go similarity index 80% rename from src/internal/agent/http/admission.go rename to src/internal/agent/http/admission/handler.go index fbeeb3b983..b4e3109af7 100644 --- a/src/internal/agent/http/admission.go +++ b/src/internal/agent/http/admission/handler.go @@ -1,8 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2021-Present The Zarf Authors -// Package http provides a http server for the webhook and proxy. -package http +// Package admission provides an HTTP handler for a Kubernetes admission webhook. +// It includes functionality to decode incoming admission requests, execute +// the corresponding operations, and return appropriate admission responses. +package admission import ( "encoding/json" @@ -19,20 +21,20 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer" ) -// admissionHandler represents the HTTP handler for an admission webhook. -type admissionHandler struct { +// Handler represents the HTTP handler for an admission webhook. +type Handler struct { decoder runtime.Decoder } -// newAdmissionHandler returns an instance of AdmissionHandler. -func newAdmissionHandler() *admissionHandler { - return &admissionHandler{ +// NewHandler returns a new admission Handler. +func NewHandler() *Handler { + return &Handler{ decoder: serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer(), } } -// Serve returns a http.HandlerFunc for an admission webhook. -func (h *admissionHandler) Serve(hook operations.Hook) http.HandlerFunc { +// Serve returns an http.HandlerFunc for an admission webhook. +func (h *Handler) Serve(hook operations.Hook) http.HandlerFunc { message.Debugf("http.Serve(%#v)", hook) return func(w http.ResponseWriter, r *http.Request) { message.Debugf("http.Serve()(writer, %#v)", r.URL) @@ -67,7 +69,7 @@ func (h *admissionHandler) Serve(hook operations.Hook) http.HandlerFunc { result, err := hook.Execute(review.Request) if err != nil { - message.WarnErr(err, lang.AgentErrBindHandler) + message.Warnf("%s: %s", lang.AgentErrBindHandler, err.Error()) w.WriteHeader(http.StatusInternalServerError) return } @@ -84,7 +86,7 @@ func (h *admissionHandler) Serve(hook operations.Hook) http.HandlerFunc { }, } - // set the patch operations for mutating admission + // Set the patch operations for mutating admission if len(result.PatchOps) > 0 { jsonPatchType := v1.PatchTypeJSONPatch patchBytes, err := json.Marshal(result.PatchOps) diff --git a/src/internal/agent/http/server.go b/src/internal/agent/http/server.go index 86ff5e828f..71f560852c 100644 --- a/src/internal/agent/http/server.go +++ b/src/internal/agent/http/server.go @@ -10,6 +10,8 @@ import ( "time" "github.com/defenseunicorns/zarf/src/internal/agent/hooks" + "github.com/defenseunicorns/zarf/src/internal/agent/http/admission" + "github.com/defenseunicorns/zarf/src/internal/agent/state" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -18,14 +20,19 @@ import ( func NewAdmissionServer(port string) *http.Server { message.Debugf("http.NewServer(%s)", port) + zarfState, err := state.GetZarfStateFromAgentPod() + if err != nil { + message.Fatal(err, err.Error()) + } + // Instances hooks - podsMutation := hooks.NewPodMutationHook() + podsMutation := hooks.NewPodMutationHook(zarfState) fluxGitRepositoryMutation := hooks.NewGitRepositoryMutationHook() argocdApplicationMutation := hooks.NewApplicationMutationHook() argocdRepositoryMutation := hooks.NewRepositoryMutationHook() // Routers - ah := newAdmissionHandler() + ah := admission.NewHandler() mux := http.NewServeMux() mux.Handle("/healthz", healthz()) mux.Handle("/mutate/pod", ah.Serve(podsMutation))