Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: pod agent unit tests #2526

Merged
merged 33 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6dbbb7d
works on my machine
May 20, 2024
9599ad4
Merge branch 'main' into agent-unit-tests
AustinAbro321 May 20, 2024
2249473
Merge branch 'main' into agent-unit-tests
May 20, 2024
13bb0da
move sendAdmissionRequest() utils_test.go
May 20, 2024
db0f7f3
Merge branch 'agent-unit-tests' of https://github.com/defenseunicorns…
May 20, 2024
04eb292
load state from cluster
May 20, 2024
131f666
Merge branch 'main' into agent-unit-tests
May 20, 2024
c287ca2
adding clusterrole to agent
AustinAbro321 May 20, 2024
bcad43e
WIP flux tests
AustinAbro321 May 20, 2024
ddb08eb
WIP flux
May 20, 2024
0070938
tests passing
AustinAbro321 May 20, 2024
f213837
flux working
May 20, 2024
48304d4
use upstream type for flux test and delete comments
May 20, 2024
5f25efc
beter err handling
May 20, 2024
628ab54
moar stuff
May 20, 2024
e253431
ctx
May 20, 2024
a477285
add test cases for flux webhook
May 20, 2024
3de6435
add helpers for test setup
May 20, 2024
5218d10
argo working
May 21, 2024
edec371
copy argo app types from upstream
May 21, 2024
3ae4ec4
use upstream types for argo mutations
May 21, 2024
32f88b0
happy path argo test
AustinAbro321 May 21, 2024
1985894
happy path argo test
AustinAbro321 May 21, 2024
83b0bfc
separating PR
AustinAbro321 May 21, 2024
0940c58
pod pod pod
AustinAbro321 May 21, 2024
093bd36
refactor
AustinAbro321 May 21, 2024
73eb950
fix source
AustinAbro321 May 21, 2024
18554a9
Merge branch 'agent-unit-tests' into pod-agent-unit-tests
AustinAbro321 May 21, 2024
70644ad
test
AustinAbro321 May 21, 2024
554ecf5
messaging
AustinAbro321 May 21, 2024
c86b62f
uno reverse
AustinAbro321 May 21, 2024
8a167d5
setting namespace on role & binding
AustinAbro321 May 21, 2024
ba9f20a
rerun test
AustinAbro321 May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/zarf-agent/manifests/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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###"
Expand Down
12 changes: 12 additions & 0 deletions packages/zarf-agent/manifests/role.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: zarf-agent
namespace: zarf
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
13 changes: 13 additions & 0 deletions packages/zarf-agent/manifests/rolebinding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: zarf-agent-binding
AustinAbro321 marked this conversation as resolved.
Show resolved Hide resolved
namespace: zarf
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: zarf-agent
subjects:
- kind: ServiceAccount
name: zarf
namespace: zarf
5 changes: 5 additions & 0 deletions packages/zarf-agent/manifests/serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: zarf
namespace: zarf
3 changes: 3 additions & 0 deletions packages/zarf-agent/zarf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/config/lang/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
39 changes: 22 additions & 17 deletions src/internal/agent/hooks/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
},
}
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
149 changes: 149 additions & 0 deletions src/internal/agent/hooks/pods_test.go
Original file line number Diff line number Diff line change
@@ -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()
AustinAbro321 marked this conversation as resolved.
Show resolved Hide resolved
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))
}
})
}
}
70 changes: 70 additions & 0 deletions src/internal/agent/hooks/utils_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading