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: agent flux unit test #2528

Merged
merged 49 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
6dbbb7d
works on my machine
lucasrod16 May 20, 2024
9599ad4
Merge branch 'main' into agent-unit-tests
AustinAbro321 May 20, 2024
2249473
Merge branch 'main' into agent-unit-tests
lucasrod16 May 20, 2024
13bb0da
move sendAdmissionRequest() utils_test.go
lucasrod16 May 20, 2024
db0f7f3
Merge branch 'agent-unit-tests' of https://github.com/defenseunicorns…
lucasrod16 May 20, 2024
04eb292
load state from cluster
lucasrod16 May 20, 2024
131f666
Merge branch 'main' into agent-unit-tests
lucasrod16 May 20, 2024
c287ca2
adding clusterrole to agent
AustinAbro321 May 20, 2024
bcad43e
WIP flux tests
AustinAbro321 May 20, 2024
ddb08eb
WIP flux
lucasrod16 May 20, 2024
0070938
tests passing
AustinAbro321 May 20, 2024
f213837
flux working
lucasrod16 May 20, 2024
48304d4
use upstream type for flux test and delete comments
lucasrod16 May 20, 2024
5f25efc
beter err handling
lucasrod16 May 20, 2024
628ab54
moar stuff
lucasrod16 May 20, 2024
e253431
ctx
lucasrod16 May 20, 2024
a477285
add test cases for flux webhook
lucasrod16 May 20, 2024
3de6435
add helpers for test setup
lucasrod16 May 20, 2024
5218d10
argo working
lucasrod16 May 21, 2024
edec371
copy argo app types from upstream
lucasrod16 May 21, 2024
3ae4ec4
use upstream types for argo mutations
lucasrod16 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
0202eff
extrememlely very much wip wip wip
AustinAbro321 May 21, 2024
053d5da
unit tests passing
AustinAbro321 May 21, 2024
6847125
refactor tests
AustinAbro321 May 21, 2024
6644e6d
merge
AustinAbro321 May 21, 2024
1aa5b07
changing error we return
AustinAbro321 May 22, 2024
63452c3
simplify
AustinAbro321 May 22, 2024
ad32444
cleaning up test
AustinAbro321 May 22, 2024
e6bb0cc
Merge branch 'main' into flux-agent-unit-tests
AustinAbro321 May 22, 2024
d68265d
adding admission response
AustinAbro321 May 22, 2024
637096f
Merge branch 'flux-agent-unit-tests' of github.com:defenseunicorns/za…
AustinAbro321 May 22, 2024
8244894
admission metadata is required? :/
AustinAbro321 May 22, 2024
4e703df
Merge branch 'main' into flux-agent-unit-tests
AustinAbro321 May 22, 2024
4a4b25e
Merge branch 'main' into flux-agent-unit-tests
AustinAbro321 May 22, 2024
12d8528
go mod
AustinAbro321 May 22, 2024
4c27af5
Merge branch 'main' into flux-agent-unit-tests
AustinAbro321 May 22, 2024
5ebfe9b
Merge branch 'main' into flux-agent-unit-tests
Noxsios May 22, 2024
f35d515
comment
AustinAbro321 May 22, 2024
b8a3214
Merge branch 'flux-agent-unit-tests' of github.com:defenseunicorns/za…
AustinAbro321 May 22, 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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/fairwindsops/pluto/v5 v5.18.4
github.com/fatih/color v1.16.0
github.com/fluxcd/helm-controller/api v0.37.4
github.com/fluxcd/pkg/apis/meta v1.3.0
github.com/fluxcd/source-controller/api v1.2.4
github.com/go-git/go-git/v5 v5.11.0
github.com/go-logr/logr v1.4.1
Expand Down Expand Up @@ -227,7 +228,6 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fluxcd/pkg/apis/acl v0.1.0 // indirect
github.com/fluxcd/pkg/apis/kustomize v1.3.0 // indirect
github.com/fluxcd/pkg/apis/meta v1.3.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
Expand Down
1 change: 1 addition & 0 deletions src/config/lang/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ const (
AgentErrBindHandler = "Unable to bind the webhook handler"
AgentErrCouldNotDeserializeReq = "could not deserialize request: %s"
AgentErrGetState = "failed to load zarf state: %w"
AgentErrParsePod = "failed to parse pod: %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
68 changes: 29 additions & 39 deletions src/internal/agent/hooks/flux.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,91 +5,86 @@
package hooks

import (
"context"
"encoding/json"
"fmt"

"github.com/defenseunicorns/pkg/helpers"
"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"
"github.com/defenseunicorns/zarf/src/types"
fluxmeta "github.com/fluxcd/pkg/apis/meta"
flux "github.com/fluxcd/source-controller/api/v1"
v1 "k8s.io/api/admission/v1"
)

// SecretRef contains the name used to reference a git repository secret.
type SecretRef struct {
Name string `json:"name"`
}

// GenericGitRepo contains the URL of a git repo and the secret that corresponds to it for use with Flux.
type GenericGitRepo struct {
Spec struct {
URL string `json:"url"`
SecretRef SecretRef `json:"secretRef,omitempty"`
} `json:"spec"`
}
// AgentErrTransformGitURL is thrown when the agent fails to make the git url a Zarf compatible url
const AgentErrTransformGitURL = "unable to transform the git url"

// NewGitRepositoryMutationHook creates a new instance of the git repo mutation hook.
func NewGitRepositoryMutationHook() operations.Hook {
func NewGitRepositoryMutationHook(ctx context.Context, cluster *cluster.Cluster) operations.Hook {
message.Debug("hooks.NewGitRepositoryMutationHook()")
return operations.Hook{
Create: mutateGitRepo,
Update: mutateGitRepo,
Create: func(r *v1.AdmissionRequest) (*operations.Result, error) {
return mutateGitRepo(ctx, r, cluster)
},
Update: func(r *v1.AdmissionRequest) (*operations.Result, error) {
return mutateGitRepo(ctx, r, cluster)
},
}
}

// mutateGitRepoCreate mutates the git repository url to point to the repository URL defined in the ZarfState.
func mutateGitRepo(r *v1.AdmissionRequest) (result *operations.Result, err error) {
func mutateGitRepo(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Cluster) (result *operations.Result, err error) {

var (
zarfState *types.ZarfState
patches []operations.PatchOperation
isPatched bool

isCreate = r.Operation == v1.Create
isUpdate = r.Operation == v1.Update
)

// Form the zarfState.GitServer.Address from the zarfState
if zarfState, err = state.GetZarfStateFromAgentPod(); err != nil {
state, err := cluster.LoadZarfState(ctx)
if err != nil {
return nil, fmt.Errorf(lang.AgentErrGetState, err)
}

message.Debugf("Using the url of (%s) to mutate the flux repository", zarfState.GitServer.Address)
message.Debugf("Using the url of (%s) to mutate the flux repository", state.GitServer.Address)

// parse to simple struct to read the git url
src := &GenericGitRepo{}
if err = json.Unmarshal(r.Object.Raw, &src); err != nil {
repo := flux.GitRepository{}
if err = json.Unmarshal(r.Object.Raw, &repo); err != nil {
return nil, fmt.Errorf(lang.ErrUnmarshal, err)
}
patchedURL := src.Spec.URL

// Check if this is an update operation and the hostname is different from what we have in the zarfState
// NOTE: We mutate on updates IF AND ONLY IF the hostname in the request is different than the hostname in the zarfState
// NOTE: We are checking if the hostname is different before because we do not want to potentially mutate a URL that has already been mutated.
if isUpdate {
isPatched, err = helpers.DoHostnamesMatch(zarfState.GitServer.Address, src.Spec.URL)
isPatched, err = helpers.DoHostnamesMatch(state.GitServer.Address, repo.Spec.URL)
if err != nil {
return nil, fmt.Errorf(lang.AgentErrHostnameMatch, err)
}
}

patchedURL := repo.Spec.URL

// Mutate the git URL if necessary
if isCreate || (isUpdate && !isPatched) {
// Mutate the git URL so that the hostname matches the hostname in the Zarf state
transformedURL, err := transform.GitURL(zarfState.GitServer.Address, patchedURL, zarfState.GitServer.PushUsername)
transformedURL, err := transform.GitURL(state.GitServer.Address, patchedURL, state.GitServer.PushUsername)
if err != nil {
message.Warnf("Unable to transform the git url, using the original url we have: %s", patchedURL)
return nil, fmt.Errorf("%s: %w", AgentErrTransformGitURL, err)
}
patchedURL = transformedURL.String()
message.Debugf("original git URL of (%s) got mutated to (%s)", src.Spec.URL, patchedURL)
message.Debugf("original git URL of (%s) got mutated to (%s)", repo.Spec.URL, patchedURL)
}

// Patch updates of the repo spec
patches = populatePatchOperations(patchedURL, src.Spec.SecretRef.Name)
patches = populatePatchOperations(patchedURL)

return &operations.Result{
Allowed: true,
Expand All @@ -98,17 +93,12 @@ func mutateGitRepo(r *v1.AdmissionRequest) (result *operations.Result, err error
}

// Patch updates of the repo spec.
func populatePatchOperations(repoURL string, secretName string) []operations.PatchOperation {
func populatePatchOperations(repoURL string) []operations.PatchOperation {
var patches []operations.PatchOperation
patches = append(patches, operations.ReplacePatchOperation("/spec/url", repoURL))

// If a prior secret exists, replace it
if secretName != "" {
patches = append(patches, operations.ReplacePatchOperation("/spec/secretRef/name", config.ZarfGitServerSecretName))
} else {
// Otherwise, add the new secret
patches = append(patches, operations.AddPatchOperation("/spec/secretRef", SecretRef{Name: config.ZarfGitServerSecretName}))
}
newSecretRef := fluxmeta.LocalObjectReference{Name: config.ZarfGitServerSecretName}
patches = append(patches, operations.AddPatchOperation("/spec/secretRef", newSecretRef))
AustinAbro321 marked this conversation as resolved.
Show resolved Hide resolved

return patches
}
116 changes: 116 additions & 0 deletions src/internal/agent/hooks/flux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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"
fluxmeta "github.com/fluxcd/pkg/apis/meta"
flux "github.com/fluxcd/source-controller/api/v1"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)

func createFluxGitRepoAdmissionRequest(t *testing.T, op v1.Operation, fluxGitRepo *flux.GitRepository) *v1.AdmissionRequest {
t.Helper()
raw, err := json.Marshal(fluxGitRepo)
require.NoError(t, err)
return &v1.AdmissionRequest{
Operation: op,
Object: runtime.RawExtension{
Raw: raw,
},
}
}

func TestFluxMutationWebhook(t *testing.T) {
t.Parallel()

ctx := context.Background()
state := &types.ZarfState{GitServer: types.GitServerInfo{
Address: "https://git-server.com",
PushUsername: "a-push-user",
}}
c := createTestClientWithZarfState(ctx, t, state)
handler := admission.NewHandler().Serve(NewGitRepositoryMutationHook(ctx, c))

tests := []admissionTest{
{
name: "should be mutated",
admissionReq: createFluxGitRepoAdmissionRequest(t, v1.Create, &flux.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: "mutate-this",
},
Spec: flux.GitRepositorySpec{
URL: "https://github.com/stefanprodan/podinfo.git",
},
}),
patch: []operations.PatchOperation{
operations.ReplacePatchOperation(
"/spec/url",
"https://git-server.com/a-push-user/podinfo-1646971829.git",
),
operations.AddPatchOperation(
"/spec/secretRef",
fluxmeta.LocalObjectReference{Name: config.ZarfGitServerSecretName},
),
},
code: http.StatusOK,
},
{
name: "should not mutate invalid git url",
admissionReq: createFluxGitRepoAdmissionRequest(t, v1.Update, &flux.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: "mutate-this",
},
Spec: flux.GitRepositorySpec{
URL: "not-a-git-url",
},
}),
patch: nil,
code: http.StatusInternalServerError,
errContains: AgentErrTransformGitURL,
},
{
name: "should patch to same url and update secret if hostname matches",
admissionReq: createFluxGitRepoAdmissionRequest(t, v1.Update, &flux.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: "no-mutate",
},
Spec: flux.GitRepositorySpec{
URL: "https://git-server.com/a-push-user/podinfo.git",
},
}),
patch: []operations.PatchOperation{
operations.ReplacePatchOperation(
"/spec/url",
"https://git-server.com/a-push-user/podinfo.git",
),
operations.AddPatchOperation(
"/spec/secretRef",
fluxmeta.LocalObjectReference{Name: config.ZarfGitServerSecretName},
),
},
code: http.StatusOK,
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
rr := sendAdmissionRequest(t, tt.admissionReq, handler)
verifyAdmission(t, rr, tt)
})
}
}
2 changes: 1 addition & 1 deletion src/internal/agent/hooks/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func mutatePod(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Clu

pod, err := parsePod(r.Object.Raw)
if err != nil {
return &operations.Result{Msg: err.Error()}, nil
return nil, fmt.Errorf(lang.AgentErrParsePod, err)
}

if pod.Labels != nil && pod.Labels["zarf-agent"] == "patched" {
Expand Down
27 changes: 7 additions & 20 deletions src/internal/agent/hooks/pods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,7 @@ func TestPodMutationWebhook(t *testing.T) {
c := createTestClientWithZarfState(ctx, t, state)
handler := admission.NewHandler().Serve(NewPodMutationHook(ctx, c))

tests := []struct {
name string
admissionReq *v1.AdmissionRequest
expectedPatch []operations.PatchOperation
code int
}{
tests := []admissionTest{
{
name: "pod with label should be mutated",
admissionReq: createPodAdmissionRequest(t, v1.Create, &corev1.Pod{
Expand All @@ -65,7 +60,7 @@ func TestPodMutationWebhook(t *testing.T) {
},
},
}),
expectedPatch: []operations.PatchOperation{
patch: []operations.PatchOperation{
operations.ReplacePatchOperation(
"/spec/imagePullSecrets",
[]corev1.LocalObjectReference{{Name: config.ZarfImagePullSecretName}},
Expand Down Expand Up @@ -99,8 +94,8 @@ func TestPodMutationWebhook(t *testing.T) {
Containers: []corev1.Container{{Image: "nginx"}},
},
}),
expectedPatch: nil,
code: http.StatusOK,
patch: nil,
code: http.StatusOK,
},
{
name: "pod with no labels should not error",
Expand All @@ -112,7 +107,7 @@ func TestPodMutationWebhook(t *testing.T) {
Containers: []corev1.Container{{Image: "nginx"}},
},
}),
expectedPatch: []operations.PatchOperation{
patch: []operations.PatchOperation{
operations.ReplacePatchOperation(
"/spec/imagePullSecrets",
[]corev1.LocalObjectReference{{Name: config.ZarfImagePullSecretName}},
Expand All @@ -134,16 +129,8 @@ func TestPodMutationWebhook(t *testing.T) {
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))
}
rr := sendAdmissionRequest(t, tt.admissionReq, handler)
verifyAdmission(t, rr, tt)
})
}
}
Loading
Loading