WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit b5395cb

Browse files
committed
feat: add support for Argo CD ApplicationSets
Signed-off-by: Robin Lieb <[email protected]>
1 parent e3e5f19 commit b5395cb

File tree

4 files changed

+245
-0
lines changed

4 files changed

+245
-0
lines changed

packages/zarf-agent/chart/templates/webhook.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,48 @@ webhooks:
222222
- "v1"
223223
- "v1beta1"
224224
sideEffects: None
225+
- name: agent-argocd-applicationset.zarf.dev
226+
namespaceSelector:
227+
matchExpressions:
228+
# Ensure we don't mess with kube-system
229+
- key: "kubernetes.io/metadata.name"
230+
operator: NotIn
231+
values:
232+
- "kube-system"
233+
# Allow ignoring whole namespaces
234+
- key: zarf.dev/agent
235+
operator: NotIn
236+
values:
237+
- "skip"
238+
- "ignore"
239+
objectSelector:
240+
matchExpressions:
241+
# Always ignore specific resources if requested by annotation/label
242+
- key: zarf.dev/agent
243+
operator: NotIn
244+
values:
245+
- "skip"
246+
- "ignore"
247+
clientConfig:
248+
service:
249+
name: {{ .Values.service.name }}
250+
namespace: {{ .Release.Namespace }}
251+
path: "/mutate/argocd-applicationset"
252+
caBundle: "###ZARF_AGENT_CA###"
253+
rules:
254+
- operations:
255+
- "CREATE"
256+
- "UPDATE"
257+
apiGroups:
258+
- "argoproj.io"
259+
apiVersions:
260+
- "v1alpha1"
261+
resources:
262+
- "applicationsets"
263+
admissionReviewVersions:
264+
- "v1"
265+
- "v1beta1"
266+
sideEffects: None
225267
- name: agent-argocd-repository.zarf.dev
226268
namespaceSelector:
227269
matchExpressions:
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors
3+
4+
// Package hooks contains the mutation hooks for the Zarf agent.
5+
package hooks
6+
7+
import (
8+
"context"
9+
"encoding/json"
10+
"fmt"
11+
12+
"github.com/zarf-dev/zarf/src/config/lang"
13+
"github.com/zarf-dev/zarf/src/internal/agent/operations"
14+
"github.com/zarf-dev/zarf/src/pkg/cluster"
15+
"github.com/zarf-dev/zarf/src/pkg/logger"
16+
v1 "k8s.io/api/admission/v1"
17+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18+
)
19+
20+
// ApplicationSet is a definition of an ArgoCD ApplicationSet resource.
21+
// The ArgoCD ApplicationSet structs in this file have been partially copied from upstream.
22+
//
23+
// https://github.com/argoproj/argo-cd/blob/v2.11.0/pkg/apis/application/v1alpha1/applicationset_types.go
24+
//
25+
// There were errors encountered when trying to import argocd as a Go package.
26+
//
27+
// For more information: https://argo-cd.readthedocs.io/en/stable/user-guide/import/
28+
type ApplicationSet struct {
29+
Spec ApplicationSetSpec `json:"spec"`
30+
metav1.ObjectMeta `json:"metadata,omitempty"`
31+
}
32+
33+
// ApplicationSetSpec represents a class of application set state.
34+
type ApplicationSetSpec struct {
35+
Generators []ApplicationSetGenerator `json:"generators,omitempty"`
36+
}
37+
38+
// ApplicationSetGenerator represents a generator at the top level of an ApplicationSet.
39+
type ApplicationSetGenerator struct {
40+
Git *GitGenerator `json:"git,omitempty"`
41+
}
42+
43+
// GitGenerator represents a class of git generator.
44+
type GitGenerator struct {
45+
RepoURL string `json:"repoURL"`
46+
}
47+
48+
// NewApplicationSetMutationHook creates a new instance of the ArgoCD ApplicationSet mutation hook.
49+
func NewApplicationSetMutationHook(ctx context.Context, cluster *cluster.Cluster) operations.Hook {
50+
return operations.Hook{
51+
Create: func(r *v1.AdmissionRequest) (*operations.Result, error) {
52+
return mutateApplicationSet(ctx, r, cluster)
53+
},
54+
Update: func(r *v1.AdmissionRequest) (*operations.Result, error) {
55+
return mutateApplicationSet(ctx, r, cluster)
56+
},
57+
}
58+
}
59+
60+
// mutateApplication mutates the git repository urls to point to the repository URL defined in the ZarfState.
61+
func mutateApplicationSet(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Cluster) (*operations.Result, error) {
62+
l := logger.From(ctx)
63+
s, err := cluster.LoadState(ctx)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
appSet := ApplicationSet{}
69+
if err = json.Unmarshal(r.Object.Raw, &appSet); err != nil {
70+
return nil, fmt.Errorf(lang.ErrUnmarshal, err)
71+
}
72+
73+
l.Info("using the Zarf git server URL to mutate the ArgoCD ApplicationSet",
74+
"name", appSet.Name,
75+
"git-server", s.GitServer.Address)
76+
77+
patches := make([]operations.PatchOperation, 0)
78+
79+
for genIdx, generator := range appSet.Spec.Generators {
80+
if generator.Git != nil && generator.Git.RepoURL != "" {
81+
patchedURL, err := getPatchedRepoURL(ctx, generator.Git.RepoURL, s.GitServer, r)
82+
if err != nil {
83+
return nil, err
84+
}
85+
patches = append(patches, operations.ReplacePatchOperation(fmt.Sprintf("/spec/generators/%d/git/repoURL", genIdx), patchedURL))
86+
}
87+
}
88+
89+
patches = append(patches, getLabelPatch(appSet.Labels))
90+
91+
return &operations.Result{
92+
Allowed: true,
93+
PatchOps: patches,
94+
}, nil
95+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors
3+
4+
package hooks
5+
6+
import (
7+
"context"
8+
"encoding/json"
9+
"net/http"
10+
"testing"
11+
12+
"github.com/stretchr/testify/require"
13+
"github.com/zarf-dev/zarf/src/internal/agent/http/admission"
14+
"github.com/zarf-dev/zarf/src/internal/agent/operations"
15+
"github.com/zarf-dev/zarf/src/pkg/state"
16+
v1 "k8s.io/api/admission/v1"
17+
"k8s.io/apimachinery/pkg/runtime"
18+
)
19+
20+
func createArgoAppSetAdmissionRequest(t *testing.T, op v1.Operation, argoAppSet *ApplicationSet) *v1.AdmissionRequest {
21+
t.Helper()
22+
raw, err := json.Marshal(argoAppSet)
23+
require.NoError(t, err)
24+
return &v1.AdmissionRequest{
25+
Operation: op,
26+
Object: runtime.RawExtension{
27+
Raw: raw,
28+
},
29+
}
30+
}
31+
32+
func TestArgoAppSetWebhook(t *testing.T) {
33+
t.Parallel()
34+
35+
ctx := context.Background()
36+
s := &state.State{GitServer: state.GitServerInfo{
37+
Address: "https://git-server.com",
38+
PushUsername: "a-push-user",
39+
}}
40+
c := createTestClientWithZarfState(ctx, t, s)
41+
handler := admission.NewHandler().Serve(ctx, NewApplicationSetMutationHook(ctx, c))
42+
43+
tests := []admissionTest{
44+
{
45+
name: "should mutate git generators and template sources",
46+
admissionReq: createArgoAppSetAdmissionRequest(t, v1.Create, &ApplicationSet{
47+
Spec: ApplicationSetSpec{
48+
Generators: []ApplicationSetGenerator{
49+
{
50+
Git: &GitGenerator{
51+
RepoURL: "https://diff-git-server.com/walnuts",
52+
},
53+
},
54+
{
55+
Git: &GitGenerator{
56+
RepoURL: "https://diff-git-server.com/pecans",
57+
},
58+
},
59+
},
60+
},
61+
}),
62+
patch: []operations.PatchOperation{
63+
operations.ReplacePatchOperation(
64+
"/spec/generators/0/git/repoURL",
65+
"https://git-server.com/a-push-user/walnuts-1104520479",
66+
),
67+
operations.ReplacePatchOperation(
68+
"/spec/generators/1/git/repoURL",
69+
"https://git-server.com/a-push-user/pecans-1381863636",
70+
),
71+
operations.ReplacePatchOperation(
72+
"/metadata/labels",
73+
map[string]string{
74+
"zarf-agent": "patched",
75+
},
76+
),
77+
},
78+
code: http.StatusOK,
79+
},
80+
{
81+
name: "should return internal server error on bad git URL",
82+
admissionReq: createArgoAppSetAdmissionRequest(t, v1.Create, &ApplicationSet{
83+
Spec: ApplicationSetSpec{
84+
Generators: []ApplicationSetGenerator{
85+
{
86+
Git: &GitGenerator{
87+
RepoURL: "https://bad-url",
88+
},
89+
},
90+
},
91+
},
92+
}),
93+
code: http.StatusInternalServerError,
94+
errContains: AgentErrTransformGitURL,
95+
},
96+
}
97+
98+
for _, tt := range tests {
99+
tt := tt
100+
t.Run(tt.name, func(t *testing.T) {
101+
t.Parallel()
102+
rr := sendAdmissionRequest(t, tt.admissionReq, handler)
103+
verifyAdmission(t, rr, tt)
104+
})
105+
}
106+
}

src/internal/agent/start.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func StartWebhook(ctx context.Context, cluster *cluster.Cluster) error {
3838
podsMutation := hooks.NewPodMutationHook(ctx, cluster)
3939
fluxGitRepositoryMutation := hooks.NewGitRepositoryMutationHook(ctx, cluster)
4040
argocdApplicationMutation := hooks.NewApplicationMutationHook(ctx, cluster)
41+
argocdApplicationSetMutation := hooks.NewApplicationSetMutationHook(ctx, cluster)
4142
argocdAppProjectMutation := hooks.NewAppProjectMutationHook(ctx, cluster)
4243
argocdRepositoryMutation := hooks.NewRepositorySecretMutationHook(ctx, cluster)
4344
fluxHelmRepositoryMutation := hooks.NewHelmRepositoryMutationHook(ctx, cluster)
@@ -50,6 +51,7 @@ func StartWebhook(ctx context.Context, cluster *cluster.Cluster) error {
5051
mux.Handle("/mutate/flux-helmrepository", admissionHandler.Serve(ctx, fluxHelmRepositoryMutation))
5152
mux.Handle("/mutate/flux-ocirepository", admissionHandler.Serve(ctx, fluxOCIRepositoryMutation))
5253
mux.Handle("/mutate/argocd-application", admissionHandler.Serve(ctx, argocdApplicationMutation))
54+
mux.Handle("/mutate/argocd-applicationset", admissionHandler.Serve(ctx, argocdApplicationSetMutation))
5355
mux.Handle("/mutate/argocd-appproject", admissionHandler.Serve(ctx, argocdAppProjectMutation))
5456
mux.Handle("/mutate/argocd-repository", admissionHandler.Serve(ctx, argocdRepositoryMutation))
5557

0 commit comments

Comments
 (0)