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/http/admission.go b/src/internal/agent/http/admission.go index fbeeb3b983..de4a3b23d0 100644 --- a/src/internal/agent/http/admission.go +++ b/src/internal/agent/http/admission.go @@ -67,7 +67,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 } diff --git a/src/internal/agent/http/server.go b/src/internal/agent/http/server.go index 86ff5e828f..1473afb249 100644 --- a/src/internal/agent/http/server.go +++ b/src/internal/agent/http/server.go @@ -11,15 +11,16 @@ import ( "github.com/defenseunicorns/zarf/src/internal/agent/hooks" "github.com/defenseunicorns/zarf/src/pkg/message" + "github.com/defenseunicorns/zarf/src/types" "github.com/prometheus/client_golang/prometheus/promhttp" ) // NewAdmissionServer creates a http.Server for the mutating webhook admission handler. -func NewAdmissionServer(port string) *http.Server { +func NewAdmissionServer(port string, zarfState *types.ZarfState) *http.Server { message.Debugf("http.NewServer(%s)", port) // Instances hooks - podsMutation := hooks.NewPodMutationHook() + podsMutation := hooks.NewPodMutationHook(zarfState) fluxGitRepositoryMutation := hooks.NewGitRepositoryMutationHook() argocdApplicationMutation := hooks.NewApplicationMutationHook() argocdRepositoryMutation := hooks.NewRepositoryMutationHook() diff --git a/src/internal/agent/http/server_test.go b/src/internal/agent/http/server_test.go new file mode 100644 index 0000000000..88242dd1d0 --- /dev/null +++ b/src/internal/agent/http/server_test.go @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package http + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/defenseunicorns/zarf/src/config" + "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" +) + +// NewTestAdmissionServer creates an in-memory test server for the admission webhook. +// It does not bind to a port, so the port passed into NewAdmissionServer() does not matter. +func NewTestAdmissionServer(t *testing.T, zarfState *types.ZarfState) *httptest.Server { + t.Helper() + server := NewAdmissionServer("0", zarfState) + return httptest.NewServer(server.Handler) +} + +// createPodAdmissionRequest creates an admission request for a pod. +func createPodAdmissionRequest(t *testing.T, operation v1.Operation, pod *corev1.Pod) *v1.AdmissionRequest { + t.Helper() + raw, err := json.Marshal(pod) + require.NoError(t, err) + return &v1.AdmissionRequest{ + Operation: operation, + Kind: metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }, + Object: runtime.RawExtension{ + Raw: raw, + }, + } +} + +// sendAdmissionReview sends an admission review request to the server and returns the response. +func sendAdmissionReview(t *testing.T, server *httptest.Server, endpoint string, admissionReq *v1.AdmissionRequest) *v1.AdmissionResponse { + t.Helper() + + b, err := json.Marshal(&v1.AdmissionReview{ + Request: admissionReq, + }) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(b)) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + server.Config.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 +} + +// TestPodMutationWebhook tests the pod mutation webhook. +func TestPodMutationWebhook(t *testing.T) { + t.Parallel() + + server := NewTestAdmissionServer( + t, + &types.ZarfState{ + RegistryInfo: types.RegistryInfo{ + Address: "127.0.0.1:31999", + }, + }) + defer server.Close() + + 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{ + Name: "test-pod-mutation", + Namespace: "test-pod-mutation", + Labels: map[string]string{"should-be": "mutated"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "nginx", + Image: "nginx", + }, + }, + InitContainers: []corev1.Container{ + { + Name: "busybox", + Image: "busybox", + }, + }, + EphemeralContainers: []corev1.EphemeralContainer{ + { + EphemeralContainerCommon: corev1.EphemeralContainerCommon{ + Name: "alpine", + 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{ + Name: "nginx", + Namespace: "nginx", + Labels: map[string]string{"zarf-agent": "patched"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "nginx", + Image: "nginx", + }, + }, + }, + }), + expectedPatch: nil, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + resp := sendAdmissionReview(t, server, "/mutate/pod", tt.admissionReq) + if tt.expectedPatch != nil { + expectedPatchJSON, err := json.Marshal(tt.expectedPatch) + require.NoError(t, err) + require.JSONEq(t, string(expectedPatchJSON), string(resp.Patch)) + } else { + require.Nil(t, resp.Patch) + } + }) + } +} diff --git a/src/internal/agent/start.go b/src/internal/agent/start.go index 35b57e5fe2..51a8981715 100644 --- a/src/internal/agent/start.go +++ b/src/internal/agent/start.go @@ -13,6 +13,7 @@ import ( "github.com/defenseunicorns/zarf/src/config/lang" agentHttp "github.com/defenseunicorns/zarf/src/internal/agent/http" + "github.com/defenseunicorns/zarf/src/internal/agent/state" "github.com/defenseunicorns/zarf/src/pkg/message" ) @@ -30,7 +31,11 @@ const ( func StartWebhook() { message.Debug("agent.StartWebhook()") - startServer(agentHttp.NewAdmissionServer(httpPort)) + zarfState, err := state.GetZarfStateFromAgentPod() + if err != nil { + message.Fatal(err, err.Error()) + } + startServer(agentHttp.NewAdmissionServer(httpPort, zarfState)) } // StartHTTPProxy launches the zarf agent proxy in the cluster.