diff --git a/packages/zarf-agent/manifests/deployment.yaml b/packages/zarf-agent/manifests/deployment.yaml
index 2d4767ba56..a4113812f9 100644
--- a/packages/zarf-agent/manifests/deployment.yaml
+++ b/packages/zarf-agent/manifests/deployment.yaml
@@ -20,6 +20,7 @@ spec:
imagePullSecrets:
- name: private-registry
priorityClassName: system-node-critical
+ serviceAccountName: zarf
containers:
- name: server
image: "###ZARF_REGISTRY###/###ZARF_CONST_AGENT_IMAGE###:###ZARF_CONST_AGENT_IMAGE_TAG###"
diff --git a/packages/zarf-agent/manifests/role.yaml b/packages/zarf-agent/manifests/role.yaml
new file mode 100644
index 0000000000..c310ca19b1
--- /dev/null
+++ b/packages/zarf-agent/manifests/role.yaml
@@ -0,0 +1,12 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: zarf-agent
+ namespace: zarf
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - secrets
+ verbs:
+ - get
diff --git a/packages/zarf-agent/manifests/rolebinding.yaml b/packages/zarf-agent/manifests/rolebinding.yaml
new file mode 100644
index 0000000000..c46cae1b72
--- /dev/null
+++ b/packages/zarf-agent/manifests/rolebinding.yaml
@@ -0,0 +1,13 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: zarf-agent-binding
+ namespace: zarf
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: zarf-agent
+subjects:
+- kind: ServiceAccount
+ name: zarf
+ namespace: zarf
diff --git a/packages/zarf-agent/manifests/serviceaccount.yaml b/packages/zarf-agent/manifests/serviceaccount.yaml
new file mode 100644
index 0000000000..2f9b060094
--- /dev/null
+++ b/packages/zarf-agent/manifests/serviceaccount.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: zarf
+ namespace: zarf
diff --git a/packages/zarf-agent/zarf.yaml b/packages/zarf-agent/zarf.yaml
index 86799ff03f..32a6c34997 100644
--- a/packages/zarf-agent/zarf.yaml
+++ b/packages/zarf-agent/zarf.yaml
@@ -27,6 +27,9 @@ components:
- manifests/secret.yaml
- manifests/deployment.yaml
- manifests/webhook.yaml
+ - manifests/role.yaml
+ - manifests/rolebinding.yaml
+ - manifests/serviceaccount.yaml
actions:
onCreate:
before:
diff --git a/site/astro.config.ts b/site/astro.config.ts
index 6e54f621a0..ac3b8dec24 100644
--- a/site/astro.config.ts
+++ b/site/astro.config.ts
@@ -25,6 +25,19 @@ export default defineConfig({
integrations: [
starlight({
title: "Zarf",
+ head: [
+ {
+ tag: "script",
+ content: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
+ new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
+ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
+ 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
+ })(window,document,'script','dataLayer','G-N1XZ8ZXCWL');`,
+ },
+ ],
+ components: {
+ SkipLink: "./src/components/SkipLink.astro",
+ },
social: {
github: "https://github.com/defenseunicorns/zarf",
slack: "https://kubernetes.slack.com/archives/C03B6BJAUJ3",
diff --git a/site/src/components/SkipLink.astro b/site/src/components/SkipLink.astro
new file mode 100644
index 0000000000..7d863429e4
--- /dev/null
+++ b/site/src/components/SkipLink.astro
@@ -0,0 +1,18 @@
+---
+import type { Props } from '@astrojs/starlight/props'
+import Default from '@astrojs/starlight/components/SkipLink.astro'
+---
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/config/lang/english.go b/src/config/lang/english.go
index b72ffa1461..c5deeffc0c 100644
--- a/src/config/lang/english.go
+++ b/src/config/lang/english.go
@@ -664,7 +664,7 @@ const (
AgentErrBadRequest = "could not read request body: %s"
AgentErrBindHandler = "Unable to bind the webhook handler"
AgentErrCouldNotDeserializeReq = "could not deserialize request: %s"
- AgentErrGetState = "failed to load zarf state from file: %w"
+ AgentErrGetState = "failed to load zarf state: %w"
AgentErrHostnameMatch = "failed to complete hostname matching: %w"
AgentErrImageSwap = "Unable to swap the host for (%s)"
AgentErrInvalidMethod = "invalid method only POST requests are allowed"
diff --git a/src/internal/agent/hooks/pods.go b/src/internal/agent/hooks/pods.go
index a7b6911991..cefb024df0 100644
--- a/src/internal/agent/hooks/pods.go
+++ b/src/internal/agent/hooks/pods.go
@@ -5,13 +5,14 @@
package hooks
import (
+ "context"
"encoding/json"
"fmt"
"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/cluster"
"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/defenseunicorns/zarf/src/pkg/transform"
v1 "k8s.io/api/admission/v1"
@@ -20,11 +21,15 @@ import (
)
// NewPodMutationHook creates a new instance of pods mutation hook.
-func NewPodMutationHook() operations.Hook {
+func NewPodMutationHook(ctx context.Context, cluster *cluster.Cluster) operations.Hook {
message.Debug("hooks.NewMutationHook()")
return operations.Hook{
- Create: mutatePod,
- Update: mutatePod,
+ Create: func(r *v1.AdmissionRequest) (*operations.Result, error) {
+ return mutatePod(ctx, r, cluster)
+ },
+ Update: func(r *v1.AdmissionRequest) (*operations.Result, error) {
+ return mutatePod(ctx, r, cluster)
+ },
}
}
@@ -34,14 +39,12 @@ func parsePod(object []byte) (*corev1.Pod, error) {
if err := json.Unmarshal(object, &pod); err != nil {
return nil, err
}
-
return &pod, nil
}
-func mutatePod(r *v1.AdmissionRequest) (*operations.Result, error) {
+func mutatePod(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Cluster) (*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
pod, err := parsePod(r.Object.Raw)
if err != nil {
return &operations.Result{Msg: err.Error()}, nil
@@ -51,24 +54,26 @@ func mutatePod(r *v1.AdmissionRequest) (*operations.Result, error) {
// We've already played with this pod, just keep swimming 🐟
return &operations.Result{
Allowed: true,
- PatchOps: patchOperations,
+ PatchOps: []operations.PatchOperation{},
}, nil
}
- // Add the zarf secret to the podspec
- zarfSecret := []corev1.LocalObjectReference{{Name: config.ZarfImagePullSecretName}}
- patchOperations = append(patchOperations, operations.ReplacePatchOperation("/spec/imagePullSecrets", zarfSecret))
-
- zarfState, err := state.GetZarfStateFromAgentPod()
+ state, err := cluster.LoadZarfState(ctx)
if err != nil {
return nil, fmt.Errorf(lang.AgentErrGetState, err)
}
- containerRegistryURL := zarfState.RegistryInfo.Address
+ registryURL := state.RegistryInfo.Address
+
+ var patchOperations []operations.PatchOperation
+
+ // Add the zarf secret to the podspec
+ zarfSecret := []corev1.LocalObjectReference{{Name: config.ZarfImagePullSecretName}}
+ patchOperations = append(patchOperations, operations.ReplacePatchOperation("/spec/imagePullSecrets", zarfSecret))
// update the image host for each init container
for idx, container := range pod.Spec.InitContainers {
path := fmt.Sprintf("/spec/initContainers/%d/image", idx)
- replacement, err := transform.ImageTransformHost(containerRegistryURL, container.Image)
+ replacement, err := transform.ImageTransformHost(registryURL, container.Image)
if err != nil {
message.Warnf(lang.AgentErrImageSwap, container.Image)
continue // Continue, because we might as well attempt to mutate the other containers for this pod
@@ -79,7 +84,7 @@ func mutatePod(r *v1.AdmissionRequest) (*operations.Result, error) {
// update the image host for each ephemeral container
for idx, container := range pod.Spec.EphemeralContainers {
path := fmt.Sprintf("/spec/ephemeralContainers/%d/image", idx)
- replacement, err := transform.ImageTransformHost(containerRegistryURL, container.Image)
+ replacement, err := transform.ImageTransformHost(registryURL, container.Image)
if err != nil {
message.Warnf(lang.AgentErrImageSwap, container.Image)
continue // Continue, because we might as well attempt to mutate the other containers for this pod
@@ -90,7 +95,7 @@ func mutatePod(r *v1.AdmissionRequest) (*operations.Result, error) {
// update the image host for each normal container
for idx, container := range pod.Spec.Containers {
path := fmt.Sprintf("/spec/containers/%d/image", idx)
- replacement, err := transform.ImageTransformHost(containerRegistryURL, container.Image)
+ replacement, err := transform.ImageTransformHost(registryURL, container.Image)
if err != nil {
message.Warnf(lang.AgentErrImageSwap, container.Image)
continue // Continue, because we might as well attempt to mutate the other containers for this pod
diff --git a/src/internal/agent/hooks/pods_test.go b/src/internal/agent/hooks/pods_test.go
new file mode 100644
index 0000000000..eb3480b415
--- /dev/null
+++ b/src/internal/agent/hooks/pods_test.go
@@ -0,0 +1,149 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: 2021-Present The Zarf Authors
+
+package hooks
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "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"
+)
+
+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,
+ },
+ }
+}
+
+func TestPodMutationWebhook(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.Background()
+
+ state := &types.ZarfState{RegistryInfo: types.RegistryInfo{Address: "127.0.0.1:31999"}}
+ c := createTestClientWithZarfState(ctx, t, state)
+ handler := admission.NewHandler().Serve(NewPodMutationHook(ctx, c))
+
+ tests := []struct {
+ name string
+ admissionReq *v1.AdmissionRequest
+ expectedPatch []operations.PatchOperation
+ code int
+ }{
+ {
+ 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",
+ ),
+ },
+ code: http.StatusOK,
+ },
+ {
+ name: "pod with zarf-agent patched label should not be mutated",
+ 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,
+ code: http.StatusOK,
+ },
+ {
+ name: "pod with no labels should not error",
+ 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"},
+ ),
+ },
+ code: http.StatusOK,
+ },
+ }
+
+ for _, tt := range tests {
+ tt := tt
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ resp := sendAdmissionRequest(t, tt.admissionReq, handler, tt.code)
+ if tt.expectedPatch == nil {
+ require.Empty(t, string(resp.Patch))
+ } else {
+ expectedPatchJSON, err := json.Marshal(tt.expectedPatch)
+ require.NoError(t, err)
+ require.NotNil(t, resp)
+ require.True(t, resp.Allowed)
+ require.JSONEq(t, string(expectedPatchJSON), string(resp.Patch))
+ }
+ })
+ }
+}
diff --git a/src/internal/agent/hooks/utils_test.go b/src/internal/agent/hooks/utils_test.go
new file mode 100644
index 0000000000..909cd1f301
--- /dev/null
+++ b/src/internal/agent/hooks/utils_test.go
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: 2021-Present The Zarf Authors
+
+package hooks
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/defenseunicorns/zarf/src/pkg/cluster"
+ "github.com/defenseunicorns/zarf/src/pkg/k8s"
+ "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/client-go/kubernetes/fake"
+)
+
+func createTestClientWithZarfState(ctx context.Context, t *testing.T, state *types.ZarfState) *cluster.Cluster {
+ t.Helper()
+ c := &cluster.Cluster{K8s: &k8s.K8s{Clientset: fake.NewSimpleClientset()}}
+ stateData, err := json.Marshal(state)
+ require.NoError(t, err)
+
+ secret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: cluster.ZarfStateSecretName,
+ Namespace: cluster.ZarfNamespaceName,
+ },
+ Data: map[string][]byte{
+ cluster.ZarfStateDataKey: stateData,
+ },
+ }
+ _, err = c.Clientset.CoreV1().Secrets(cluster.ZarfNamespaceName).Create(ctx, secret, metav1.CreateOptions{})
+ require.NoError(t, err)
+ return c
+}
+
+// sendAdmissionRequest sends an admission request to the handler and returns the response.
+func sendAdmissionRequest(t *testing.T, admissionReq *v1.AdmissionRequest, handler http.HandlerFunc, code int) *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, code, rr.Code)
+
+ var admissionReview v1.AdmissionReview
+ if rr.Code == http.StatusOK {
+ err = json.NewDecoder(rr.Body).Decode(&admissionReview)
+ require.NoError(t, err)
+ }
+
+ return admissionReview.Response
+}
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..7b07b1445d 100644
--- a/src/internal/agent/http/server.go
+++ b/src/internal/agent/http/server.go
@@ -5,27 +5,37 @@
package http
import (
+ "context"
"fmt"
"net/http"
"time"
"github.com/defenseunicorns/zarf/src/internal/agent/hooks"
+ "github.com/defenseunicorns/zarf/src/internal/agent/http/admission"
+ "github.com/defenseunicorns/zarf/src/pkg/cluster"
"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// NewAdmissionServer creates a http.Server for the mutating webhook admission handler.
func NewAdmissionServer(port string) *http.Server {
- message.Debugf("http.NewServer(%s)", port)
+ message.Debugf("http.NewAdmissionServer(%s)", port)
+
+ c, err := cluster.NewCluster()
+ if err != nil {
+ message.Fatalf(err, err.Error())
+ }
+
+ ctx := context.Background()
// Instances hooks
- podsMutation := hooks.NewPodMutationHook()
+ podsMutation := hooks.NewPodMutationHook(ctx, c)
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))
diff --git a/src/pkg/cluster/zarf_test.go b/src/pkg/cluster/zarf_test.go
index b9451f3e72..0d40f58bfa 100644
--- a/src/pkg/cluster/zarf_test.go
+++ b/src/pkg/cluster/zarf_test.go
@@ -213,9 +213,6 @@ func TestGetDeployedPackage(t *testing.T) {
for _, p := range packages {
b, err := json.Marshal(p)
require.NoError(t, err)
- data := map[string][]byte{
- "data": b,
- }
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: strings.Join([]string{config.ZarfPackagePrefix, p.Name}, ""),
@@ -224,7 +221,9 @@ func TestGetDeployedPackage(t *testing.T) {
ZarfPackageInfoLabel: p.Name,
},
},
- Data: data,
+ Data: map[string][]byte{
+ "data": b,
+ },
}
c.Clientset.CoreV1().Secrets("zarf").Create(ctx, &secret, metav1.CreateOptions{})
actual, err := c.GetDeployedPackage(ctx, p.Name)