diff --git a/src/internal/agent/hooks/argocd-application.go b/src/internal/agent/hooks/argocd-application.go index 90a5b98744..0e037fbb00 100644 --- a/src/internal/agent/hooks/argocd-application.go +++ b/src/internal/agent/hooks/argocd-application.go @@ -17,6 +17,7 @@ import ( "github.com/defenseunicorns/zarf/src/pkg/transform" "github.com/defenseunicorns/zarf/src/types" v1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Application is a definition of an ArgoCD Application resource. @@ -29,6 +30,7 @@ import ( // For more information: https://argo-cd.readthedocs.io/en/stable/user-guide/import/ type Application struct { Spec ApplicationSpec `json:"spec"` + metav1.ObjectMeta } // ApplicationSpec represents desired application state. Contains link to repository with application definition. @@ -93,6 +95,8 @@ func mutateApplication(ctx context.Context, r *v1.AdmissionRequest, cluster *clu } } + patches = append(patches, getLabelPatch(app.Labels)) + return &operations.Result{ Allowed: true, PatchOps: patches, diff --git a/src/internal/agent/hooks/argocd-application_test.go b/src/internal/agent/hooks/argocd-application_test.go index 5d241dd571..6ef39b2730 100644 --- a/src/internal/agent/hooks/argocd-application_test.go +++ b/src/internal/agent/hooks/argocd-application_test.go @@ -69,6 +69,12 @@ func TestArgoAppWebhook(t *testing.T) { "/spec/sources/1/repoURL", "https://git-server.com/a-push-user/almonds-640159520", ), + operations.ReplacePatchOperation( + "/metadata/labels", + map[string]string{ + "zarf-agent": "patched", + }, + ), }, code: http.StatusOK, }, diff --git a/src/internal/agent/hooks/argocd-repository.go b/src/internal/agent/hooks/argocd-repository.go index 9e643414d7..53d96af44b 100644 --- a/src/internal/agent/hooks/argocd-repository.go +++ b/src/internal/agent/hooks/argocd-repository.go @@ -95,9 +95,12 @@ func mutateRepositorySecret(ctx context.Context, r *v1.AdmissionRequest, cluster message.Debugf("original url of (%s) got mutated to (%s)", repoCreds.URL, patchedURL) } + patches := populateArgoRepositoryPatchOperations(patchedURL, state.GitServer) + patches = append(patches, getLabelPatch(secret.Labels)) + return &operations.Result{ Allowed: true, - PatchOps: populateArgoRepositoryPatchOperations(patchedURL, state.GitServer), + PatchOps: patches, }, nil } diff --git a/src/internal/agent/hooks/argocd-repository_test.go b/src/internal/agent/hooks/argocd-repository_test.go index b84a3e8d07..4506b682e1 100644 --- a/src/internal/agent/hooks/argocd-repository_test.go +++ b/src/internal/agent/hooks/argocd-repository_test.go @@ -73,6 +73,13 @@ func TestArgoRepoWebhook(t *testing.T) { "/data/password", b64.StdEncoding.EncodeToString([]byte(state.GitServer.PullPassword)), ), + operations.ReplacePatchOperation( + "/metadata/labels", + map[string]string{ + "argocd.argoproj.io/secret-type": "repository", + "zarf-agent": "patched", + }, + ), }, code: http.StatusOK, }, @@ -103,6 +110,13 @@ func TestArgoRepoWebhook(t *testing.T) { "/data/password", b64.StdEncoding.EncodeToString([]byte(state.GitServer.PullPassword)), ), + operations.ReplacePatchOperation( + "/metadata/labels", + map[string]string{ + "argocd.argoproj.io/secret-type": "repository", + "zarf-agent": "patched", + }, + ), }, code: http.StatusOK, }, diff --git a/src/internal/agent/hooks/common.go b/src/internal/agent/hooks/common.go new file mode 100644 index 0000000000..ed1de69797 --- /dev/null +++ b/src/internal/agent/hooks/common.go @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package hooks contains the mutation hooks for the Zarf agent. +package hooks + +import "github.com/defenseunicorns/zarf/src/internal/agent/operations" + +func getLabelPatch(currLabels map[string]string) operations.PatchOperation { + if currLabels == nil { + currLabels = make(map[string]string) + } + currLabels["zarf-agent"] = "patched" + return operations.ReplacePatchOperation("/metadata/labels", currLabels) +} diff --git a/src/internal/agent/hooks/flux.go b/src/internal/agent/hooks/flux.go index 617b91901c..77382aecf3 100644 --- a/src/internal/agent/hooks/flux.go +++ b/src/internal/agent/hooks/flux.go @@ -85,6 +85,7 @@ func mutateGitRepo(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster // Patch updates of the repo spec patches = populatePatchOperations(patchedURL) + patches = append(patches, getLabelPatch(repo.Labels)) return &operations.Result{ Allowed: true, diff --git a/src/internal/agent/hooks/flux_test.go b/src/internal/agent/hooks/flux_test.go index cf56ac844a..a68cbfe591 100644 --- a/src/internal/agent/hooks/flux_test.go +++ b/src/internal/agent/hooks/flux_test.go @@ -64,6 +64,12 @@ func TestFluxMutationWebhook(t *testing.T) { "/spec/secretRef", fluxmeta.LocalObjectReference{Name: config.ZarfGitServerSecretName}, ), + operations.ReplacePatchOperation( + "/metadata/labels", + map[string]string{ + "zarf-agent": "patched", + }, + ), }, code: http.StatusOK, }, @@ -100,6 +106,12 @@ func TestFluxMutationWebhook(t *testing.T) { "/spec/secretRef", fluxmeta.LocalObjectReference{Name: config.ZarfGitServerSecretName}, ), + operations.ReplacePatchOperation( + "/metadata/labels", + map[string]string{ + "zarf-agent": "patched", + }, + ), }, code: http.StatusOK, }, diff --git a/src/internal/agent/hooks/pods.go b/src/internal/agent/hooks/pods.go index 658c585b12..299b841f31 100644 --- a/src/internal/agent/hooks/pods.go +++ b/src/internal/agent/hooks/pods.go @@ -64,11 +64,11 @@ func mutatePod(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Clu } registryURL := state.RegistryInfo.Address - var patchOperations []operations.PatchOperation + var patches []operations.PatchOperation // Add the zarf secret to the podspec zarfSecret := []corev1.LocalObjectReference{{Name: config.ZarfImagePullSecretName}} - patchOperations = append(patchOperations, operations.ReplacePatchOperation("/spec/imagePullSecrets", zarfSecret)) + patches = append(patches, operations.ReplacePatchOperation("/spec/imagePullSecrets", zarfSecret)) // update the image host for each init container for idx, container := range pod.Spec.InitContainers { @@ -78,7 +78,7 @@ func mutatePod(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Clu message.Warnf(lang.AgentErrImageSwap, container.Image) continue // Continue, because we might as well attempt to mutate the other containers for this pod } - patchOperations = append(patchOperations, operations.ReplacePatchOperation(path, replacement)) + patches = append(patches, operations.ReplacePatchOperation(path, replacement)) } // update the image host for each ephemeral container @@ -89,7 +89,7 @@ func mutatePod(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Clu message.Warnf(lang.AgentErrImageSwap, container.Image) continue // Continue, because we might as well attempt to mutate the other containers for this pod } - patchOperations = append(patchOperations, operations.ReplacePatchOperation(path, replacement)) + patches = append(patches, operations.ReplacePatchOperation(path, replacement)) } // update the image host for each normal container @@ -100,22 +100,13 @@ func mutatePod(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Clu message.Warnf(lang.AgentErrImageSwap, container.Image) continue // Continue, because we might as well attempt to mutate the other containers for this pod } - patchOperations = append(patchOperations, operations.ReplacePatchOperation(path, replacement)) + patches = append(patches, operations.ReplacePatchOperation(path, replacement)) } - // Add a label noting the zarf mutation - if pod.Labels == nil { - // If the labels path does not exist - create with map[string]string value - patchOperations = append(patchOperations, operations.AddPatchOperation("/metadata/labels", - map[string]string{ - "zarf-agent": "patched", - })) - } else { - patchOperations = append(patchOperations, operations.ReplacePatchOperation("/metadata/labels/zarf-agent", "patched")) - } + patches = append(patches, getLabelPatch(pod.Labels)) return &operations.Result{ Allowed: true, - PatchOps: patchOperations, + PatchOps: patches, }, nil } diff --git a/src/internal/agent/hooks/pods_test.go b/src/internal/agent/hooks/pods_test.go index 8eca704f59..3a41f9227c 100644 --- a/src/internal/agent/hooks/pods_test.go +++ b/src/internal/agent/hooks/pods_test.go @@ -46,7 +46,8 @@ func TestPodMutationWebhook(t *testing.T) { name: "pod with label should be mutated", admissionReq: createPodAdmissionRequest(t, v1.Create, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"should-be": "mutated"}, + Labels: map[string]string{"should-be": "mutated"}, + Annotations: map[string]string{"should-be": "mutated"}, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{Image: "nginx"}}, @@ -78,8 +79,11 @@ func TestPodMutationWebhook(t *testing.T) { "127.0.0.1:31999/library/nginx:latest-zarf-3793515731", ), operations.ReplacePatchOperation( - "/metadata/labels/zarf-agent", - "patched", + "/metadata/labels", + map[string]string{ + "zarf-agent": "patched", + "should-be": "mutated", + }, ), }, code: http.StatusOK, @@ -116,7 +120,7 @@ func TestPodMutationWebhook(t *testing.T) { "/spec/containers/0/image", "127.0.0.1:31999/library/nginx:latest-zarf-3793515731", ), - operations.AddPatchOperation( + operations.ReplacePatchOperation( "/metadata/labels", map[string]string{"zarf-agent": "patched"}, ), diff --git a/src/pkg/cluster/state.go b/src/pkg/cluster/state.go index 83af027217..fbcccf7989 100644 --- a/src/pkg/cluster/state.go +++ b/src/pkg/cluster/state.go @@ -64,6 +64,9 @@ func (c *Cluster) InitZarfState(ctx context.Context, initOptions types.ZarfInitO if err != nil { return err } + if len(nodeList.Items) == 0 { + return fmt.Errorf("cannot init Zarf state in empty cluster") + } namespaceList, err := c.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) if err != nil { return err @@ -90,6 +93,10 @@ func (c *Cluster) InitZarfState(ctx context.Context, initOptions types.ZarfInitO } // Mark existing namespaces as ignored for the zarf agent to prevent mutating resources we don't own. for _, namespace := range namespaces.Items { + // Skip Zarf namespace if it already exists. + if namespace.Name == ZarfNamespaceName { + continue + } spinner.Updatef("Marking existing namespace %s as ignored by Zarf Agent", namespace.Name) if namespace.Labels == nil { // Ensure label map exists to avoid nil panic @@ -107,8 +114,22 @@ func (c *Cluster) InitZarfState(ctx context.Context, initOptions types.ZarfInitO // Try to create the zarf namespace. spinner.Updatef("Creating the Zarf namespace") zarfNamespace := NewZarfManagedNamespace(ZarfNamespaceName) - if _, err := c.CreateNamespace(ctx, zarfNamespace); err != nil { - return fmt.Errorf("unable to create the zarf namespace: %w", err) + err = func() error { + _, err := c.Clientset.CoreV1().Namespaces().Create(ctx, zarfNamespace, metav1.CreateOptions{}) + if err != nil && !kerrors.IsAlreadyExists(err) { + return fmt.Errorf("unable to create the Zarf namespace: %w", err) + } + if err == nil { + return nil + } + _, err = c.Clientset.CoreV1().Namespaces().Update(ctx, zarfNamespace, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("unable to update the Zarf namespace: %w", err) + } + return nil + }() + if err != nil { + return err } // Wait up to 2 minutes for the default service account to be created. diff --git a/src/pkg/cluster/state_test.go b/src/pkg/cluster/state_test.go index 72f10cd873..e056aab5f4 100644 --- a/src/pkg/cluster/state_test.go +++ b/src/pkg/cluster/state_test.go @@ -5,16 +5,179 @@ package cluster import ( + "context" "fmt" "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" "github.com/defenseunicorns/pkg/helpers" + + "github.com/defenseunicorns/zarf/src/pkg/k8s" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/pki" "github.com/defenseunicorns/zarf/src/types" - "github.com/stretchr/testify/require" ) +func TestInitZarfState(t *testing.T) { + tests := []struct { + name string + initOpts types.ZarfInitOptions + nodes []corev1.Node + namespaces []corev1.Namespace + secrets []corev1.Secret + expectedErr string + }{ + { + name: "no nodes in cluster", + expectedErr: "cannot init Zarf state in empty cluster", + }, + { + name: "no namespaces exist", + initOpts: types.ZarfInitOptions{}, + nodes: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node", + }, + }, + }, + }, + { + name: "namespaces exists", + nodes: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node", + }, + }, + }, + namespaces: []corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-system", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + }, + }, + }, + }, + { + name: "Zarf namespace exists", + nodes: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node", + }, + }, + }, + namespaces: []corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: ZarfNamespaceName, + }, + }, + }, + }, + { + name: "Zarf state exists", + nodes: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node", + }, + }, + }, + namespaces: []corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: ZarfNamespaceName, + }, + }, + }, + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: ZarfNamespaceName, + Name: ZarfStateSecretName, + }, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + cs := fake.NewSimpleClientset() + for _, node := range tt.nodes { + _, err := cs.CoreV1().Nodes().Create(ctx, &node, metav1.CreateOptions{}) + require.NoError(t, err) + } + for _, namespace := range tt.namespaces { + _, err := cs.CoreV1().Namespaces().Create(ctx, &namespace, metav1.CreateOptions{}) + require.NoError(t, err) + } + for _, secret := range tt.secrets { + _, err := cs.CoreV1().Secrets(secret.ObjectMeta.Namespace).Create(ctx, &secret, metav1.CreateOptions{}) + require.NoError(t, err) + } + c := &Cluster{ + &k8s.K8s{ + Clientset: cs, + Log: func(string, ...any) {}, + }, + } + + // Create default service account in Zarf namespace + go func() { + for { + time.Sleep(1 * time.Second) + ns, err := cs.CoreV1().Namespaces().Get(ctx, ZarfNamespaceName, metav1.GetOptions{}) + if err != nil { + continue + } + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "default", + }, + } + cs.CoreV1().ServiceAccounts(ns.Name).Create(ctx, sa, metav1.CreateOptions{}) + break + } + }() + + err := c.InitZarfState(ctx, tt.initOpts) + if tt.expectedErr != "" { + require.EqualError(t, err, tt.expectedErr) + return + } + require.NoError(t, err) + zarfNs, err := cs.CoreV1().Namespaces().Get(ctx, ZarfNamespaceName, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, map[string]string{"app.kubernetes.io/managed-by": "zarf"}, zarfNs.Labels) + _, err = cs.CoreV1().Secrets(zarfNs.Name).Get(ctx, ZarfStateSecretName, metav1.GetOptions{}) + require.NoError(t, err) + for _, ns := range tt.namespaces { + if ns.Name == zarfNs.Name { + continue + } + ns, err := cs.CoreV1().Namespaces().Get(ctx, ns.Name, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, map[string]string{k8s.AgentLabel: "ignore"}, ns.Labels) + } + }) + } +} + // TODO: Change password gen method to make testing possible. func TestMergeZarfStateRegistry(t *testing.T) { t.Parallel()