Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 42 additions & 0 deletions packages/zarf-agent/chart/templates/webhook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,48 @@ webhooks:
- "v1"
- "v1beta1"
sideEffects: None
- name: agent-argocd-applicationset.zarf.dev
namespaceSelector:
matchExpressions:
# Ensure we don't mess with kube-system
- key: "kubernetes.io/metadata.name"
operator: NotIn
values:
- "kube-system"
# Allow ignoring whole namespaces
- key: zarf.dev/agent
operator: NotIn
values:
- "skip"
- "ignore"
objectSelector:
matchExpressions:
# Always ignore specific resources if requested by annotation/label
- key: zarf.dev/agent
operator: NotIn
values:
- "skip"
- "ignore"
clientConfig:
service:
name: {{ .Values.service.name }}
namespace: {{ .Release.Namespace }}
path: "/mutate/argocd-applicationset"
caBundle: "###ZARF_AGENT_CA###"
rules:
- operations:
- "CREATE"
- "UPDATE"
apiGroups:
- "argoproj.io"
apiVersions:
- "v1alpha1"
resources:
- "applicationsets"
admissionReviewVersions:
- "v1"
- "v1beta1"
sideEffects: None
- name: agent-argocd-repository.zarf.dev
namespaceSelector:
matchExpressions:
Expand Down
95 changes: 95 additions & 0 deletions src/internal/agent/hooks/argocd-applicationset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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 (
"context"
"encoding/json"
"fmt"

"github.com/zarf-dev/zarf/src/config/lang"
"github.com/zarf-dev/zarf/src/internal/agent/operations"
"github.com/zarf-dev/zarf/src/pkg/cluster"
"github.com/zarf-dev/zarf/src/pkg/logger"
v1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ApplicationSet is a definition of an ArgoCD ApplicationSet resource.
// The ArgoCD ApplicationSet structs in this file have been partially copied from upstream.
//
// https://github.com/argoproj/argo-cd/blob/v2.11.0/pkg/apis/application/v1alpha1/applicationset_types.go
//
// There were errors encountered when trying to import argocd as a Go package.
//
// For more information: https://argo-cd.readthedocs.io/en/stable/user-guide/import/
type ApplicationSet struct {
Spec ApplicationSetSpec `json:"spec"`
metav1.ObjectMeta `json:"metadata,omitempty"`
}

// ApplicationSetSpec represents a class of application set state.
type ApplicationSetSpec struct {
Generators []ApplicationSetGenerator `json:"generators,omitempty"`
}

// ApplicationSetGenerator represents a generator at the top level of an ApplicationSet.
type ApplicationSetGenerator struct {
Git *GitGenerator `json:"git,omitempty"`
}

// GitGenerator represents a class of git generator.
type GitGenerator struct {
RepoURL string `json:"repoURL"`
}

// NewApplicationSetMutationHook creates a new instance of the ArgoCD ApplicationSet mutation hook.
func NewApplicationSetMutationHook(ctx context.Context, cluster *cluster.Cluster) operations.Hook {
return operations.Hook{
Create: func(r *v1.AdmissionRequest) (*operations.Result, error) {
return mutateApplicationSet(ctx, r, cluster)
},
Update: func(r *v1.AdmissionRequest) (*operations.Result, error) {
return mutateApplicationSet(ctx, r, cluster)
},
}
}

// mutateApplication mutates the git repository urls to point to the repository URL defined in the ZarfState.
func mutateApplicationSet(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Cluster) (*operations.Result, error) {
l := logger.From(ctx)
s, err := cluster.LoadState(ctx)
if err != nil {
return nil, err
}

appSet := ApplicationSet{}
if err = json.Unmarshal(r.Object.Raw, &appSet); err != nil {
return nil, fmt.Errorf(lang.ErrUnmarshal, err)
}

l.Info("using the Zarf git server URL to mutate the ArgoCD ApplicationSet",
"name", appSet.Name,
"git-server", s.GitServer.Address)

patches := make([]operations.PatchOperation, 0)

for genIdx, generator := range appSet.Spec.Generators {
if generator.Git != nil && generator.Git.RepoURL != "" {
patchedURL, err := getPatchedRepoURL(ctx, generator.Git.RepoURL, s.GitServer, r)
if err != nil {
return nil, err
}
patches = append(patches, operations.ReplacePatchOperation(fmt.Sprintf("/spec/generators/%d/git/repoURL", genIdx), patchedURL))
}
}

patches = append(patches, getLabelPatch(appSet.Labels))

return &operations.Result{
Allowed: true,
PatchOps: patches,
}, nil
}
106 changes: 106 additions & 0 deletions src/internal/agent/hooks/argocd-applicationset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

package hooks

import (
"context"
"encoding/json"
"net/http"
"testing"

"github.com/stretchr/testify/require"
"github.com/zarf-dev/zarf/src/internal/agent/http/admission"
"github.com/zarf-dev/zarf/src/internal/agent/operations"
"github.com/zarf-dev/zarf/src/pkg/state"
v1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/runtime"
)

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

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

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

tests := []admissionTest{
{
name: "should mutate git generators and template sources",
admissionReq: createArgoAppSetAdmissionRequest(t, v1.Create, &ApplicationSet{
Spec: ApplicationSetSpec{
Generators: []ApplicationSetGenerator{
{
Git: &GitGenerator{
RepoURL: "https://diff-git-server.com/walnuts",
},
},
{
Git: &GitGenerator{
RepoURL: "https://diff-git-server.com/pecans",
},
},
},
},
}),
patch: []operations.PatchOperation{
operations.ReplacePatchOperation(
"/spec/generators/0/git/repoURL",
"https://git-server.com/a-push-user/walnuts-1104520479",
),
operations.ReplacePatchOperation(
"/spec/generators/1/git/repoURL",
"https://git-server.com/a-push-user/pecans-1381863636",
),
operations.ReplacePatchOperation(
"/metadata/labels",
map[string]string{
"zarf-agent": "patched",
},
),
},
code: http.StatusOK,
},
{
name: "should return internal server error on bad git URL",
admissionReq: createArgoAppSetAdmissionRequest(t, v1.Create, &ApplicationSet{
Spec: ApplicationSetSpec{
Generators: []ApplicationSetGenerator{
{
Git: &GitGenerator{
RepoURL: "https://bad-url",
},
},
},
},
}),
code: http.StatusInternalServerError,
errContains: AgentErrTransformGitURL,
},
}

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: 2 additions & 0 deletions src/internal/agent/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func StartWebhook(ctx context.Context, cluster *cluster.Cluster) error {
podsMutation := hooks.NewPodMutationHook(ctx, cluster)
fluxGitRepositoryMutation := hooks.NewGitRepositoryMutationHook(ctx, cluster)
argocdApplicationMutation := hooks.NewApplicationMutationHook(ctx, cluster)
argocdApplicationSetMutation := hooks.NewApplicationSetMutationHook(ctx, cluster)
argocdAppProjectMutation := hooks.NewAppProjectMutationHook(ctx, cluster)
argocdRepositoryMutation := hooks.NewRepositorySecretMutationHook(ctx, cluster)
fluxHelmRepositoryMutation := hooks.NewHelmRepositoryMutationHook(ctx, cluster)
Expand All @@ -50,6 +51,7 @@ func StartWebhook(ctx context.Context, cluster *cluster.Cluster) error {
mux.Handle("/mutate/flux-helmrepository", admissionHandler.Serve(ctx, fluxHelmRepositoryMutation))
mux.Handle("/mutate/flux-ocirepository", admissionHandler.Serve(ctx, fluxOCIRepositoryMutation))
mux.Handle("/mutate/argocd-application", admissionHandler.Serve(ctx, argocdApplicationMutation))
mux.Handle("/mutate/argocd-applicationset", admissionHandler.Serve(ctx, argocdApplicationSetMutation))
mux.Handle("/mutate/argocd-appproject", admissionHandler.Serve(ctx, argocdAppProjectMutation))
mux.Handle("/mutate/argocd-repository", admissionHandler.Serve(ctx, argocdRepositoryMutation))

Expand Down