Repository: argoproj-labs/applicationset
Pull request #48 (2020-09-17 21:51:12)

add sync status
Add the option to determine the sync status. When using the git generator, a  user can cause an application to be deleted by changing the name of the folder. This PR aimed to give an option to prevent delete / update in the reconcile. 

It's should probably be part of the generator metadata, but for now, we can start with a global flag.

Based on the policies from external-dns

xianlubird: pls fix the conflict
dgoodwin: Are we confident exposing this as a global setting on the CLI isn't going to get picked up by someone and used beyond what we'd like? This really should be in the generator spec as you mentioned. Doing it across the whole controller is definitely not something we'd want to live for very long.



Patch:
diff --git a/main.go b/main.go
index 8aa7f31..7b6731c 100644
--- a/main.go
+++ b/main.go
@@ -44,6 +44,7 @@ func main() {
 	var enableLeaderElection bool
 	var namespace string
 	var argocdRepoServer string
+	var policy string
 	var debugLog bool
 	flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
 	flag.StringVar(&metricsAddr, "probe-addr", ":8081", "The address the probe endpoint binds to.")
@@ -52,14 +53,23 @@ func main() {
 			"Enabling this will ensure there is only one active controller manager.")
 	flag.StringVar(&namespace, "namespace", "argocd", "Argo CD repo namesapce")
 	flag.StringVar(&argocdRepoServer, "argocd-repo-server", "argocd-repo-server:8081", "Argo CD repo server address")
+	flag.StringVar(&policy, "policy", "sync", "Modify how application is sync between the generator and the cluster. Default is sync (create & update & delete), options: create-only, create-update (no deletion)")
 	flag.BoolVar(&debugLog, "debug", false, "print debug logs")
 	flag.Parse()
 
+
+
 	ctrl.SetLogger(zap.New(zap.UseDevMode(true)))
 
-	if debugLog {
-		log.SetLevel(log.DebugLevel)
+	policyObj, exists := utils.Policies[policy]
+	if !exists {
+		setupLog.Info("Policy value can be: sync, create-only, create-update")
+		os.Exit(1)
 	}
+  
+  if debugLog {
+    log.SetLevel(log.DebugLevel)
+  }
 
 	// Determine the namespace we're running in. Normally injected into the pod as an env
 	// var via the Kube downward API configured in the Deployment.
@@ -100,6 +110,7 @@ func main() {
 		Scheme:      mgr.GetScheme(),
 		Recorder:    mgr.GetEventRecorderFor("applicationset-controller"),
 		Renderer: &utils.Render{},
+    Policy: policyObj,
 	}).SetupWithManager(mgr); err != nil {
 		setupLog.Error(err, "unable to create controller", "controller", "ApplicationSet")
 		os.Exit(1)
diff --git a/pkg/controllers/applicationset_controller.go b/pkg/controllers/applicationset_controller.go
index 0592750..a32d530 100644
--- a/pkg/controllers/applicationset_controller.go
+++ b/pkg/controllers/applicationset_controller.go
@@ -44,6 +44,7 @@ type ApplicationSetReconciler struct {
 	Scheme   *runtime.Scheme
 	Recorder record.EventRecorder
 	Generators []generators.Generator
+	utils.Policy
 	utils.Renderer
 }
 
@@ -69,13 +70,23 @@ func (r *ApplicationSetReconciler) Reconcile(req ctrl.Request) (ctrl.Result, err
 		return ctrl.Result{}, err
 	}
 
-	err = r.createOrUpdateInCluster(ctx, applicationSetInfo, desiredApplications)
-	if err != nil {
-		return ctrl.Result{}, err
+	if r.Policy.Update() {
+		err = r.createOrUpdateInCluster(ctx, applicationSetInfo, desiredApplications)
+		if err != nil {
+			return ctrl.Result{}, err
+		}
+	} else {
+		err = r.createInCluster(ctx, applicationSetInfo, desiredApplications)
+		if err != nil {
+			return ctrl.Result{}, err
+		}
 	}
-	err = r.deleteInCluster(ctx, applicationSetInfo, desiredApplications)
-	if err != nil {
-		return ctrl.Result{}, err
+
+	if r.Policy.Delete() {
+		err = r.deleteInCluster(ctx, applicationSetInfo, desiredApplications)
+		if err != nil {
+			return ctrl.Result{}, err
+		}
 	}
 
 	return ctrl.Result{}, nil
@@ -180,7 +191,7 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
 		})
 
 		if err != nil {
-			appLog.WithError(err).WithField("action", action).Error("failed to create/update Application")
+			appLog.WithError(err).WithField("action", action).Errorf("failed to %s Application", action)
 			if firstError == nil {
 				firstError = err
 			}
@@ -193,13 +204,55 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
 	return firstError
 }
 
+
+// createInCluster will filter from the desiredApplications only the application that needs to be created
+// Then it will call createOrUpdateInCluster to do the actual create
+func (r *ApplicationSetReconciler) createInCluster(ctx context.Context, applicationSet argoprojiov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error {
+
+	var createApps []argov1alpha1.Application
+	current, err := r.getCurrentApplications(ctx,applicationSet)
+	if err != nil {
+		return err
+	}
+
+	m := make(map[string]bool) // Will holds the app names that are current in the cluster
+
+	for _, app := range current {
+		m[app.Name] = true
+	}
+
+	// filter applications that are not in m[string]bool (new to the cluster)
+	for _, app := range desiredApplications {
+		_, exists := m[app.Name]
+
+		if !exists {
+			createApps = append(createApps, app)
+		}
+	}
+
+	return r.createOrUpdateInCluster(ctx, applicationSet, createApps)
+}
+
+func (r *ApplicationSetReconciler) getCurrentApplications(ctx context.Context, applicationSet argoprojiov1alpha1.ApplicationSet) ([]argov1alpha1.Application, error){
+	var current argov1alpha1.ApplicationList
+	err := r.Client.List(context.Background(), &current, client.MatchingFields{".metadata.controller": applicationSet.Name})
+
+	if err != nil {
+		return nil, err
+	}
+
+	return current.Items, nil
+}
+
 // deleteInCluster will delete application that are current in the cluster but not in appList.
 // The function must be called after all generators had been called and generated applications
 func (r *ApplicationSetReconciler) deleteInCluster(ctx context.Context, applicationSet argoprojiov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error {
 
 	// Save current applications to be able to delete the ones that are not in appList
-	var current argov1alpha1.ApplicationList
-	_ = r.Client.List(context.Background(), &current, client.MatchingFields{".metadata.controller": applicationSet.Name})
+	current,err := r.getCurrentApplications(ctx,applicationSet)
+	if err != nil {
+		return err
+	}
 
 	m := make(map[string]bool) // Will holds the app names in appList for the deletion process
 
@@ -209,7 +262,7 @@ func (r *ApplicationSetReconciler) deleteInCluster(ctx context.Context, applicat
 
 	// Delete apps that are not in m[string]bool
 	var firstError error
-	for _, app := range current.Items {
+	for _, app := range current {
 		appLog := log.WithFields(log.Fields{"app": app.Name, "appSet": applicationSet.Name})
 		_, exists := m[app.Name]
 
diff --git a/pkg/utils/policy.go b/pkg/utils/policy.go
new file mode 100644
index 0000000..d474035
--- /dev/null
+++ b/pkg/utils/policy.go
@@ -0,0 +1,45 @@
+package utils
+
+// Policy allows to apply different rules to a set of changes.
+type Policy interface {
+	Update() bool
+	Delete() bool
+}
+
+// Policies is a registry of available policies.
+var Policies = map[string]Policy{
+	"sync":        &SyncPolicy{},
+	"create-only": &CreateOnlyPolicy{},
+	"create-update": &CreateUpdatePolicy{},
+}
+
+type SyncPolicy struct{}
+
+func (p *SyncPolicy) Update() bool {
+	return true
+}
+
+func (p *SyncPolicy) Delete() bool {
+	return true
+}
+
+type CreateUpdatePolicy struct{}
+
+func (p *CreateUpdatePolicy) Update() bool {
+	return true
+}
+
+func (p *CreateUpdatePolicy) Delete() bool {
+	return false
+}
+
+type CreateOnlyPolicy struct{}
+
+func (p *CreateOnlyPolicy) Update() bool {
+	return false
+}
+
+func (p *CreateOnlyPolicy) Delete() bool {
+	return false
+}
+


Test patch:
diff --git a/pkg/controllers/applicationset_controller_test.go b/pkg/controllers/applicationset_controller_test.go
index 81643bd..312fb31 100644
--- a/pkg/controllers/applicationset_controller_test.go
+++ b/pkg/controllers/applicationset_controller_test.go
@@ -356,6 +356,193 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
 
 }
 
+func TestCreateApplications(t *testing.T) {
+
+	scheme := runtime.NewScheme()
+	argoprojiov1alpha1.AddToScheme(scheme)
+	argov1alpha1.AddToScheme(scheme)
+
+	for _, c := range []struct {
+		appSet     argoprojiov1alpha1.ApplicationSet
+		existsApps []argov1alpha1.Application
+		apps       []argov1alpha1.Application
+		expected   []argov1alpha1.Application
+	}{
+		{
+			appSet: argoprojiov1alpha1.ApplicationSet{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "name",
+					Namespace: "namespace",
+				},
+			},
+			existsApps: nil,
+			apps: []argov1alpha1.Application{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "app1",
+					},
+				},
+			},
+			expected: []argov1alpha1.Application{
+				{
+					TypeMeta: metav1.TypeMeta{
+						Kind:       "Application",
+						APIVersion: "argoproj.io/v1alpha1",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            "app1",
+						Namespace:       "namespace",
+						ResourceVersion: "1",
+					},
+				},
+			},
+		},
+		{
+			appSet: argoprojiov1alpha1.ApplicationSet{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "name",
+					Namespace: "namespace",
+				},
+				Spec: argoprojiov1alpha1.ApplicationSetSpec{
+					Template: argoprojiov1alpha1.ApplicationSetTemplate{
+						Spec: argov1alpha1.ApplicationSpec{
+							Project: "project",
+						},
+					},
+				},
+			},
+			existsApps: []argov1alpha1.Application{
+				argov1alpha1.Application{
+					TypeMeta: metav1.TypeMeta{
+						Kind:       "Application",
+						APIVersion: "argoproj.io/v1alpha1",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            "app1",
+						Namespace:       "namespace",
+						ResourceVersion: "2",
+					},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "test",
+					},
+				},
+			},
+			apps: []argov1alpha1.Application{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "app1",
+					},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "project",
+					},
+				},
+			},
+			expected: []argov1alpha1.Application{
+				{
+					TypeMeta: metav1.TypeMeta{
+						Kind:       "Application",
+						APIVersion: "argoproj.io/v1alpha1",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            "app1",
+						Namespace:       "namespace",
+						ResourceVersion: "2",
+					},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "test",
+					},
+				},
+			},
+		},
+		{
+			appSet: argoprojiov1alpha1.ApplicationSet{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "name",
+					Namespace: "namespace",
+				},
+				Spec: argoprojiov1alpha1.ApplicationSetSpec{
+					Template: argoprojiov1alpha1.ApplicationSetTemplate{
+						Spec: argov1alpha1.ApplicationSpec{
+							Project: "project",
+						},
+					},
+				},
+			},
+			existsApps: []argov1alpha1.Application{
+				argov1alpha1.Application{
+					TypeMeta: metav1.TypeMeta{
+						Kind:       "Application",
+						APIVersion: "argoproj.io/v1alpha1",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            "app1",
+						Namespace:       "namespace",
+						ResourceVersion: "2",
+					},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "test",
+					},
+				},
+			},
+			apps: []argov1alpha1.Application{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "app2",
+					},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "project",
+					},
+				},
+			},
+			expected: []argov1alpha1.Application{
+				{
+					TypeMeta: metav1.TypeMeta{
+						Kind:       "Application",
+						APIVersion: "argoproj.io/v1alpha1",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            "app2",
+						Namespace:       "namespace",
+						ResourceVersion: "1",
+					},
+					Spec: argov1alpha1.ApplicationSpec{
+						Project: "project",
+					},
+				},
+			},
+		},
+	} {
+		initObjs := []runtime.Object{&c.appSet}
+		for _, a := range c.existsApps {
+			controllerutil.SetControllerReference(&c.appSet, &a, scheme)
+			initObjs = append(initObjs, &a)
+		}
+
+		client := fake.NewFakeClientWithScheme(scheme, initObjs...)
+
+		r := ApplicationSetReconciler{
+			Client:   client,
+			Scheme:   scheme,
+			Recorder: record.NewFakeRecorder(len(initObjs) + len(c.expected)),
+		}
+
+		r.createInCluster(context.TODO(), c.appSet, c.apps)
+
+		for _, obj := range c.expected {
+			got := &argov1alpha1.Application{}
+			_ = client.Get(context.Background(), crtclient.ObjectKey{
+				Namespace: obj.Namespace,
+				Name:      obj.Name,
+			}, got)
+
+			controllerutil.SetControllerReference(&c.appSet, &obj, r.Scheme)
+
+			assert.Equal(t, obj, *got)
+		}
+	}
+
+}
+
 func TestDeleteInCluster(t *testing.T) {
 
 	scheme := runtime.NewScheme()