Skip to content

Commit 93eea49

Browse files
committed
fix(appset): add cluster secret metadata to ClusterDecisionResource generator
Signed-off-by: Mike Ng <[email protected]>
1 parent 7ba0898 commit 93eea49

File tree

3 files changed

+221
-23
lines changed

3 files changed

+221
-23
lines changed

applicationset/generators/duck_type.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
log "github.com/sirupsen/logrus"
1111
"sigs.k8s.io/controller-runtime/pkg/client"
1212

13+
corev1 "k8s.io/api/core/v1"
1314
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1415
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1516
"k8s.io/apimachinery/pkg/fields"
@@ -18,6 +19,7 @@ import (
1819
"k8s.io/client-go/kubernetes"
1920

2021
"github.com/argoproj/argo-cd/v3/applicationset/utils"
22+
"github.com/argoproj/argo-cd/v3/common"
2123
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
2224
)
2325

@@ -147,6 +149,12 @@ func (g *DuckTypeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.A
147149
return nil, nil
148150
}
149151

152+
// Get cluster secrets to retrieve metadata (labels, annotations)
153+
clusterSecrets, err := g.getSecretsByClusterName()
154+
if err != nil {
155+
return nil, fmt.Errorf("error getting cluster secrets: %w", err)
156+
}
157+
150158
res := []map[string]any{}
151159
for _, clusterDecision := range clusterDecisions {
152160
cluster := findCluster(clustersFromArgoCD, clusterDecision, matchKey, statusListKey)
@@ -161,6 +169,11 @@ func (g *DuckTypeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.A
161169
"server": cluster.Server,
162170
}
163171

172+
// Add metadata (labels, annotations) from the cluster secret
173+
if secretForCluster, exists := clusterSecrets[cluster.Name]; exists {
174+
appendClusterMetadata(params, &secretForCluster, appSet)
175+
}
176+
164177
for key, value := range clusterDecision.(map[string]any) {
165178
params[key] = value.(string)
166179
}
@@ -227,3 +240,45 @@ func collectParams(appSet *argoprojiov1alpha1.ApplicationSet, params map[string]
227240
params["values."+key] = value
228241
}
229242
}
243+
244+
func (g *DuckTypeGenerator) getSecretsByClusterName() (map[string]corev1.Secret, error) {
245+
clusterSecretList, err := g.clientset.CoreV1().Secrets(g.namespace).List(g.ctx,
246+
metav1.ListOptions{LabelSelector: common.LabelKeySecretType + "=" + common.LabelValueSecretTypeCluster})
247+
if err != nil {
248+
return nil, err
249+
}
250+
251+
res := map[string]corev1.Secret{}
252+
for _, cluster := range clusterSecretList.Items {
253+
clusterName := string(cluster.Data["name"])
254+
res[clusterName] = cluster
255+
}
256+
return res, nil
257+
}
258+
259+
func appendClusterMetadata(params map[string]any, cluster *corev1.Secret, appSet *argoprojiov1alpha1.ApplicationSet) {
260+
if cluster == nil {
261+
return
262+
}
263+
264+
if appSet.Spec.GoTemplate {
265+
meta := map[string]any{}
266+
267+
if len(cluster.Annotations) > 0 {
268+
meta["annotations"] = cluster.Annotations
269+
}
270+
if len(cluster.Labels) > 0 {
271+
meta["labels"] = cluster.Labels
272+
}
273+
274+
params["metadata"] = meta
275+
} else {
276+
for key, value := range cluster.Annotations {
277+
params["metadata.annotations."+key] = value
278+
}
279+
280+
for key, value := range cluster.Labels {
281+
params["metadata.labels."+key] = value
282+
}
283+
}
284+
}

applicationset/generators/duck_type_test.go

Lines changed: 158 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,14 @@ func TestGenerateParamsForDuckType(t *testing.T) {
177177
resource: duckType,
178178
values: nil,
179179
expected: []map[string]any{
180-
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
181-
182-
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
180+
{
181+
"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
182+
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
183+
},
184+
{
185+
"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
186+
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging",
187+
},
183188
},
184189
expectedError: nil,
185190
},
@@ -191,7 +196,10 @@ func TestGenerateParamsForDuckType(t *testing.T) {
191196
"foo": "bar",
192197
},
193198
expected: []map[string]any{
194-
{"clusterName": "production-01", "values.foo": "bar", "name": "production-01", "server": "https://production-01.example.com"},
199+
{
200+
"clusterName": "production-01", "values.foo": "bar", "name": "production-01", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
201+
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
202+
},
195203
},
196204
expectedError: nil,
197205
},
@@ -219,9 +227,14 @@ func TestGenerateParamsForDuckType(t *testing.T) {
219227
resource: duckType,
220228
values: nil,
221229
expected: []map[string]any{
222-
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
223-
224-
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
230+
{
231+
"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
232+
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
233+
},
234+
{
235+
"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
236+
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging",
237+
},
225238
},
226239
expectedError: nil,
227240
},
@@ -234,7 +247,10 @@ func TestGenerateParamsForDuckType(t *testing.T) {
234247
"foo": "bar",
235248
},
236249
expected: []map[string]any{
237-
{"clusterName": "production-01", "values.foo": "bar", "name": "production-01", "server": "https://production-01.example.com"},
250+
{
251+
"clusterName": "production-01", "values.foo": "bar", "name": "production-01", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
252+
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
253+
},
238254
},
239255
expectedError: nil,
240256
},
@@ -251,9 +267,14 @@ func TestGenerateParamsForDuckType(t *testing.T) {
251267
resource: duckType,
252268
values: nil,
253269
expected: []map[string]any{
254-
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
255-
256-
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
270+
{
271+
"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
272+
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
273+
},
274+
{
275+
"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
276+
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging",
277+
},
257278
},
258279
expectedError: nil,
259280
},
@@ -473,9 +494,36 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
473494
resource: duckType,
474495
values: nil,
475496
expected: []map[string]any{
476-
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
477-
478-
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
497+
{
498+
"clusterName": "production-01",
499+
"name": "production-01",
500+
"server": "https://production-01.example.com",
501+
"metadata": map[string]any{
502+
"labels": map[string]string{
503+
"argocd.argoproj.io/secret-type": "cluster",
504+
"environment": "production",
505+
"org": "bar",
506+
},
507+
"annotations": map[string]string{
508+
"foo.argoproj.io": "production",
509+
},
510+
},
511+
},
512+
{
513+
"clusterName": "staging-01",
514+
"name": "staging-01",
515+
"server": "https://staging-01.example.com",
516+
"metadata": map[string]any{
517+
"labels": map[string]string{
518+
"argocd.argoproj.io/secret-type": "cluster",
519+
"environment": "staging",
520+
"org": "foo",
521+
},
522+
"annotations": map[string]string{
523+
"foo.argoproj.io": "staging",
524+
},
525+
},
526+
},
479527
},
480528
expectedError: nil,
481529
},
@@ -487,7 +535,24 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
487535
"foo": "bar",
488536
},
489537
expected: []map[string]any{
490-
{"clusterName": "production-01", "values": map[string]string{"foo": "bar"}, "name": "production-01", "server": "https://production-01.example.com"},
538+
{
539+
"clusterName": "production-01",
540+
"name": "production-01",
541+
"server": "https://production-01.example.com",
542+
"metadata": map[string]any{
543+
"labels": map[string]string{
544+
"argocd.argoproj.io/secret-type": "cluster",
545+
"environment": "production",
546+
"org": "bar",
547+
},
548+
"annotations": map[string]string{
549+
"foo.argoproj.io": "production",
550+
},
551+
},
552+
"values": map[string]string{
553+
"foo": "bar",
554+
},
555+
},
491556
},
492557
expectedError: nil,
493558
},
@@ -515,9 +580,36 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
515580
resource: duckType,
516581
values: nil,
517582
expected: []map[string]any{
518-
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
519-
520-
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
583+
{
584+
"clusterName": "production-01",
585+
"name": "production-01",
586+
"server": "https://production-01.example.com",
587+
"metadata": map[string]any{
588+
"labels": map[string]string{
589+
"argocd.argoproj.io/secret-type": "cluster",
590+
"environment": "production",
591+
"org": "bar",
592+
},
593+
"annotations": map[string]string{
594+
"foo.argoproj.io": "production",
595+
},
596+
},
597+
},
598+
{
599+
"clusterName": "staging-01",
600+
"name": "staging-01",
601+
"server": "https://staging-01.example.com",
602+
"metadata": map[string]any{
603+
"labels": map[string]string{
604+
"argocd.argoproj.io/secret-type": "cluster",
605+
"environment": "staging",
606+
"org": "foo",
607+
},
608+
"annotations": map[string]string{
609+
"foo.argoproj.io": "staging",
610+
},
611+
},
612+
},
521613
},
522614
expectedError: nil,
523615
},
@@ -530,7 +622,24 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
530622
"foo": "bar",
531623
},
532624
expected: []map[string]any{
533-
{"clusterName": "production-01", "values": map[string]string{"foo": "bar"}, "name": "production-01", "server": "https://production-01.example.com"},
625+
{
626+
"clusterName": "production-01",
627+
"name": "production-01",
628+
"server": "https://production-01.example.com",
629+
"metadata": map[string]any{
630+
"labels": map[string]string{
631+
"argocd.argoproj.io/secret-type": "cluster",
632+
"environment": "production",
633+
"org": "bar",
634+
},
635+
"annotations": map[string]string{
636+
"foo.argoproj.io": "production",
637+
},
638+
},
639+
"values": map[string]string{
640+
"foo": "bar",
641+
},
642+
},
534643
},
535644
expectedError: nil,
536645
},
@@ -547,9 +656,36 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
547656
resource: duckType,
548657
values: nil,
549658
expected: []map[string]any{
550-
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
551-
552-
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
659+
{
660+
"clusterName": "production-01",
661+
"name": "production-01",
662+
"server": "https://production-01.example.com",
663+
"metadata": map[string]any{
664+
"labels": map[string]string{
665+
"argocd.argoproj.io/secret-type": "cluster",
666+
"environment": "production",
667+
"org": "bar",
668+
},
669+
"annotations": map[string]string{
670+
"foo.argoproj.io": "production",
671+
},
672+
},
673+
},
674+
{
675+
"clusterName": "staging-01",
676+
"name": "staging-01",
677+
"server": "https://staging-01.example.com",
678+
"metadata": map[string]any{
679+
"labels": map[string]string{
680+
"argocd.argoproj.io/secret-type": "cluster",
681+
"environment": "staging",
682+
"org": "foo",
683+
},
684+
"annotations": map[string]string{
685+
"foo.argoproj.io": "staging",
686+
},
687+
},
688+
},
553689
},
554690
expectedError: nil,
555691
},

docs/operator-manual/applicationset/Generators-Cluster-Decision-Resource.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,14 @@ This example leverages the cluster management capabilities of the [open-cluster-
7777
## How it works
7878
The ApplicationSet needs to be created in the Argo CD namespace, placing the `ConfigMap` in the same namespace allows the ClusterDecisionResource generator to read it. The `ConfigMap` stores the GVK information as well as the status key definitions. In the open-cluster-management example, the ApplicationSet generator will read the kind `placementrules` with an apiVersion of `apps.open-cluster-management.io/v1`. It will attempt to extract the **list** of clusters from the key `decisions`. It then validates the actual cluster name as defined in Argo CD against the **value** from the key `clusterName` in each of the elements in the list.
7979

80-
The ClusterDecisionResource generator passes the 'name', 'server' and any other key/value in the duck-type resource's status list as parameters into the ApplicationSet template. In this example, the decision array contained an additional key `clusterName`, which is now available to the ApplicationSet template.
80+
The ClusterDecisionResource generator provides the following parameter values to the ApplicationSet template for each cluster:
81+
82+
- `name`
83+
- `server`
84+
- `metadata.labels.<key>` *(for each label in the cluster Secret)*
85+
- `metadata.annotations.<key>` *(for each annotation in the cluster Secret)*
86+
87+
Additionally, any other key/value pairs in the duck-type resource's status list are passed as parameters. In this example, the decision array contained an additional key `clusterName`, which is now available to the ApplicationSet template.
8188

8289
> [!NOTE]
8390
> **Clusters listed as `Status.Decisions` must be predefined in Argo CD**

0 commit comments

Comments
 (0)