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
55 changes: 55 additions & 0 deletions applicationset/generators/duck_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
log "github.com/sirupsen/logrus"
"sigs.k8s.io/controller-runtime/pkg/client"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
Expand All @@ -18,6 +19,7 @@ import (
"k8s.io/client-go/kubernetes"

"github.com/argoproj/argo-cd/v3/applicationset/utils"
"github.com/argoproj/argo-cd/v3/common"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)

Expand Down Expand Up @@ -147,6 +149,12 @@ func (g *DuckTypeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.A
return nil, nil
}

// Get cluster secrets to retrieve metadata (labels, annotations)
clusterSecrets, err := g.getSecretsByClusterName()
if err != nil {
return nil, fmt.Errorf("error getting cluster secrets: %w", err)
}

res := []map[string]any{}
for _, clusterDecision := range clusterDecisions {
cluster := findCluster(clustersFromArgoCD, clusterDecision, matchKey, statusListKey)
Expand All @@ -161,6 +169,11 @@ func (g *DuckTypeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.A
"server": cluster.Server,
}

// Add metadata (labels, annotations) from the cluster secret
if secretForCluster, exists := clusterSecrets[cluster.Name]; exists {
appendClusterMetadata(params, &secretForCluster, appSet)
}

for key, value := range clusterDecision.(map[string]any) {
params[key] = value.(string)
}
Expand Down Expand Up @@ -227,3 +240,45 @@ func collectParams(appSet *argoprojiov1alpha1.ApplicationSet, params map[string]
params["values."+key] = value
}
}

func (g *DuckTypeGenerator) getSecretsByClusterName() (map[string]corev1.Secret, error) {
clusterSecretList, err := g.clientset.CoreV1().Secrets(g.namespace).List(g.ctx,
metav1.ListOptions{LabelSelector: common.LabelKeySecretType + "=" + common.LabelValueSecretTypeCluster})
if err != nil {
return nil, err
}

res := map[string]corev1.Secret{}
for _, cluster := range clusterSecretList.Items {
clusterName := string(cluster.Data["name"])
res[clusterName] = cluster
}
return res, nil
}

func appendClusterMetadata(params map[string]any, cluster *corev1.Secret, appSet *argoprojiov1alpha1.ApplicationSet) {
if cluster == nil {
return
}

if appSet.Spec.GoTemplate {
meta := map[string]any{}

if len(cluster.Annotations) > 0 {
meta["annotations"] = cluster.Annotations
}
if len(cluster.Labels) > 0 {
meta["labels"] = cluster.Labels
}

params["metadata"] = meta
} else {
for key, value := range cluster.Annotations {
params["metadata.annotations."+key] = value
}

for key, value := range cluster.Labels {
params["metadata.labels."+key] = value
}
}
}
180 changes: 158 additions & 22 deletions applicationset/generators/duck_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,14 @@ func TestGenerateParamsForDuckType(t *testing.T) {
resource: duckType,
values: nil,
expected: []map[string]any{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},

{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
{
"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
},
{
"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging",
},
},
expectedError: nil,
},
Expand All @@ -191,7 +196,10 @@ func TestGenerateParamsForDuckType(t *testing.T) {
"foo": "bar",
},
expected: []map[string]any{
{"clusterName": "production-01", "values.foo": "bar", "name": "production-01", "server": "https://production-01.example.com"},
{
"clusterName": "production-01", "values.foo": "bar", "name": "production-01", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
},
},
expectedError: nil,
},
Expand Down Expand Up @@ -219,9 +227,14 @@ func TestGenerateParamsForDuckType(t *testing.T) {
resource: duckType,
values: nil,
expected: []map[string]any{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},

{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
{
"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
},
{
"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging",
},
},
expectedError: nil,
},
Expand All @@ -234,7 +247,10 @@ func TestGenerateParamsForDuckType(t *testing.T) {
"foo": "bar",
},
expected: []map[string]any{
{"clusterName": "production-01", "values.foo": "bar", "name": "production-01", "server": "https://production-01.example.com"},
{
"clusterName": "production-01", "values.foo": "bar", "name": "production-01", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
},
},
expectedError: nil,
},
Expand All @@ -251,9 +267,14 @@ func TestGenerateParamsForDuckType(t *testing.T) {
resource: duckType,
values: nil,
expected: []map[string]any{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},

{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
{
"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
},
{
"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging",
},
},
expectedError: nil,
},
Expand Down Expand Up @@ -473,9 +494,36 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
resource: duckType,
values: nil,
expected: []map[string]any{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},

{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
{
"clusterName": "production-01",
"name": "production-01",
"server": "https://production-01.example.com",
"metadata": map[string]any{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
"org": "bar",
},
"annotations": map[string]string{
"foo.argoproj.io": "production",
},
},
},
{
"clusterName": "staging-01",
"name": "staging-01",
"server": "https://staging-01.example.com",
"metadata": map[string]any{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "staging",
"org": "foo",
},
"annotations": map[string]string{
"foo.argoproj.io": "staging",
},
},
},
},
expectedError: nil,
},
Expand All @@ -487,7 +535,24 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
"foo": "bar",
},
expected: []map[string]any{
{"clusterName": "production-01", "values": map[string]string{"foo": "bar"}, "name": "production-01", "server": "https://production-01.example.com"},
{
"clusterName": "production-01",
"name": "production-01",
"server": "https://production-01.example.com",
"metadata": map[string]any{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
"org": "bar",
},
"annotations": map[string]string{
"foo.argoproj.io": "production",
},
},
"values": map[string]string{
"foo": "bar",
},
},
},
expectedError: nil,
},
Expand Down Expand Up @@ -515,9 +580,36 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
resource: duckType,
values: nil,
expected: []map[string]any{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},

{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
{
"clusterName": "production-01",
"name": "production-01",
"server": "https://production-01.example.com",
"metadata": map[string]any{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
"org": "bar",
},
"annotations": map[string]string{
"foo.argoproj.io": "production",
},
},
},
{
"clusterName": "staging-01",
"name": "staging-01",
"server": "https://staging-01.example.com",
"metadata": map[string]any{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "staging",
"org": "foo",
},
"annotations": map[string]string{
"foo.argoproj.io": "staging",
},
},
},
},
expectedError: nil,
},
Expand All @@ -530,7 +622,24 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
"foo": "bar",
},
expected: []map[string]any{
{"clusterName": "production-01", "values": map[string]string{"foo": "bar"}, "name": "production-01", "server": "https://production-01.example.com"},
{
"clusterName": "production-01",
"name": "production-01",
"server": "https://production-01.example.com",
"metadata": map[string]any{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
"org": "bar",
},
"annotations": map[string]string{
"foo.argoproj.io": "production",
},
},
"values": map[string]string{
"foo": "bar",
},
},
},
expectedError: nil,
},
Expand All @@ -547,9 +656,36 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
resource: duckType,
values: nil,
expected: []map[string]any{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},

{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
{
"clusterName": "production-01",
"name": "production-01",
"server": "https://production-01.example.com",
"metadata": map[string]any{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
"org": "bar",
},
"annotations": map[string]string{
"foo.argoproj.io": "production",
},
},
},
{
"clusterName": "staging-01",
"name": "staging-01",
"server": "https://staging-01.example.com",
"metadata": map[string]any{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "staging",
"org": "foo",
},
"annotations": map[string]string{
"foo.argoproj.io": "staging",
},
},
},
},
expectedError: nil,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,14 @@ This example leverages the cluster management capabilities of the [open-cluster-
## How it works
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.

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.
The ClusterDecisionResource generator provides the following parameter values to the ApplicationSet template for each cluster:

- `name`
- `server`
- `metadata.labels.<key>` *(for each label in the cluster Secret)*
- `metadata.annotations.<key>` *(for each annotation in the cluster Secret)*

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.

> [!NOTE]
> **Clusters listed as `Status.Decisions` must be predefined in Argo CD**
Expand Down
Loading