Repository: argoproj-labs/applicationset
Pull request #125 (2021-02-14 20:55:16)

Fix leader election
Fix https://github.com/argoproj-labs/applicationset/issues/124

maruina: /assign @jgwest 

Patch:
diff --git a/Makefile b/Makefile
index cdbf04f..8300f61 100644
--- a/Makefile
+++ b/Makefile
@@ -78,7 +78,7 @@ start-e2e:
 # Begin the tests, targetting the standalone controller (started by make start-e2e) and the e2e argo-cd (started by make start-e2e)
 .PHONY: test-e2e
 test-e2e:
-	NAMESPACE=argocd-e2e go test -race -count=1 -v -timeout 120s ./test/e2e/applicationset
+	NAMESPACE=argocd-e2e go test -race -count=1 -v -timeout 480s ./test/e2e/applicationset
 
 # Generate code
 generate: controller-gen
diff --git a/main.go b/main.go
index c541053..f055fb8 100644
--- a/main.go
+++ b/main.go
@@ -115,7 +115,7 @@ func main() {
 		HealthProbeBindAddress: probeBindAddr,
 		Port:                   9443,
 		LeaderElection:         enableLeaderElection,
-		LeaderElectionID:       "58ac56fa.",
+		LeaderElectionID:       "58ac56fa.applicationsets.argoproj.io",
 		DryRunClient:           dryRun,
 	})
 	if err != nil {
@@ -128,7 +128,7 @@ func main() {
 	if err = (&controllers.ApplicationSetReconciler{
 		Generators: map[string]generators.Generator{
 			"List":     generators.NewListGenerator(),
-			"Clusters": generators.NewClusterGenerator(mgr.GetClient()),
+			"Clusters": generators.NewClusterGenerator(mgr.GetClient(), context.Background(), k8s, namespace),
 			"Git":      generators.NewGitGenerator(services.NewArgoCDService(context.Background(), k8s, namespace, argocdRepoServer)),
 		},
 		Client:   mgr.GetClient(),
diff --git a/pkg/generators/cluster.go b/pkg/generators/cluster.go
index 75bc622..01aec27 100644
--- a/pkg/generators/cluster.go
+++ b/pkg/generators/cluster.go
@@ -5,10 +5,13 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/argoproj/argo-cd/util/db"
+	"github.com/argoproj/argo-cd/util/settings"
 	log "github.com/sirupsen/logrus"
 
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	argoprojiov1alpha1 "github.com/argoproj-labs/applicationset/api/v1alpha1"
@@ -24,11 +27,18 @@ var _ Generator = (*ClusterGenerator)(nil)
 // ClusterGenerator generates Applications for some or all clusters registered with ArgoCD.
 type ClusterGenerator struct {
 	client.Client
+	ctx       context.Context
+	clientset kubernetes.Interface
+	// namespace is the Argo CD namespace
+	namespace string
 }
 
-func NewClusterGenerator(c client.Client) Generator {
+func NewClusterGenerator(c client.Client, ctx context.Context, clientset kubernetes.Interface, namespace string) Generator {
 	g := &ClusterGenerator{
-		Client: c,
+		Client:    c,
+		ctx:       ctx,
+		clientset: clientset,
+		namespace: namespace,
 	}
 	return g
 }
@@ -52,24 +62,57 @@ func (g *ClusterGenerator) GenerateParams(
 		return nil, nil
 	}
 
-	// List all Clusters:
-	clusterSecretList := &corev1.SecretList{}
+	// Do not include the local cluster in the cluster parameters IF there is a non-empty selector
+	// - Since local clusters do not have secrets, they do not have labels to match against
+	ignoreLocalClusters := len(appSetGenerator.Clusters.Selector.MatchExpressions) > 0 || len(appSetGenerator.Clusters.Selector.MatchLabels) > 0
 
-	selector := metav1.AddLabelToSelector(&appSetGenerator.Clusters.Selector, ArgoCDSecretTypeLabel, ArgoCDSecretTypeCluster)
-	secretSelector, err := metav1.LabelSelectorAsSelector(selector)
+	// ListCluster from Argo CD's util/db package will include the local cluster in the list of clusters
+	settingsMgr := settings.NewSettingsManager(g.ctx, g.clientset, g.namespace)
+	newDB := db.NewDB(g.namespace, settingsMgr, g.clientset)
+	clustersFromArgoCD, err := newDB.ListClusters(g.ctx)
 	if err != nil {
 		return nil, err
 	}
 
-	if err := g.Client.List(context.Background(), clusterSecretList, client.MatchingLabelsSelector{Selector: secretSelector}); err != nil {
+	if clustersFromArgoCD == nil {
+		return nil, nil
+	}
+
+	clusterSecrets, err := g.getSecretsByClusterName(appSetGenerator)
+	if err != nil {
 		return nil, err
 	}
-	log.Debug("clusters matching labels", "count", len(clusterSecretList.Items))
 
-	// For each matching cluster secret
-	res := make([]map[string]string, len(clusterSecretList.Items))
-	for i, cluster := range clusterSecretList.Items {
-		params := make(map[string]string, len(appSetGenerator.Clusters.Values)+len(cluster.ObjectMeta.Annotations)+len(cluster.ObjectMeta.Labels)+2)
+	res := []map[string]string{}
+
+	secretsFound := []corev1.Secret{}
+
+	for _, cluster := range clustersFromArgoCD.Items {
+
+		// If there is a secret for this cluster, then it's a non-local cluster, so it will be
+		// handled by the next step.
+		if secretForCluster, exists := clusterSecrets[cluster.Name]; exists {
+			secretsFound = append(secretsFound, secretForCluster)
+
+		} else if !ignoreLocalClusters {
+			// If there is no secret for the cluster, it's the local cluster, so handle it here.
+			params := map[string]string{}
+			params["name"] = cluster.Name
+			params["server"] = cluster.Server
+
+			for key, value := range appSetGenerator.Clusters.Values {
+				params[fmt.Sprintf("values.%s", key)] = value
+			}
+
+			log.WithField("cluster", "local cluster").Info("matched local cluster")
+
+			res = append(res, params)
+		}
+	}
+
+	// For each matching cluster secret (non-local clusters only)
+	for _, cluster := range secretsFound {
+		params := map[string]string{}
 		params["name"] = string(cluster.Data["name"])
 		params["server"] = string(cluster.Data["server"])
 		for key, value := range cluster.ObjectMeta.Annotations {
@@ -83,8 +126,35 @@ func (g *ClusterGenerator) GenerateParams(
 		}
 		log.WithField("cluster", cluster.Name).Info("matched cluster secret")
 
-		res[i] = params
+		res = append(res, params)
+	}
+
+	return res, nil
+}
+
+func (g *ClusterGenerator) getSecretsByClusterName(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) (map[string]corev1.Secret, error) {
+	// List all Clusters:
+	clusterSecretList := &corev1.SecretList{}
+
+	selector := metav1.AddLabelToSelector(&appSetGenerator.Clusters.Selector, ArgoCDSecretTypeLabel, ArgoCDSecretTypeCluster)
+	secretSelector, err := metav1.LabelSelectorAsSelector(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := g.Client.List(context.Background(), clusterSecretList, client.MatchingLabelsSelector{Selector: secretSelector}); err != nil {
+		return nil, err
+	}
+	log.Debug("clusters matching labels", "count", len(clusterSecretList.Items))
+
+	res := map[string]corev1.Secret{}
+
+	for _, cluster := range clusterSecretList.Items {
+		clusterName := string(cluster.Data["name"])
+
+		res[clusterName] = cluster
 	}
 
 	return res, nil
+
 }


Test patch:
diff --git a/pkg/generators/cluster_test.go b/pkg/generators/cluster_test.go
index 0d48074..11ba21d 100644
--- a/pkg/generators/cluster_test.go
+++ b/pkg/generators/cluster_test.go
@@ -2,17 +2,18 @@ package generators
 
 import (
 	"context"
-	"encoding/base64"
 	"errors"
 
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	"testing"
 
 	argoprojiov1alpha1 "github.com/argoproj-labs/applicationset/api/v1alpha1"
+	kubefake "k8s.io/client-go/kubernetes/fake"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -49,8 +50,8 @@ func TestGenerateParams(t *testing.T) {
 				},
 			},
 			Data: map[string][]byte{
-				"config": []byte(base64.StdEncoding.EncodeToString([]byte("foo"))),
-				"name":   []byte(base64.StdEncoding.EncodeToString([]byte("staging-01"))),
+				"config": []byte("{}"),
+				"name":   []byte("staging-01"),
 				"server": []byte("https://staging-01.example.com"),
 			},
 			Type: corev1.SecretType("Opaque"),
@@ -73,47 +74,76 @@ func TestGenerateParams(t *testing.T) {
 				},
 			},
 			Data: map[string][]byte{
-				"config": []byte(base64.StdEncoding.EncodeToString([]byte("foo"))),
-				"name":   []byte(base64.StdEncoding.EncodeToString([]byte("production-01"))),
+				"config": []byte("{}"),
+				"name":   []byte("production-01"),
 				"server": []byte("https://production-01.example.com"),
 			},
 			Type: corev1.SecretType("Opaque"),
 		},
 	}
 	testCases := []struct {
-		selector      metav1.LabelSelector
-		values        map[string]string
-		expected      []map[string]string
+		name     string
+		selector metav1.LabelSelector
+		values   map[string]string
+		expected []map[string]string
+		// clientError is true if a k8s client error should be simulated
 		clientError   bool
 		expectedError error
 	}{
 		{
-			metav1.LabelSelector{},
-			nil,
-			[]map[string]string{
-				{"name": "cHJvZHVjdGlvbi0wMQ==", "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"},
-				{"name": "c3RhZ2luZy0wMQ==", "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"},
-			},
-			false,
-			nil,
+			name:     "no label selector",
+			selector: metav1.LabelSelector{},
+			values:   nil,
+			expected: []map[string]string{
+				{"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"},
+
+				{"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"},
+
+				{"name": "in-cluster", "server": "https://kubernetes.default.svc"},
+			},
+			clientError:   false,
+			expectedError: nil,
+		},
+		{
+			name: "secret type label selector",
+			selector: metav1.LabelSelector{
+				MatchLabels: map[string]string{
+					"argocd.argoproj.io/secret-type": "cluster",
+				},
+			},
+			values: nil,
+			expected: []map[string]string{
+				{"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"},
+
+				{"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"},
+			},
+			clientError:   false,
+			expectedError: nil,
 		},
 		{
-			metav1.LabelSelector{
+			name: "production-only",
+			selector: metav1.LabelSelector{
 				MatchLabels: map[string]string{
 					"environment": "production",
 				},
 			},
-			map[string]string{
+			values: map[string]string{
 				"foo": "bar",
 			},
-			[]map[string]string{
-				{"values.foo": "bar", "name": "cHJvZHVjdGlvbi0wMQ==", "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"},
+			expected: []map[string]string{
+				{"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"},
 			},
-			false,
-			nil,
+			clientError:   false,
+			expectedError: nil,
 		},
 		{
-			metav1.LabelSelector{
+			name: "production or staging",
+			selector: metav1.LabelSelector{
 				MatchExpressions: []metav1.LabelSelectorRequirement{
 					{
 						Key:      "environment",
@@ -125,18 +155,21 @@ func TestGenerateParams(t *testing.T) {
 					},
 				},
 			},
-			map[string]string{
+			values: map[string]string{
 				"foo": "bar",
 			},
-			[]map[string]string{
-				{"values.foo": "bar", "name": "c3RhZ2luZy0wMQ==", "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"},
-				{"values.foo": "bar", "name": "cHJvZHVjdGlvbi0wMQ==", "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"},
+			expected: []map[string]string{
+				{"values.foo": "bar", "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"},
+				{"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"},
 			},
-			false,
-			nil,
+			clientError:   false,
+			expectedError: nil,
 		},
 		{
-			metav1.LabelSelector{
+			name: "production or staging with match labels",
+			selector: metav1.LabelSelector{
 				MatchExpressions: []metav1.LabelSelectorRequirement{
 					{
 						Key:      "environment",
@@ -151,46 +184,60 @@ func TestGenerateParams(t *testing.T) {
 					"org": "foo",
 				},
 			},
-			map[string]string{
+			values: map[string]string{
 				"name": "baz",
 			},
-			[]map[string]string{
-				{"values.name": "baz", "name": "c3RhZ2luZy0wMQ==", "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"},
+			expected: []map[string]string{
+				{"values.name": "baz", "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"},
 			},
-			false,
-			nil,
+			clientError:   false,
+			expectedError: nil,
 		},
 		{
-			metav1.LabelSelector{},
-			nil,
-			nil,
-			true,
-			errors.New("could not list Secrets"),
+			name:          "simulate client error",
+			selector:      metav1.LabelSelector{},
+			values:        nil,
+			expected:      nil,
+			clientError:   true,
+			expectedError: errors.New("could not list Secrets"),
 		},
 	}
 
+	// convert []client.Object to []runtime.Object, for use by kubefake package
+	runtimeClusters := []runtime.Object{}
+	for _, clientCluster := range clusters {
+		runtimeClusters = append(runtimeClusters, clientCluster)
+	}
+
 	for _, testCase := range testCases {
-		fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build()
-		cl := &possiblyErroringFakeCtrlRuntimeClient{
-			fakeClient,
-			testCase.clientError,
-		}
 
-		var clusterGenerator = NewClusterGenerator(cl)
+		t.Run(testCase.name, func(t *testing.T) {
 
-		got, err := clusterGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
-			Clusters: &argoprojiov1alpha1.ClusterGenerator{
-				Selector: testCase.selector,
-				Values:   testCase.values,
-			},
-		})
+			appClientset := kubefake.NewSimpleClientset(runtimeClusters...)
 
-		if testCase.expectedError != nil {
-			assert.Error(t, testCase.expectedError, err)
-		} else {
-			assert.NoError(t, err)
-			assert.ElementsMatch(t, testCase.expected, got)
-		}
+			fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build()
+			cl := &possiblyErroringFakeCtrlRuntimeClient{
+				fakeClient,
+				testCase.clientError,
+			}
 
+			var clusterGenerator = NewClusterGenerator(cl, context.Background(), appClientset, "namespace")
+
+			got, err := clusterGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
+				Clusters: &argoprojiov1alpha1.ClusterGenerator{
+					Selector: testCase.selector,
+					Values:   testCase.values,
+				},
+			})
+
+			if testCase.expectedError != nil {
+				assert.Error(t, testCase.expectedError, err)
+			} else {
+				assert.NoError(t, err)
+				assert.ElementsMatch(t, testCase.expected, got)
+			}
+
+		})
 	}
 }
diff --git a/test/e2e/applicationset/applicationset_test.go b/test/e2e/applicationset/applicationset_test.go
index 0b20c96..b1a1247 100644
--- a/test/e2e/applicationset/applicationset_test.go
+++ b/test/e2e/applicationset/applicationset_test.go
@@ -85,86 +85,6 @@ func TestSimpleListGenerator(t *testing.T) {
 
 }
 
-func TestSimpleClusterGenerator(t *testing.T) {
-
-	expectedApp := argov1alpha1.Application{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "Application",
-			APIVersion: "argoproj.io/v1alpha1",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      "cluster1-guestbook",
-			Namespace: utils.ArgoCDNamespace,
-		},
-		Spec: argov1alpha1.ApplicationSpec{
-			Project: "default",
-			Source: argov1alpha1.ApplicationSource{
-				RepoURL:        "https://github.com/argoproj/argocd-example-apps.git",
-				TargetRevision: "HEAD",
-				Path:           "guestbook",
-			},
-			Destination: argov1alpha1.ApplicationDestination{
-				Name:      "cluster1",
-				Namespace: "guestbook",
-			},
-		},
-	}
-
-	var expectedAppNewNamespace *argov1alpha1.Application
-
-	Given(t).
-		// Create a ClusterGenerator-based ApplicationSet
-		When().
-		CreateClusterSecret("my-secret", "cluster1", "https://kubernetes.default.svc").
-		Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
-			Name: "simple-cluster-generator",
-		},
-			Spec: v1alpha1.ApplicationSetSpec{
-				Template: v1alpha1.ApplicationSetTemplate{
-					ObjectMeta: metav1.ObjectMeta{Name: "{{name}}-guestbook"},
-					Spec: argov1alpha1.ApplicationSpec{
-						Project: "default",
-						Source: argov1alpha1.ApplicationSource{
-							RepoURL:        "https://github.com/argoproj/argocd-example-apps.git",
-							TargetRevision: "HEAD",
-							Path:           "guestbook",
-						},
-						Destination: argov1alpha1.ApplicationDestination{
-							Name: "{{name}}",
-							// Server:    "{{server}}",
-							Namespace: "guestbook",
-						},
-					},
-				},
-				Generators: []v1alpha1.ApplicationSetGenerator{
-					{
-						Clusters: &v1alpha1.ClusterGenerator{
-							Selector: metav1.LabelSelector{
-								MatchLabels: map[string]string{
-									"argocd.argoproj.io/secret-type": "cluster",
-								},
-							},
-						},
-					},
-				},
-			},
-		}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})).
-
-		// Update the ApplicationSet template namespace, and verify it updates the Applications
-		When().
-		And(func() {
-			expectedAppNewNamespace = expectedApp.DeepCopy()
-			expectedAppNewNamespace.Spec.Destination.Namespace = "guestbook2"
-		}).
-		Update(func(appset *v1alpha1.ApplicationSet) {
-			appset.Spec.Template.Spec.Destination.Namespace = "guestbook2"
-		}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewNamespace})).
-
-		// Delete the ApplicationSet, and verify it deletes the Applications
-		When().
-		Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{*expectedAppNewNamespace}))
-}
-
 func TestSimpleGitGenerator(t *testing.T) {
 
 	generateExpectedApp := func(name string) argov1alpha1.Application {
diff --git a/test/e2e/applicationset/cluster_e2e_test.go b/test/e2e/applicationset/cluster_e2e_test.go
new file mode 100644
index 0000000..6bb7a2b
--- /dev/null
+++ b/test/e2e/applicationset/cluster_e2e_test.go
@@ -0,0 +1,194 @@
+package applicationsets
+
+import (
+	"testing"
+
+	"github.com/argoproj-labs/applicationset/api/v1alpha1"
+	. "github.com/argoproj-labs/applicationset/test/e2e/fixture/applicationsets"
+	"github.com/argoproj-labs/applicationset/test/e2e/fixture/applicationsets/utils"
+	argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestSimpleClusterGenerator(t *testing.T) {
+
+	expectedApp := argov1alpha1.Application{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Application",
+			APIVersion: "argoproj.io/v1alpha1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "cluster1-guestbook",
+			Namespace: utils.ArgoCDNamespace,
+		},
+		Spec: argov1alpha1.ApplicationSpec{
+			Project: "default",
+			Source: argov1alpha1.ApplicationSource{
+				RepoURL:        "https://github.com/argoproj/argocd-example-apps.git",
+				TargetRevision: "HEAD",
+				Path:           "guestbook",
+			},
+			Destination: argov1alpha1.ApplicationDestination{
+				Name:      "cluster1",
+				Namespace: "guestbook",
+			},
+		},
+	}
+
+	var expectedAppNewNamespace *argov1alpha1.Application
+
+	Given(t).
+		// Create a ClusterGenerator-based ApplicationSet
+		When().
+		CreateClusterSecret("my-secret", "cluster1", "https://kubernetes.default.svc").
+		Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
+			Name: "simple-cluster-generator",
+		},
+			Spec: v1alpha1.ApplicationSetSpec{
+				Template: v1alpha1.ApplicationSetTemplate{
+					ObjectMeta: metav1.ObjectMeta{Name: "{{name}}-guestbook"},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "default",
+						Source: argov1alpha1.ApplicationSource{
+							RepoURL:        "https://github.com/argoproj/argocd-example-apps.git",
+							TargetRevision: "HEAD",
+							Path:           "guestbook",
+						},
+						Destination: argov1alpha1.ApplicationDestination{
+							Name: "{{name}}",
+							// Server:    "{{server}}",
+							Namespace: "guestbook",
+						},
+					},
+				},
+				Generators: []v1alpha1.ApplicationSetGenerator{
+					{
+						Clusters: &v1alpha1.ClusterGenerator{
+							Selector: metav1.LabelSelector{
+								MatchLabels: map[string]string{
+									"argocd.argoproj.io/secret-type": "cluster",
+								},
+							},
+						},
+					},
+				},
+			},
+		}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})).
+
+		// Update the ApplicationSet template namespace, and verify it updates the Applications
+		When().
+		And(func() {
+			expectedAppNewNamespace = expectedApp.DeepCopy()
+			expectedAppNewNamespace.Spec.Destination.Namespace = "guestbook2"
+		}).
+		Update(func(appset *v1alpha1.ApplicationSet) {
+			appset.Spec.Template.Spec.Destination.Namespace = "guestbook2"
+		}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewNamespace})).
+
+		// Delete the ApplicationSet, and verify it deletes the Applications
+		When().
+		Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{*expectedAppNewNamespace}))
+}
+
+func TestClusterGeneratorWithLocalCluster(t *testing.T) {
+
+	expectedAppTemplate := argov1alpha1.Application{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Application",
+			APIVersion: "argoproj.io/v1alpha1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "in-cluster-guestbook",
+			Namespace: utils.ArgoCDNamespace,
+		},
+		Spec: argov1alpha1.ApplicationSpec{
+			Project: "default",
+			Source: argov1alpha1.ApplicationSource{
+				RepoURL:        "https://github.com/argoproj/argocd-example-apps.git",
+				TargetRevision: "HEAD",
+				Path:           "guestbook",
+			},
+			// Destination comes from appDestination below
+		},
+	}
+
+	tests := []struct {
+		name              string
+		appsetDestination argov1alpha1.ApplicationDestination
+		appDestination    argov1alpha1.ApplicationDestination
+	}{
+		{
+			name: "specify local cluster by server field",
+			appDestination: argov1alpha1.ApplicationDestination{
+				Server:    "https://kubernetes.default.svc",
+				Namespace: "guestbook",
+			},
+			appsetDestination: argov1alpha1.ApplicationDestination{
+				Server:    "{{server}}",
+				Namespace: "guestbook",
+			},
+		},
+		{
+			name: "specify local cluster by name field",
+			appDestination: argov1alpha1.ApplicationDestination{
+				Name:      "in-cluster",
+				Namespace: "guestbook",
+			},
+			appsetDestination: argov1alpha1.ApplicationDestination{
+				Name:      "{{name}}",
+				Namespace: "guestbook",
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			var expectedAppNewNamespace *argov1alpha1.Application
+
+			// Create the expected application from the template, and copy in the destination from the test case
+			expectedApp := *expectedAppTemplate.DeepCopy()
+			expectedApp.Spec.Destination = test.appDestination
+
+			Given(t).
+				// Create a ClusterGenerator-based ApplicationSet
+				When().
+				Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
+					Name: "in-cluster-generator",
+				},
+					Spec: v1alpha1.ApplicationSetSpec{
+						Template: v1alpha1.ApplicationSetTemplate{
+							ObjectMeta: metav1.ObjectMeta{Name: "{{name}}-guestbook"},
+							Spec: argov1alpha1.ApplicationSpec{
+								Project: "default",
+								Source: argov1alpha1.ApplicationSource{
+									RepoURL:        "https://github.com/argoproj/argocd-example-apps.git",
+									TargetRevision: "HEAD",
+									Path:           "guestbook",
+								},
+								Destination: test.appsetDestination,
+							},
+						},
+						Generators: []v1alpha1.ApplicationSetGenerator{
+							{
+								Clusters: &v1alpha1.ClusterGenerator{},
+							},
+						},
+					},
+				}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})).
+
+				// Update the ApplicationSet template namespace, and verify it updates the Applications
+				When().
+				And(func() {
+					expectedAppNewNamespace = expectedApp.DeepCopy()
+					expectedAppNewNamespace.Spec.Destination.Namespace = "guestbook2"
+				}).
+				Update(func(appset *v1alpha1.ApplicationSet) {
+					appset.Spec.Template.Spec.Destination.Namespace = "guestbook2"
+				}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewNamespace})).
+
+				// Delete the ApplicationSet, and verify it deletes the Applications
+				When().
+				Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{*expectedAppNewNamespace}))
+		})
+	}
+}
diff --git a/test/e2e/fixture/applicationsets/actions.go b/test/e2e/fixture/applicationsets/actions.go
index dffb240..75117f8 100644
--- a/test/e2e/fixture/applicationsets/actions.go
+++ b/test/e2e/fixture/applicationsets/actions.go
@@ -100,8 +100,9 @@ func (a *Actions) Create(appSet v1alpha1.ApplicationSet) *Actions {
 func (a *Actions) Delete() *Actions {
 	a.context.t.Helper()
 
-	err := utils.AppSetClientset.Delete(context.Background(), a.context.name, metav1.DeleteOptions{})
-	a.describeAction = fmt.Sprintf("deleting ApplicationSet '%s'", a.context.name)
+	deleteProp := metav1.DeletePropagationForeground
+	err := utils.AppSetClientset.Delete(context.Background(), a.context.name, metav1.DeleteOptions{PropagationPolicy: &deleteProp})
+	a.describeAction = fmt.Sprintf("Deleting ApplicationSet '%s' %v", a.context.name, err)
 	a.lastOutput, a.lastError = "", err
 	a.verifyAction()
 
diff --git a/test/e2e/fixture/applicationsets/consequences.go b/test/e2e/fixture/applicationsets/consequences.go
index 6ec2acc..c2e43f9 100644
--- a/test/e2e/fixture/applicationsets/consequences.go
+++ b/test/e2e/fixture/applicationsets/consequences.go
@@ -33,7 +33,7 @@ func (c *Consequences) Expect(e Expectation) *Consequences {
 			c.context.t.Fatalf("failed expectation: %s", message)
 			return c
 		}
-		log.Infof("pending: %s", message)
+		log.Infof("expectation pending: %s", message)
 	}
 	c.context.t.Fatal("timeout waiting for: " + message)
 	return c