Repository: argoproj-labs/applicationset
Pull request #18 (2020-08-19 07:33:53)

Fix ClusterGenerator
This implements generating Argo `Application`s from `ClusterGenerator`.

**TODO**
- [x] unit tests

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/argoproj-labs/applicationset?pullRequest=18) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/argoproj-labs/applicationset?pullRequest=18) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/argoproj-labs/applicationset?pullRequest=18) it.</sub>
xianlubird: @OmerKahani Pls take a look
OmerKahani: Tested with the example, everything is working. @rtnpro Thanks for contributing!

I would love to see unit tests (we are missing them all over the project)
rtnpro: Thanks for the review @OmerKahani . I will be adding the unitttests for the `ClusterGenerator` tomorrow.

Patch:
diff --git a/Makefile b/Makefile
index 711da8a..657b2a3 100644
--- a/Makefile
+++ b/Makefile
@@ -5,12 +5,12 @@ DOCKER_PUSH?=true
 
 .PHONY: build
 build:
-	CGO_ENABLED=0 go build -ldflags="-w -s" -o ./dist/argocd-aplicationset .
+	CGO_ENABLED=0 go build -ldflags="-w -s" -o ./dist/argocd-applicationset .
 
 .PHONY: image
 image:
-	docker build -t $(IMAGE_PREFIX)/argocd-aplicationset:$(IMAGE_TAG) .
-	@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)/argocd-aplicationset:$(IMAGE_TAG) ; fi
+	docker build -t $(IMAGE_PREFIX)/argocd-applicationset:$(IMAGE_TAG) .
+	@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)/argocd-applicationset:$(IMAGE_TAG) ; fi
 
 # Generate manifests e.g. CRD, RBAC etc.
 .PHONY: manifests
diff --git a/main.go b/main.go
index 615d745..919ce1c 100644
--- a/main.go
+++ b/main.go
@@ -44,12 +44,12 @@ func main() {
 	ctrl.SetLogger(zap.New(zap.UseDevMode(true)))
 
 	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
-		Scheme:             scheme,
-		MetricsBindAddress: metricsAddr,
+		Scheme:                 scheme,
+		MetricsBindAddress:     metricsAddr,
 		HealthProbeBindAddress: probeBindAddr,
-		Port:               9443,
-		LeaderElection:     enableLeaderElection,
-		LeaderElectionID:   "58ac56fa.",
+		Port:                   9443,
+		LeaderElection:         enableLeaderElection,
+		LeaderElectionID:       "58ac56fa.",
 	})
 	if err != nil {
 		setupLog.Error(err, "unable to start manager")
@@ -57,8 +57,8 @@ func main() {
 	}
 
 	if err = (&controllers.ApplicationSetReconciler{
-		Client: mgr.GetClient(),
-		Scheme: mgr.GetScheme(),
+		Client:   mgr.GetClient(),
+		Scheme:   mgr.GetScheme(),
 		Recorder: mgr.GetEventRecorderFor("applicationset-controller"),
 	}).SetupWithManager(mgr); err != nil {
 		setupLog.Error(err, "unable to create controller", "controller", "ApplicationSet")
diff --git a/manifests/base/deployment.yaml b/manifests/base/deployment.yaml
index b538ccb..1c4bbe2 100644
--- a/manifests/base/deployment.yaml
+++ b/manifests/base/deployment.yaml
@@ -16,9 +16,9 @@ spec:
         app.kubernetes.io/name: argocd-applicationset-controller
     spec:
       containers:
-      - command:
-        - applicationset-controller
-        image: registry.cn-hangzhou.aliyuncs.com/appcenter/argocd-aplicationset:v0.1.0
-        imagePullPolicy: Always
-        name: argocd-applicationset-controller
-      serviceAccountName: default
\ No newline at end of file
+        - command:
+            - applicationset-controller
+          image: registry.cn-hangzhou.aliyuncs.com/appcenter/argocd-applicationset:v0.1.0
+          imagePullPolicy: Always
+          name: argocd-applicationset-controller
+      serviceAccountName: default
diff --git a/pkg/controllers/clustereventhandler.go b/pkg/controllers/clustereventhandler.go
index 2935c5f..9b1460d 100644
--- a/pkg/controllers/clustereventhandler.go
+++ b/pkg/controllers/clustereventhandler.go
@@ -73,5 +73,3 @@ func (h *clusterSecretEventHandler) queueRelatedAppGenerators(q workqueue.RateLi
 		}
 	}
 }
-
-
diff --git a/pkg/generators/cluster.go b/pkg/generators/cluster.go
index 3ab77b7..a6edee4 100644
--- a/pkg/generators/cluster.go
+++ b/pkg/generators/cluster.go
@@ -9,8 +9,9 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
 	argoprojiov1alpha1 "github.com/argoproj-labs/applicationset/api/v1alpha1"
+	"github.com/argoproj-labs/applicationset/pkg/utils"
+	argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
 )
 
 const (
@@ -56,9 +57,28 @@ func (g *ClusterGenerator) GenerateApplications(
 	}
 	log.Debug("clusters matching labels", "count", len(clusterSecretList.Items))
 
+	var tmplApplication argov1alpha1.Application
+	tmplApplication.Namespace = appSet.Spec.Template.Namespace
+	tmplApplication.Name = appSet.Spec.Template.Name
+	tmplApplication.Spec = appSet.Spec.Template.Spec
+
+	var resultingApplications []argov1alpha1.Application
+
 	for _, cluster := range clusterSecretList.Items {
+		params := make(map[string]string)
+		params["name"] = cluster.Name
+		params["server"] = string(cluster.Data["server"])
+		for key, value := range cluster.ObjectMeta.Labels {
+			params[fmt.Sprintf("metadata.labels.%s", key)] = value
+		}
 		log.WithField("cluster", cluster.Name).Info("matched cluster secret")
+		tmpApplication, err := utils.RenderTemplateParams(&tmplApplication, params)
+		if err != nil {
+			log.WithField("cluster", cluster.Name).Error("Error during rendering template params")
+			continue
+		}
+		resultingApplications = append(resultingApplications, *tmpApplication)
 	}
 
-	return nil, nil
+	return resultingApplications, nil
 }
diff --git a/pkg/utils/constants.go b/pkg/utils/constants.go
index 129519b..2ab8c49 100644
--- a/pkg/utils/constants.go
+++ b/pkg/utils/constants.go
@@ -2,5 +2,5 @@ package utils
 
 const (
 	ClusterListGeneratorKeyName = "cluster"
-	UrlGeneratorKeyName = "url"
+	UrlGeneratorKeyName         = "url"
 )


Test patch:
diff --git a/pkg/generators/cluster_test.go b/pkg/generators/cluster_test.go
new file mode 100644
index 0000000..e36e82d
--- /dev/null
+++ b/pkg/generators/cluster_test.go
@@ -0,0 +1,193 @@
+package generators
+
+import (
+	"context"
+	"encoding/base64"
+	"errors"
+	"fmt"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/kubernetes/scheme"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	argoprojiov1alpha1 "github.com/argoproj-labs/applicationset/api/v1alpha1"
+	argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
+
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+type possiblyErroringFakeCtrlRuntimeClient struct {
+	client.Client
+	shouldError bool
+}
+
+func (p *possiblyErroringFakeCtrlRuntimeClient) List(ctx context.Context, secretList runtime.Object, opts ...client.ListOption) error {
+	if p.shouldError {
+		return errors.New("Could not list Secrets.")
+	}
+	return p.Client.List(ctx, secretList, opts...)
+}
+
+func getRenderTemplate(appName, clusterName, server, environmentLabel string) *argov1alpha1.Application {
+	return &argov1alpha1.Application{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      fmt.Sprintf("%s-%s", clusterName, appName),
+			Namespace: "namespace",
+			Finalizers: []string{
+				"resources-finalizer.argocd.argoproj.io",
+			},
+		},
+		Spec: argov1alpha1.ApplicationSpec{
+			Source: argov1alpha1.ApplicationSource{
+				RepoURL:        "RepoURL",
+				Path:           fmt.Sprintf("%s/%s", appName, environmentLabel),
+				TargetRevision: "HEAD",
+			},
+			Destination: argov1alpha1.ApplicationDestination{
+				Server:    server,
+				Namespace: "destinationNamespace",
+			},
+			Project: "project",
+		},
+	}
+}
+
+func TestGenerateApplications(t *testing.T) {
+	clusters := []runtime.Object{
+		&corev1.Secret{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       "Secret",
+				APIVersion: "v1",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "staging-01",
+				Namespace: "namespace",
+				Labels: map[string]string{
+					"argocd.argoproj.io/secret-type": "cluster",
+					"environment":                    "staging",
+				},
+			},
+			Data: map[string][]byte{
+				"config": []byte(base64.StdEncoding.EncodeToString([]byte("foo"))),
+				"name":   []byte(base64.StdEncoding.EncodeToString([]byte("staging-01"))),
+				"server": []byte("https://staging-01.example.com"),
+			},
+			Type: corev1.SecretType("Opaque"),
+		},
+		&corev1.Secret{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       "Secret",
+				APIVersion: "v1",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "production-01",
+				Namespace: "namespace",
+				Labels: map[string]string{
+					"argocd.argoproj.io/secret-type": "cluster",
+					"environment":                    "production",
+				},
+			},
+			Data: map[string][]byte{
+				"config": []byte(base64.StdEncoding.EncodeToString([]byte("foo"))),
+				"name":   []byte(base64.StdEncoding.EncodeToString([]byte("productuion-01"))),
+				"server": []byte("https://production-01.example.com"),
+			},
+			Type: corev1.SecretType("Opaque"),
+		},
+	}
+
+	applicationSetTemplate := argoprojiov1alpha1.ApplicationSetTemplate{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "{{name}}-app",
+			Namespace: "namespace",
+		},
+		Spec: argov1alpha1.ApplicationSpec{
+			Source: argov1alpha1.ApplicationSource{
+				RepoURL:        "RepoURL",
+				Path:           "app/{{metadata.labels.environment}}",
+				TargetRevision: "HEAD",
+			},
+			Destination: argov1alpha1.ApplicationDestination{
+				Server:    "{{server}}",
+				Namespace: "destinationNamespace",
+			},
+			Project: "project",
+		},
+	}
+
+	testCases := []struct {
+		template      argoprojiov1alpha1.ApplicationSetTemplate
+		selector      metav1.LabelSelector
+		expected      []argov1alpha1.Application
+		clientError   bool
+		expectedError error
+	}{
+		{
+			applicationSetTemplate,
+			metav1.LabelSelector{},
+			[]argov1alpha1.Application{
+				*getRenderTemplate("app", "staging-01", "https://staging-01.example.com", "staging"),
+				*getRenderTemplate("app", "production-01", "https://production-01.example.com", "production"),
+			},
+			false,
+			nil,
+		},
+		{
+			applicationSetTemplate,
+			metav1.LabelSelector{
+				MatchLabels: map[string]string{
+					"environment": "production",
+				},
+			},
+			[]argov1alpha1.Application{
+				*getRenderTemplate("app", "production-01", "https://production-01.example.com", "production"),
+			},
+			false,
+			nil,
+		},
+		{
+			applicationSetTemplate,
+			metav1.LabelSelector{},
+			nil,
+			true,
+			errors.New("Could not list Secrets."),
+		},
+	}
+
+	for _, testCase := range testCases {
+		fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, clusters...)
+		cl := &possiblyErroringFakeCtrlRuntimeClient{
+			fakeClient,
+			testCase.clientError,
+		}
+
+		var clusterGenerator = NewClusterGenerator(cl)
+		applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "set",
+			},
+			Spec: argoprojiov1alpha1.ApplicationSetSpec{
+				Generators: []argoprojiov1alpha1.ApplicationSetGenerator{{
+					Clusters: &argoprojiov1alpha1.ClusterGenerator{
+						Selector: testCase.selector,
+					},
+				}},
+				Template: testCase.template,
+			},
+		}
+
+		got, err := clusterGenerator.GenerateApplications(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo)
+
+		if testCase.expectedError != nil {
+			assert.Error(t, testCase.expectedError, err)
+		} else {
+			assert.NoError(t, err)
+			assert.Equal(t, testCase.expected, got)
+		}
+
+	}
+}