Skip to content

Commit

Permalink
test: agent flux unit test (#2528)
Browse files Browse the repository at this point in the history
## Description


Relates to #2512 

## Checklist before merging

- [ ] Test, docs, adr added or updated as needed
- [ ] [Contributor Guide
Steps](https://github.com/defenseunicorns/zarf/blob/main/.github/CONTRIBUTING.md#developer-workflow)
followed

---------

Co-authored-by: Lucas Rodriguez <lucas.rodriguez@defenseunicorns.com>
Co-authored-by: razzle <razzle@defenseunicorns.com>
  • Loading branch information
3 people authored May 22, 2024
1 parent 6b047b4 commit 42e1bf9
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 79 deletions.
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))

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

0 comments on commit 42e1bf9

Please sign in to comment.