Repository: argoproj-labs/applicationset (go)

Issue:
Implement Git files generator E2E tests
Now that [initial Git files generator functionality](https://github.com/argoproj-labs/applicationset/pull/45) has merged, we should add some E2E tests for it.

The E2E test framework allows us to test ApplicationSet features against a live Kubernetes cluster and a live Argo CD instance. This issue tracks adding additional tests that specifically target the git file generator.


Pull request description:
Add E2E tests for the git file generator
closes #104

Patch:
diff --git a/api/v1alpha1/applicationset_types.go b/api/v1alpha1/applicationset_types.go
index 6e6c58f..ed4ed1b 100644
--- a/api/v1alpha1/applicationset_types.go
+++ b/api/v1alpha1/applicationset_types.go
@@ -98,7 +98,7 @@ type GitGenerator struct {
 	Directories         []GitDirectoryGeneratorItem `json:"directories,omitempty"`
 	Files               []GitFileGeneratorItem      `json:"files,omitempty"`
 	Revision            string                      `json:"revision"`
-	RequeueAfterSeconds int64                       `json:"requeueAfterSeconds,omitempty"`
+	RequeueAfterSeconds *int64                      `json:"requeueAfterSeconds,omitempty"`
 	Template            ApplicationSetTemplate      `json:"template,omitempty"`
 }
 
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 05ddb1e..4353e89 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -284,6 +284,11 @@ func (in *GitGenerator) DeepCopyInto(out *GitGenerator) {
 		*out = make([]GitFileGeneratorItem, len(*in))
 		copy(*out, *in)
 	}
+	if in.RequeueAfterSeconds != nil {
+		in, out := &in.RequeueAfterSeconds, &out.RequeueAfterSeconds
+		*out = new(int64)
+		**out = **in
+	}
 	in.Template.DeepCopyInto(&out.Template)
 }
 
diff --git a/go.mod b/go.mod
index 6a2bed8..73b4ca4 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,6 @@ require (
 	github.com/sirupsen/logrus v1.6.0
 	github.com/stretchr/testify v1.6.1
 	github.com/valyala/fasttemplate v1.2.1
-	google.golang.org/grpc v1.29.1
 	k8s.io/api v0.19.2
 	k8s.io/apimachinery v0.19.2
 	k8s.io/client-go v11.0.1-0.20190816222228-6d55c1b1f1ca+incompatible
diff --git a/main.go b/main.go
index f055fb8..33fa32e 100644
--- a/main.go
+++ b/main.go
@@ -31,11 +31,14 @@ import (
 	"github.com/argoproj-labs/applicationset/pkg/utils"
 
 	argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
+	"github.com/argoproj/argo-cd/util/db"
+	argosettings "github.com/argoproj/argo-cd/util/settings"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/kubernetes"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
 
+	appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/cache"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
@@ -124,19 +127,25 @@ func main() {
 	}
 
 	k8s := kubernetes.NewForConfigOrDie(mgr.GetConfig())
+	argoSettingsMgr := argosettings.NewSettingsManager(context.Background(), k8s, namespace)
+	appclientset := appclientset.NewForConfigOrDie(mgr.GetConfig())
+
+	argoCDDB := db.NewDB(namespace, argoSettingsMgr, k8s)
 
 	if err = (&controllers.ApplicationSetReconciler{
 		Generators: map[string]generators.Generator{
 			"List":     generators.NewListGenerator(),
 			"Clusters": generators.NewClusterGenerator(mgr.GetClient(), context.Background(), k8s, namespace),
-			"Git":      generators.NewGitGenerator(services.NewArgoCDService(context.Background(), k8s, namespace, argocdRepoServer)),
+			"Git":      generators.NewGitGenerator(services.NewArgoCDService(argoCDDB, argocdRepoServer)),
 		},
-		Client:   mgr.GetClient(),
-		Log:      ctrl.Log.WithName("controllers").WithName("ApplicationSet"),
-		Scheme:   mgr.GetScheme(),
-		Recorder: mgr.GetEventRecorderFor("applicationset-controller"),
-		Renderer: &utils.Render{},
-		Policy:   policyObj,
+		Client:           mgr.GetClient(),
+		Log:              ctrl.Log.WithName("controllers").WithName("ApplicationSet"),
+		Scheme:           mgr.GetScheme(),
+		Recorder:         mgr.GetEventRecorderFor("applicationset-controller"),
+		Renderer:         &utils.Render{},
+		Policy:           policyObj,
+		ArgoAppClientset: appclientset,
+		ArgoDB:           argoCDDB,
 	}).SetupWithManager(mgr); err != nil {
 		setupLog.Error(err, "unable to create controller", "controller", "ApplicationSet")
 		os.Exit(1)
diff --git a/manifests/base/rbac.yaml b/manifests/base/rbac.yaml
index fb74c53..cb8a910 100644
--- a/manifests/base/rbac.yaml
+++ b/manifests/base/rbac.yaml
@@ -22,6 +22,7 @@ rules:
       - argoproj.io
     resources:
       - applications
+      - appprojects
       - applicationsets
       - applicationsets/finalizers
     verbs:
diff --git a/manifests/install-with-argo-cd.yaml b/manifests/install-with-argo-cd.yaml
index c4915f1..36b3355 100644
--- a/manifests/install-with-argo-cd.yaml
+++ b/manifests/install-with-argo-cd.yaml
@@ -3450,6 +3450,7 @@ rules:
   - argoproj.io
   resources:
   - applications
+  - appprojects
   - applicationsets
   - applicationsets/finalizers
   verbs:
diff --git a/manifests/install.yaml b/manifests/install.yaml
index 827631a..3dac609 100644
--- a/manifests/install.yaml
+++ b/manifests/install.yaml
@@ -1442,6 +1442,7 @@ rules:
   - argoproj.io
   resources:
   - applications
+  - appprojects
   - applicationsets
   - applicationsets/finalizers
   verbs:
diff --git a/pkg/controllers/applicationset_controller.go b/pkg/controllers/applicationset_controller.go
index 0675980..01fdcc5 100644
--- a/pkg/controllers/applicationset_controller.go
+++ b/pkg/controllers/applicationset_controller.go
@@ -28,6 +28,8 @@ import (
 	"github.com/argoproj-labs/applicationset/pkg/generators"
 	"github.com/argoproj-labs/applicationset/pkg/utils"
 	argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
+	"github.com/argoproj/argo-cd/util/argo"
+	"github.com/argoproj/argo-cd/util/db"
 	"github.com/go-logr/logr"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -43,6 +45,10 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	argoprojiov1alpha1 "github.com/argoproj-labs/applicationset/api/v1alpha1"
+	appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
+	argoutil "github.com/argoproj/argo-cd/util/argo"
+
+	apierr "k8s.io/apimachinery/pkg/api/errors"
 
 	"github.com/imdario/mergo"
 )
@@ -50,10 +56,12 @@ import (
 // ApplicationSetReconciler reconciles a ApplicationSet object
 type ApplicationSetReconciler struct {
 	client.Client
-	Log        logr.Logger
-	Scheme     *runtime.Scheme
-	Recorder   record.EventRecorder
-	Generators map[string]generators.Generator
+	Log              logr.Logger
+	Scheme           *runtime.Scheme
+	Recorder         record.EventRecorder
+	Generators       map[string]generators.Generator
+	ArgoDB           db.ArgoDB
+	ArgoAppClientset appclientset.Interface
 	utils.Policy
 	utils.Renderer
 }
@@ -81,7 +89,8 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 	if err != nil {
 		return ctrl.Result{}, err
 	}
-	if hasDuplicates, name := hasDuplicateNames(desiredApplications); hasDuplicates {
+
+	if validateError := r.validateGeneratedApplications(ctx, desiredApplications, applicationSetInfo, req.Namespace); validateError != nil {
 		// The reconciler presumes that any errors that are returned are a signal
 		// that the resource should attempt to be reconciled again (causing
 		// Reconcile to be called again, which will return the same error, ad
@@ -93,7 +102,7 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		// will fail. So just log it and return that the resource was
 		// successfully reconciled (which is true... it was reconciled to an
 		// error condition).
-		log.Errorf("ApplicationSet %s contains applications with duplicate name: %s", applicationSetInfo.Name, name)
+		log.Errorf("%s", validateError.Error())
 		return ctrl.Result{}, nil
 	}
 
@@ -124,6 +133,41 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 	}, nil
 }
 
+// validateGeneratedApplications uses the Argo CD validation functions to verify the correctness of the
+// generated applications.
+func (r *ApplicationSetReconciler) validateGeneratedApplications(ctx context.Context, desiredApplications []argov1alpha1.Application, applicationSetInfo argoprojiov1alpha1.ApplicationSet, namespace string) (err error) {
+
+	if hasDuplicates, name := hasDuplicateNames(desiredApplications); hasDuplicates {
+		return fmt.Errorf("ApplicationSet %s contains applications with duplicate name: %s", applicationSetInfo.Name, name)
+	}
+
+	for _, app := range desiredApplications {
+
+		proj, err := r.ArgoAppClientset.ArgoprojV1alpha1().AppProjects(namespace).Get(ctx, app.Spec.GetProject(), metav1.GetOptions{})
+		if err != nil {
+			if apierr.IsNotFound(err) {
+				return fmt.Errorf("application references project %s which does not exist", app.Spec.Project)
+			}
+			return err
+		}
+
+		if err := argoutil.ValidateDestination(ctx, &app.Spec.Destination, r.ArgoDB); err != nil {
+			return fmt.Errorf("application destination spec is invalid: %s", err.Error())
+		}
+
+		conditions, err := argoutil.ValidatePermissions(ctx, &app.Spec, proj, r.ArgoDB)
+		if err != nil {
+			return err
+		}
+		if len(conditions) > 0 {
+			return fmt.Errorf("application spec is invalid: %s", argo.FormatAppConditions(conditions))
+		}
+
+	}
+
+	return nil
+}
+
 // Log a warning if there are unrecognized generators
 func checkInvalidGenerators(applicationSetInfo *argoprojiov1alpha1.ApplicationSet) {
 	hasInvalidGenerators, invalidGenerators := invalidGenerators(applicationSetInfo)
diff --git a/pkg/generators/git.go b/pkg/generators/git.go
index 9c58fb0..4f00617 100644
--- a/pkg/generators/git.go
+++ b/pkg/generators/git.go
@@ -3,6 +3,7 @@ package generators
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"path"
 	"sort"
 	"time"
@@ -15,6 +16,10 @@ import (
 
 var _ Generator = (*GitGenerator)(nil)
 
+const (
+	DefaultRequeueAfterSeconds = 3 * time.Minute
+)
+
 type GitGenerator struct {
 	repos services.Repos
 }
@@ -31,7 +36,14 @@ func (g *GitGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.Applicati
 }
 
 func (g *GitGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
-	return time.Duration(appSetGenerator.Git.RequeueAfterSeconds) * time.Second
+
+	// Return a requeue default of 3 minutes, if no default is specified.
+
+	if appSetGenerator.Git.RequeueAfterSeconds != nil {
+		return time.Duration(*appSetGenerator.Git.RequeueAfterSeconds) * time.Second
+	}
+
+	return DefaultRequeueAfterSeconds
 }
 
 func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) ([]map[string]string, error) {
@@ -61,19 +73,21 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
 }
 
 func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) ([]map[string]string, error) {
-	allApps, err := g.repos.GetApps(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision)
+
+	// Directories, not files
+	allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision)
 	if err != nil {
 		return nil, err
 	}
 
 	log.WithFields(log.Fields{
-		"allAps":   allApps,
-		"total":    len(allApps),
+		"allPaths": allPaths,
+		"total":    len(allPaths),
 		"repoURL":  appSetGenerator.Git.RepoURL,
 		"revision": appSetGenerator.Git.Revision,
 	}).Info("applications result from the repo service")
 
-	requestedApps := g.filterApps(appSetGenerator.Git.Directories, allApps)
+	requestedApps := g.filterApps(appSetGenerator.Git.Directories, allPaths)
 
 	res := g.generateParamsFromApps(requestedApps, appSetGenerator)
 
@@ -85,7 +99,7 @@ func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1al
 	// Get all paths that match the requested path string, removing duplicates
 	allPathsMap := make(map[string]bool)
 	for _, requestedPath := range appSetGenerator.Git.Files {
-		paths, err := g.repos.GetPaths(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, requestedPath.Path)
+		paths, err := g.repos.GetFilePaths(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, requestedPath.Path)
 		if err != nil {
 			return nil, err
 		}
@@ -105,38 +119,59 @@ func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1al
 	// Generate params from each path, and return
 	res := []map[string]string{}
 	for _, path := range allPaths {
-		params, err := g.generateParamsFromGitFile(appSetGenerator, path)
+
+		// A JSON file path can contain multiple sets of parameters (ie it is an array)
+		paramsArray, err := g.generateParamsFromGitFile(appSetGenerator, path)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("unable to process file '%s': %v", path, err)
 		}
 
-		res = append(res, params)
+		for index := range paramsArray {
+			res = append(res, paramsArray[index])
+		}
 	}
 	return res, nil
 }
 
-func (g *GitGenerator) generateParamsFromGitFile(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, path string) (map[string]string, error) {
-	content, err := g.repos.GetFileContent(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, path)
-	if err != nil {
-		return nil, err
-	}
+func (g *GitGenerator) generateParamsFromGitFile(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, path string) ([]map[string]string, error) {
 
-	config := make(map[string]interface{})
-	err = json.Unmarshal(content, &config)
+	fileContent, err := g.repos.GetFileContent(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, path)
 	if err != nil {
 		return nil, err
 	}
 
-	flat, err := flatten.Flatten(config, "", flatten.DotStyle)
+	objectsFound := []map[string]interface{}{}
+
+	// First, we attempt to parse as an array
+	err = json.Unmarshal(fileContent, &objectsFound)
 	if err != nil {
-		return nil, err
+		// If unable to parse as an array, attempt to parse as a single JSON object
+		singleJSONObj := make(map[string]interface{})
+		err = json.Unmarshal(fileContent, &singleJSONObj)
+		if err != nil {
+			return nil, fmt.Errorf("unable to parse JSON file: %v", err)
+		}
+		objectsFound = append(objectsFound, singleJSONObj)
 	}
-	params := make(map[string]string)
-	for k, v := range flat {
-		params[k] = v.(string)
+
+	res := []map[string]string{}
+
+	// Flatten all JSON objects found, and return them
+	for _, objectFound := range objectsFound {
+
+		flat, err := flatten.Flatten(objectFound, "", flatten.DotStyle)
+		if err != nil {
+			return nil, err
+		}
+		params := map[string]string{}
+		for k, v := range flat {
+			params[k] = v.(string)
+		}
+		res = append(res, params)
 	}
 
-	return params, nil
+	return res, nil
+
 }
 
 func (g *GitGenerator) filterApps(Directories []argoprojiov1alpha1.GitDirectoryGeneratorItem, allApps []string) []string {
diff --git a/pkg/services/repo_service.go b/pkg/services/repo_service.go
index 3496324..f8728fe 100644
--- a/pkg/services/repo_service.go
+++ b/pkg/services/repo_service.go
@@ -3,17 +3,14 @@ package services
 import (
 	"context"
 	"io/ioutil"
+	"os"
 	"path/filepath"
 
 	"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/argo-cd/reposerver/apiclient"
 	"github.com/argoproj/argo-cd/util/db"
 	"github.com/argoproj/argo-cd/util/git"
-	"github.com/argoproj/argo-cd/util/io"
-	"github.com/argoproj/argo-cd/util/settings"
 	"github.com/pkg/errors"
-	log "github.com/sirupsen/logrus"
-	"k8s.io/client-go/kubernetes"
 )
 
 // RepositoryDB Is a lean facade for ArgoDB,
@@ -28,59 +25,58 @@ type argoCDService struct {
 }
 
 type Repos interface {
-	GetApps(ctx context.Context, repoURL string, revision string) ([]string, error)
-	GetPaths(ctx context.Context, repoURL string, revision string, pattern string) ([]string, error)
+
+	// GetFilePaths returns a list of files (not directories) within the target repo
+	GetFilePaths(ctx context.Context, repoURL string, revision string, pattern string) ([]string, error)
+
+	// GetDirectories returns a list of directories (not files) within the target repo
+	GetDirectories(ctx context.Context, repoURL string, revision string) ([]string, error)
+
+	// GetFileContent returns the contents of a particular repository file
 	GetFileContent(ctx context.Context, repoURL string, revision string, path string) ([]byte, error)
 }
 
-func NewArgoCDService(ctx context.Context, clientset kubernetes.Interface, namespace string, repoServerAddress string) Repos {
-	settingsMgr := settings.NewSettingsManager(ctx, clientset, namespace)
+func NewArgoCDService(db db.ArgoDB, repoServerAddress string) Repos {
 
 	return &argoCDService{
-		repositoriesDB: db.NewDB(namespace, settingsMgr, clientset).(RepositoryDB),
+		repositoriesDB: db.(RepositoryDB),
 		repoClientset:  apiclient.NewRepoServerClientset(repoServerAddress, 5),
 	}
 }
 
-func (a *argoCDService) GetApps(ctx context.Context, repoURL string, revision string) ([]string, error) {
+func (a *argoCDService) GetFilePaths(ctx context.Context, repoURL string, revision string, pattern string) ([]string, error) {
 	repo, err := a.repositoriesDB.GetRepository(ctx, repoURL)
 	if err != nil {
-
 		return nil, errors.Wrap(err, "Error in GetRepository")
 	}
 
-	conn, repoClient, err := a.repoClientset.NewRepoServerClient()
-	defer io.Close(conn)
+	gitRepoClient, err := git.NewClient(repo.Repo, repo.GetGitCreds(), repo.IsInsecure(), repo.IsLFSEnabled())
+
 	if err != nil {
-		return nil, errors.Wrap(err, "Error in creating repo service client")
+		return nil, err
 	}
 
-	apps, err := repoClient.ListApps(ctx, &apiclient.ListAppsRequest{
-		Repo:     repo,
-		Revision: revision,
-	})
-	log.Debugf("apps - %#v", apps)
+	err = checkoutRepo(gitRepoClient, revision)
 	if err != nil {
-		return nil, errors.Wrap(err, "Error in ListApps")
+		return nil, err
 	}
 
-	res := []string{}
-
-	for name := range apps.Apps {
-		res = append(res, name)
+	paths, err := gitRepoClient.LsFiles(pattern)
+	if err != nil {
+		return nil, errors.Wrap(err, "Error during listing files of local repo")
 	}
 
-	return res, nil
+	return paths, nil
 }
 
-func (a *argoCDService) GetPaths(ctx context.Context, repoURL string, revision string, pattern string) ([]string, error) {
+func (a *argoCDService) GetDirectories(ctx context.Context, repoURL string, revision string) ([]string, error) {
+
 	repo, err := a.repositoriesDB.GetRepository(ctx, repoURL)
 	if err != nil {
 		return nil, errors.Wrap(err, "Error in GetRepository")
 	}
 
 	gitRepoClient, err := git.NewClient(repo.Repo, repo.GetGitCreds(), repo.IsInsecure(), repo.IsLFSEnabled())
-
 	if err != nil {
 		return nil, err
 	}
@@ -90,12 +86,41 @@ func (a *argoCDService) GetPaths(ctx context.Context, repoURL string, revision s
 		return nil, err
 	}
 
-	paths, err := gitRepoClient.LsFiles(pattern)
-	if err != nil {
-		return nil, errors.Wrap(err, "Error during listing files of local repo")
+	filteredPaths := []string{}
+
+	repoRoot := gitRepoClient.Root()
+
+	if err := filepath.Walk(repoRoot, func(path string, info os.FileInfo, fnErr error) error {
+		if fnErr != nil {
+			return fnErr
+		}
+		if !info.IsDir() { // Skip files: directories only
+			return nil
+		}
+
+		fname := info.Name()
+		if fname == ".git" { // Skip repository metadata
+			return filepath.SkipDir
+		}
+
+		relativePath, err := filepath.Rel(repoRoot, path)
+		if err != nil {
+			return err
+		}
+
+		if relativePath == "." { // Exclude '.' from results
+			return nil
+		}
+
+		filteredPaths = append(filteredPaths, relativePath)
+
+		return nil
+	}); err != nil {
+		return nil, err
 	}
 
-	return paths, nil
+	return filteredPaths, nil
+
 }
 
 func (a *argoCDService) GetFileContent(ctx context.Context, repoURL string, revision string, path string) ([]byte, error) {
@@ -126,7 +151,7 @@ func (a *argoCDService) GetFileContent(ctx context.Context, repoURL string, revi
 func checkoutRepo(gitRepoClient git.Client, revision string) error {
 	err := gitRepoClient.Init()
 	if err != nil {
-		return errors.Wrap(err, "Error during initiliazing repo")
+		return errors.Wrap(err, "Error during initializing repo")
 	}
 
 	err = gitRepoClient.Fetch()


Test patch:
diff --git a/pkg/controllers/applicationset_controller_test.go b/pkg/controllers/applicationset_controller_test.go
index c086d53..8ad98d1 100644
--- a/pkg/controllers/applicationset_controller_test.go
+++ b/pkg/controllers/applicationset_controller_test.go
@@ -4,10 +4,12 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"strings"
 	"testing"
 	"time"
 
 	"github.com/argoproj-labs/applicationset/pkg/generators"
+	"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
 	argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
 	"github.com/sirupsen/logrus"
 	logtest "github.com/sirupsen/logrus/hooks/test"
@@ -22,6 +24,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
 	argoprojiov1alpha1 "github.com/argoproj-labs/applicationset/api/v1alpha1"
+	appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
+	dbmocks "github.com/argoproj/argo-cd/util/db/mocks"
 )
 
 type generatorMock struct {
@@ -1302,3 +1306,201 @@ func TestHasDuplicateNames(t *testing.T) {
 		assert.Equal(t, c.duplicateName, name)
 	}
 }
+
+func TestValidateGeneratedApplications(t *testing.T) {
+
+	scheme := runtime.NewScheme()
+	err := argoprojiov1alpha1.AddToScheme(scheme)
+	assert.Nil(t, err)
+
+	err = argov1alpha1.AddToScheme(scheme)
+	assert.Nil(t, err)
+
+	client := fake.NewClientBuilder().WithScheme(scheme).Build()
+
+	// Valid cluster
+	myCluster := argov1alpha1.Cluster{
+		Server: "https://kubernetes.default.svc",
+		Name:   "my-cluster",
+	}
+
+	// Valid project
+	myProject := &argov1alpha1.AppProject{
+		ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "namespace"},
+		Spec: argov1alpha1.AppProjectSpec{
+			SourceRepos: []string{"*"},
+			Destinations: []argov1alpha1.ApplicationDestination{
+				{
+					Namespace: "*",
+					Server:    "*",
+				},
+			},
+			ClusterResourceWhitelist: []metav1.GroupKind{
+				{
+					Group: "*",
+					Kind:  "*",
+				},
+			},
+		},
+	}
+
+	// Test a subset of the validations that 'validateGeneratedApplications' performs
+	for _, cc := range []struct {
+		name           string
+		apps           []argov1alpha1.Application
+		expectedErrors []string
+	}{
+		{
+			name: "valid app should return true",
+			apps: []argov1alpha1.Application{
+				{
+					TypeMeta:   metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "default",
+						Source: argov1alpha1.ApplicationSource{
+							RepoURL:        "https://url",
+							Path:           "/",
+							TargetRevision: "HEAD",
+						},
+						Destination: argov1alpha1.ApplicationDestination{
+							Namespace: "namespace",
+							Name:      "my-cluster",
+						},
+					},
+				},
+			},
+			expectedErrors: []string{},
+		},
+		{
+			name: "can't have both name and server defined",
+			apps: []argov1alpha1.Application{
+				{
+					TypeMeta:   metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "default",
+						Source: argov1alpha1.ApplicationSource{
+							RepoURL:        "https://url",
+							Path:           "/",
+							TargetRevision: "HEAD",
+						},
+						Destination: argov1alpha1.ApplicationDestination{
+							Namespace: "namespace",
+							Server:    "my-server",
+							Name:      "my-cluster",
+						},
+					},
+				},
+			},
+			expectedErrors: []string{"application destination can't have both name and server defined"},
+		},
+		{
+			name: "project mismatch should return error",
+			apps: []argov1alpha1.Application{
+				{
+					TypeMeta:   metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "DOES-NOT-EXIST",
+						Source: argov1alpha1.ApplicationSource{
+							RepoURL:        "https://url",
+							Path:           "/",
+							TargetRevision: "HEAD",
+						},
+						Destination: argov1alpha1.ApplicationDestination{
+							Namespace: "namespace",
+							Name:      "my-cluster",
+						},
+					},
+				},
+			},
+			expectedErrors: []string{"application references project DOES-NOT-EXIST which does not exist"},
+		},
+		{
+			name: "valid app should return true",
+			apps: []argov1alpha1.Application{
+				{
+					TypeMeta:   metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "default",
+						Source: argov1alpha1.ApplicationSource{
+							RepoURL:        "https://url",
+							Path:           "/",
+							TargetRevision: "HEAD",
+						},
+						Destination: argov1alpha1.ApplicationDestination{
+							Namespace: "namespace",
+							Name:      "my-cluster",
+						},
+					},
+				},
+			},
+			expectedErrors: []string{},
+		},
+		{
+			name: "cluster should match",
+			apps: []argov1alpha1.Application{
+				{
+					TypeMeta:   metav1.TypeMeta{},
+					ObjectMeta: metav1.ObjectMeta{},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "default",
+						Source: argov1alpha1.ApplicationSource{
+							RepoURL:        "https://url",
+							Path:           "/",
+							TargetRevision: "HEAD",
+						},
+						Destination: argov1alpha1.ApplicationDestination{
+							Namespace: "namespace",
+							Name:      "nonexistent-cluster",
+						},
+					},
+				},
+			},
+			expectedErrors: []string{"there are no clusters with this name: nonexistent-cluster"},
+		},
+	} {
+
+		t.Run(cc.name, func(t *testing.T) {
+
+			argoDBMock := dbmocks.ArgoDB{}
+			argoDBMock.On("GetCluster", mock.Anything, "https://kubernetes.default.svc").Return(&myCluster, nil)
+			argoDBMock.On("ListClusters", mock.Anything).Return(&v1alpha1.ClusterList{Items: []argov1alpha1.Cluster{
+				myCluster,
+			}}, nil)
+
+			argoObjs := []runtime.Object{myProject}
+			for _, app := range cc.apps {
+				argoObjs = append(argoObjs, &app)
+			}
+
+			r := ApplicationSetReconciler{
+				Client:           client,
+				Scheme:           scheme,
+				Recorder:         record.NewFakeRecorder(1),
+				Generators:       map[string]generators.Generator{},
+				ArgoDB:           &argoDBMock,
+				ArgoAppClientset: appclientset.NewSimpleClientset(argoObjs...),
+			}
+
+			appSetInfo := argoprojiov1alpha1.ApplicationSet{}
+
+			err := r.validateGeneratedApplications(context.TODO(), cc.apps, appSetInfo, "namespace")
+
+			if err == nil {
+				assert.Equal(t, len(cc.expectedErrors), 0, "Expected errors but none were seen")
+			} else {
+				// An error was returned: it should be expected
+				matched := false
+				for _, expectedErr := range cc.expectedErrors {
+					foundMatch := strings.Contains(err.Error(), expectedErr)
+					assert.True(t, foundMatch, "Unble to locate expected error: %s", cc.expectedErrors)
+					matched = matched || foundMatch
+				}
+				assert.True(t, matched, "An unexpected error occurrred: %v", err)
+			}
+		})
+	}
+}
diff --git a/pkg/generators/git_test.go b/pkg/generators/git_test.go
index ea49916..f34bd33 100644
--- a/pkg/generators/git_test.go
+++ b/pkg/generators/git_test.go
@@ -29,7 +29,7 @@ func (a argoCDServiceMock) GetApps(ctx context.Context, repoURL string, revision
 	return args.Get(0).([]string), args.Error(1)
 }
 
-func (a argoCDServiceMock) GetPaths(ctx context.Context, repoURL string, revision string, pattern string) ([]string, error) {
+func (a argoCDServiceMock) GetFilePaths(ctx context.Context, repoURL string, revision string, pattern string) ([]string, error) {
 	args := a.mock.Called(ctx, repoURL, revision, pattern)
 
 	return args.Get(0).([]string), args.Error(1)
@@ -41,6 +41,11 @@ func (a argoCDServiceMock) GetFileContent(ctx context.Context, repoURL string, r
 	return args.Get(0).([]byte), args.Error(1)
 }
 
+func (a argoCDServiceMock) GetDirectories(ctx context.Context, repoURL string, revision string) ([]string, error) {
+	args := a.mock.Called(ctx, repoURL, revision)
+	return args.Get(0).([]string), args.Error(1)
+}
+
 func TestGitGenerateParamsFromDirectories(t *testing.T) {
 
 	cases := []struct {
@@ -104,7 +109,8 @@ func TestGitGenerateParamsFromDirectories(t *testing.T) {
 		cc := c
 		t.Run(cc.name, func(t *testing.T) {
 			argoCDServiceMock := argoCDServiceMock{mock: &mock.Mock{}}
-			argoCDServiceMock.mock.On("GetApps", mock.Anything, mock.Anything, mock.Anything).Return(c.repoApps, c.repoError)
+
+			argoCDServiceMock.mock.On("GetDirectories", mock.Anything, mock.Anything, mock.Anything).Return(c.repoApps, c.repoError)
 
 			var gitGenerator = NewGitGenerator(argoCDServiceMock)
 			applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
@@ -140,11 +146,16 @@ func TestGitGenerateParamsFromDirectories(t *testing.T) {
 func TestGitGenerateParamsFromFiles(t *testing.T) {
 
 	cases := []struct {
-		name                   string
-		files                  []argoprojiov1alpha1.GitFileGeneratorItem
-		repoPaths              []string
-		repoFileContents       map[string][]byte
-		repoPathsError         error
+		name string
+		// files is the list of paths/globs to match
+		files []argoprojiov1alpha1.GitFileGeneratorItem
+		// repoPaths is the list of matching paths in the simulated git repository
+		repoPaths []string
+		// repoFileContents maps repo path to the literal contents of that path
+		repoFileContents map[string][]byte
+		// if repoPathsError is non-nil, the call to GetPaths(...) will return this error value
+		repoPathsError error
+		// if repoFileContentsErrors contains a path key, the error value will be returned on the call to GetFileContents(...)
 		repoFileContentsErrors map[string]error
 		expected               []map[string]string
 		expectedError          error
@@ -231,7 +242,66 @@ func TestGitGenerateParamsFromFiles(t *testing.T) {
 				"cluster-config/staging/config.json":    fmt.Errorf("staging config file get content error"),
 			},
 			expected:      []map[string]string{},
-			expectedError: fmt.Errorf("staging config file get content error"),
+			expectedError: fmt.Errorf("unable to process file 'cluster-config/staging/config.json': staging config file get content error"),
+		},
+		{
+			name:  "test invalid JSON file returns error",
+			files: []argoprojiov1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
+			repoPaths: []string{
+				"cluster-config/production/config.json",
+			},
+			repoFileContents: map[string][]byte{
+				"cluster-config/production/config.json": []byte(`invalid json file`),
+			},
+			repoPathsError:         nil,
+			repoFileContentsErrors: map[string]error{},
+			expected:               []map[string]string{},
+			expectedError:          fmt.Errorf("unable to process file 'cluster-config/production/config.json': unable to parse JSON file: invalid character 'i' looking for beginning of value"),
+		},
+		{
+			name:  "test JSON array",
+			files: []argoprojiov1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
+			repoPaths: []string{
+				"cluster-config/production/config.json",
+			},
+			repoFileContents: map[string][]byte{
+				"cluster-config/production/config.json": []byte(`
+				[
+					{
+						"cluster": {
+							"owner": "john.doe@example.com",
+							"name": "production",
+							"address": "https://kubernetes.default.svc",
+							"inner": {
+								"one" : "two"
+							}
+						}
+					},
+					{
+						"cluster": {
+							"owner": "john.doe@example.com",
+							"name": "staging",
+							"address": "https://kubernetes.default.svc"
+						}
+					}
+				]`),
+			},
+			repoPathsError:         nil,
+			repoFileContentsErrors: map[string]error{},
+			expected: []map[string]string{
+				{
+					"cluster.owner":     "john.doe@example.com",
+					"cluster.name":      "production",
+					"cluster.address":   "https://kubernetes.default.svc",
+					"cluster.inner.one": "two",
+				},
+				{
+					"cluster.owner":   "john.doe@example.com",
+					"cluster.name":    "staging",
+					"cluster.address": "https://kubernetes.default.svc",
+				},
+			},
+			expectedError: nil,
 		},
 	}
 
@@ -239,12 +309,12 @@ func TestGitGenerateParamsFromFiles(t *testing.T) {
 		cc := c
 		t.Run(cc.name, func(t *testing.T) {
 			argoCDServiceMock := argoCDServiceMock{mock: &mock.Mock{}}
-			argoCDServiceMock.mock.On("GetPaths", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(c.repoPaths, c.repoPathsError)
-
+			argoCDServiceMock.mock.On("GetFilePaths", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
+				Return(c.repoPaths, c.repoPathsError)
 			if c.repoPaths != nil {
 				for _, repoPath := range c.repoPaths {
-					fmt.Println("repoPath: ", repoPath)
-					argoCDServiceMock.mock.On("GetFileContent", mock.Anything, mock.Anything, mock.Anything, repoPath).Return(c.repoFileContents[repoPath], c.repoFileContentsErrors[repoPath]).Once()
+					argoCDServiceMock.mock.On("GetFileContent", mock.Anything, mock.Anything, mock.Anything, repoPath).
+						Return(c.repoFileContents[repoPath], c.repoFileContentsErrors[repoPath]).Once()
 				}
 			}
 
diff --git a/pkg/services/repo_service_test.go b/pkg/services/repo_service_test.go
index a53707b..1186968 100644
--- a/pkg/services/repo_service_test.go
+++ b/pkg/services/repo_service_test.go
@@ -11,7 +11,6 @@ import (
 	"github.com/argoproj/argo-cd/util/io"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
-	"google.golang.org/grpc"
 )
 
 type ArgocdRepositoryMock struct {
@@ -25,33 +24,6 @@ func (a ArgocdRepositoryMock) GetRepository(ctx context.Context, url string) (*v
 
 }
 
-type repoServerClientMock struct {
-	mock *mock.Mock
-}
-
-func (r repoServerClientMock) GenerateManifest(ctx context.Context, in *apiclient.ManifestRequest, opts ...grpc.CallOption) (*apiclient.ManifestResponse, error) {
-	return nil, nil
-}
-func (r repoServerClientMock) ListApps(ctx context.Context, in *apiclient.ListAppsRequest, opts ...grpc.CallOption) (*apiclient.AppList, error) {
-	args := r.mock.Called(ctx, in)
-
-	return args.Get(0).(*apiclient.AppList), args.Error(1)
-}
-func (r repoServerClientMock) ListRefs(ctx context.Context, in *apiclient.ListRefsRequest, opts ...grpc.CallOption) (*apiclient.Refs, error) {
-	args := r.mock.Called(ctx, in)
-
-	return args.Get(0).(*apiclient.Refs), args.Error(1)
-}
-func (r repoServerClientMock) GetAppDetails(ctx context.Context, in *apiclient.RepoServerAppDetailsQuery, opts ...grpc.CallOption) (*apiclient.RepoAppDetailsResponse, error) {
-	return nil, nil
-}
-func (r repoServerClientMock) GetRevisionMetadata(ctx context.Context, in *apiclient.RepoServerRevisionMetadataRequest, opts ...grpc.CallOption) (*v1alpha1.RevisionMetadata, error) {
-	return nil, nil
-}
-func (r repoServerClientMock) GetHelmCharts(ctx context.Context, in *apiclient.HelmChartsRequest, opts ...grpc.CallOption) (*apiclient.HelmChartsResponse, error) {
-	return nil, nil
-}
-
 type closer struct {
 	// mock *mock.Mock
 }
@@ -70,7 +42,13 @@ func (r repoClientsetMock) NewRepoServerClient() (io.Closer, apiclient.RepoServe
 	return closer{}, args.Get(0).(apiclient.RepoServerServiceClient), args.Error(1)
 }
 
-func TestGetApps(t *testing.T) {
+func TestGetDirectories(t *testing.T) {
+
+	// Hardcode a specific revision to changes to argocd-example-apps from regressing this test:
+	//   Author: Alexander Matyushentsev <Alexander_Matyushentsev@intuit.com>
+	//   Date:   Sun Jan 31 09:54:53 2021 -0800
+	//   chore: downgrade kustomize guestbook image tag (#73)
+	exampleRepoRevision := "08f72e2a309beab929d9fd14626071b1a61a47f9"
 
 	for _, c := range []struct {
 		name          string
@@ -78,80 +56,59 @@ func TestGetApps(t *testing.T) {
 		revision      string
 		repoRes       *v1alpha1.Repository
 		repoErr       error
-		appRes        *apiclient.AppList
-		appError      error
 		expected      []string
 		expectedError error
 	}{
 		{
-			"Happy Flow",
-			"repoURL",
-			"revision",
-			&v1alpha1.Repository{},
-			nil,
-			&apiclient.AppList{
-				Apps: map[string]string{
-					"app1": "",
-					"app2": "",
-				},
+			name:     "All child folders should be returned",
+			repoURL:  "https://github.com/argoproj/argocd-example-apps/",
+			revision: exampleRepoRevision,
+			repoRes: &v1alpha1.Repository{
+				Repo: "https://github.com/argoproj/argocd-example-apps/",
 			},
-			nil,
-			[]string{"app1", "app2"},
-			nil,
+			repoErr: nil,
+			expected: []string{"apps", "apps/templates", "blue-green", "blue-green/templates", "guestbook", "helm-dependency",
+				"helm-guestbook", "helm-guestbook/templates", "helm-hooks", "jsonnet-guestbook", "jsonnet-guestbook-tla",
+				"ksonnet-guestbook", "ksonnet-guestbook/components", "ksonnet-guestbook/environments", "ksonnet-guestbook/environments/default",
+				"ksonnet-guestbook/environments/dev", "ksonnet-guestbook/environments/prod", "kustomize-guestbook", "plugins", "plugins/kasane",
+				"plugins/kustomized-helm", "plugins/kustomized-helm/overlays", "pre-post-sync", "sock-shop", "sock-shop/base", "sync-waves"},
 		},
 		{
-			"handles GetRepository error",
-			"repoURL",
-			"revision",
-			&v1alpha1.Repository{},
-			errors.New("error"),
-			&apiclient.AppList{
-				Apps: map[string]string{
-					"app1": "",
-					"app2": "",
-				},
+			name:     "If GetRepository returns an error, it should pass back to caller",
+			repoURL:  "https://github.com/argoproj/argocd-example-apps/",
+			revision: exampleRepoRevision,
+			repoRes: &v1alpha1.Repository{
+				Repo: "https://github.com/argoproj/argocd-example-apps/",
 			},
-			nil,
-			[]string{},
-			errors.New("Error in GetRepository: error"),
+			repoErr:       errors.New("Simulated error from GetRepository"),
+			expected:      nil,
+			expectedError: errors.New("Error in GetRepository: Simulated error from GetRepository"),
 		},
 		{
-			"handles ListApps error",
-			"repoURL",
-			"revision",
-			&v1alpha1.Repository{},
-			nil,
-			&apiclient.AppList{
-				Apps: map[string]string{
-					"app1": "",
-					"app2": "",
-				},
+			name: "Test against repository containing no directories",
+			// Here I picked an arbitrary repository in argoproj-labs, with a commit containing no folders.
+			repoURL:  "https://github.com/argoproj-labs/argo-workflows-operator/",
+			revision: "5f50933a576833b73b7a172909d8545a108685f4",
+			repoRes: &v1alpha1.Repository{
+				Repo: "https://github.com/argoproj-labs/argo-workflows-operator/",
 			},
-			errors.New("error"),
-			[]string{},
-			errors.New("Error in ListApps: error"),
+			repoErr:  nil,
+			expected: []string{},
 		},
 	} {
 		cc := c
 		t.Run(cc.name, func(t *testing.T) {
 			argocdRepositoryMock := ArgocdRepositoryMock{mock: &mock.Mock{}}
-			repoServerClientMock := repoServerClientMock{mock: &mock.Mock{}}
 			repoClientsetMock := repoClientsetMock{mock: &mock.Mock{}}
 
 			argocdRepositoryMock.mock.On("GetRepository", mock.Anything, cc.repoURL).Return(cc.repoRes, cc.repoErr)
 
-			repoServerClientMock.mock.On("ListApps", mock.Anything, &apiclient.ListAppsRequest{
-				Repo:     cc.repoRes,
-				Revision: cc.revision,
-			}).Return(cc.appRes, cc.appError)
-
-			repoClientsetMock.mock.On("NewRepoServerClient").Return(repoServerClientMock, nil)
-
 			argocd := argoCDService{
 				repositoriesDB: argocdRepositoryMock,
 				repoClientset:  repoClientsetMock,
 			}
-			got, err := argocd.GetApps(context.TODO(), cc.repoURL, cc.revision)
+
+			got, err := argocd.GetDirectories(context.TODO(), cc.repoURL, cc.revision)
 
 			if cc.expectedError != nil {
 				assert.EqualError(t, err, cc.expectedError.Error())
diff --git a/test/e2e/applicationset/applicationset_test.go b/test/e2e/applicationset/applicationset_test.go
index 632f861..0319981 100644
--- a/test/e2e/applicationset/applicationset_test.go
+++ b/test/e2e/applicationset/applicationset_test.go
@@ -85,8 +85,7 @@ func TestSimpleListGenerator(t *testing.T) {
 
 }
 
-func TestSimpleGitGenerator(t *testing.T) {
-
+func TestSimpleGitDirectoryGenerator(t *testing.T) {
 	generateExpectedApp := func(name string) argov1alpha1.Application {
 		return argov1alpha1.Application{
 			TypeMeta: metav1.TypeMeta{
@@ -174,3 +173,92 @@ func TestSimpleGitGenerator(t *testing.T) {
 		When().
 		Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
 }
+
+func TestSimpleGitFilesGenerator(t *testing.T) {
+
+	generateExpectedApp := func(name string) argov1alpha1.Application {
+		return argov1alpha1.Application{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       "Application",
+				APIVersion: "argoproj.io/v1alpha1",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      name,
+				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{
+					Server:    "https://kubernetes.default.svc",
+					Namespace: "guestbook",
+				},
+			},
+		}
+	}
+
+	expectedApps := []argov1alpha1.Application{
+		generateExpectedApp("engineering-dev-guestbook"),
+		generateExpectedApp("engineering-prod-guestbook"),
+	}
+
+	var expectedAppsNewNamespace []argov1alpha1.Application
+
+	Given(t).
+		When().
+		// Create a GitGenerator-based ApplicationSet
+		Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
+			Name: "simple-git-generator",
+		},
+			Spec: v1alpha1.ApplicationSetSpec{
+				Template: v1alpha1.ApplicationSetTemplate{
+					ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{cluster.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{
+							Server:    "https://kubernetes.default.svc",
+							Namespace: "guestbook",
+						},
+					},
+				},
+				Generators: []v1alpha1.ApplicationSetGenerator{
+					{
+						Git: &v1alpha1.GitGenerator{
+							RepoURL: "https://github.com/argoproj-labs/applicationset.git",
+							Files: []v1alpha1.GitFileGeneratorItem{
+								{
+									Path: "examples/git-generator-files-discovery/cluster-config/**/config.json",
+								},
+							},
+						},
+					},
+				},
+			},
+		}).Then().Expect(ApplicationsExist(expectedApps)).
+
+		// Update the ApplicationSet template namespace, and verify it updates the Applications
+		When().
+		And(func() {
+			for _, expectedApp := range expectedApps {
+				newExpectedApp := expectedApp.DeepCopy()
+				newExpectedApp.Spec.Destination.Namespace = "guestbook2"
+				expectedAppsNewNamespace = append(expectedAppsNewNamespace, *newExpectedApp)
+			}
+		}).
+		Update(func(appset *v1alpha1.ApplicationSet) {
+			appset.Spec.Template.Spec.Destination.Namespace = "guestbook2"
+		}).Then().Expect(ApplicationsExist(expectedAppsNewNamespace)).
+
+		// Delete the ApplicationSet, and verify it deletes the Applications
+		When().
+		Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
+}