Repository: argoproj-labs/applicationset (go)

Issue:
ApplicationSet should report error conditions/status based on the result of generator/template resuls
When the user-provided generator/template produce invalid Argo CD `Application`s, we should note that error in the ApplicationSet CRD status field. See Argo CD's `ApplicationStatus` [conditions field](https://github.com/argoproj/argo-cd/blob/1ef14dca839423fe3cd9fade82bd1b3ed80eb5bd/pkg/apis/application/v1alpha1/types.go#L491).

Likewise I'm sure there are other status fields/conditions that would be useful to expose as part of an `ApplicationSet` `status` subtype.

Once implemented, one such function that would benefit from this is:
https://github.com/argoproj-labs/applicationset/issues/68

Pull request:
feat: report error conditions/status for ApplicationSet
Report error conditions/status fo ApplicationSet 

Fixes #71 

Signed off by [Ishita Sequeira](@ishitasequeira )

Patch:
diff --git a/api/v1alpha1/applicationset_types.go b/api/v1alpha1/applicationset_types.go
index c85b93a..40e0329 100644
--- a/api/v1alpha1/applicationset_types.go
+++ b/api/v1alpha1/applicationset_types.go
@@ -17,7 +17,11 @@ limitations under the License.
 package v1alpha1
 
 import (
+	"fmt"
+	"sort"
+
 	"github.com/argoproj-labs/applicationset/common"
+
 	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -32,6 +36,7 @@ type SecretRef struct {
 // ApplicationSet is a set of Application resources
 // +kubebuilder:object:root=true
 // +kubebuilder:resource:path=applicationsets,shortName=appset;appsets
+// +kubebuilder:subresource:status
 type ApplicationSet struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata"`
@@ -233,8 +238,65 @@ type PullRequestGeneratorGithub struct {
 type ApplicationSetStatus struct {
 	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
 	// Important: Run "make" to regenerate code after modifying this file
+	Conditions []ApplicationSetCondition `json:"conditions,omitempty"`
+}
+
+// ApplicationSetCondition contains details about an applicationset condition, which is usally an error or warning
+type ApplicationSetCondition struct {
+	// Type is an applicationset condition type
+	Type ApplicationSetConditionType `json:"type" protobuf:"bytes,1,opt,name=type"`
+	// Message contains human-readable message indicating details about condition
+	Message string `json:"message" protobuf:"bytes,2,opt,name=message"`
+	// LastTransitionTime is the time the condition was last observed
+	LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,3,opt,name=lastTransitionTime"`
+	// True/False/Unknown
+	Status ApplicationSetConditionStatus `json:"status" protobuf:"bytes,4,opt,name=status"`
+	//Single word camelcase representing the reason for the status eg ErrorOccurred
+	Reason string `json:"reason" protobuf:"bytes,5,opt,name=reason"`
 }
 
+// SyncStatusCode is a type which represents possible comparison results
+type ApplicationSetConditionStatus string
+
+// Application Condition Status
+const (
+	// ApplicationSetConditionStatusTrue indicates that a application has been successfully established
+	ApplicationSetConditionStatusTrue ApplicationSetConditionStatus = "True"
+	// ApplicationSetConditionStatusFalse indicates that a application attempt has failed
+	ApplicationSetConditionStatusFalse ApplicationSetConditionStatus = "False"
+	// ApplicationSetConditionStatusUnknown indicates that the application condition status could not be reliably determined
+	ApplicationSetConditionStatusUnknown ApplicationSetConditionStatus = "Unknown"
+)
+
+// ApplicationSetConditionType represents type of application condition. Type name has following convention:
+// prefix "Error" means error condition
+// prefix "Warning" means warning condition
+// prefix "Info" means informational condition
+type ApplicationSetConditionType string
+
+//ErrorOccurred / ParametersGenerated / TemplateRendered / ResourcesUpToDate
+const (
+	ApplicationSetConditionErrorOccurred       ApplicationSetConditionType = "ErrorOccurred"
+	ApplicationSetConditionParametersGenerated ApplicationSetConditionType = "ParametersGenerated"
+	ApplicationSetConditionResourcesUpToDate   ApplicationSetConditionType = "ResourcesUpToDate"
+)
+
+type ApplicationSetReasonType string
+
+const (
+	ApplicationSetReasonErrorOccurred                    = "ErrorOccurred"
+	ApplicationSetReasonApplicationSetUpToDate           = "ApplicationSetUpToDate"
+	ApplicationSetReasonParametersGenerated              = "ParametersGenerated"
+	ApplicationSetReasonApplicationGenerated             = "ApplicationGeneratedSuccessfully"
+	ApplicationSetReasonUpdateApplicationError           = "UpdateApplicationError"
+	ApplicationSetReasonApplicationParamsGenerationError = "ApplicationGenerationFromParamsError"
+	ApplicationSetReasonRenderTemplateParamsError        = "RenderTemplateParamsError"
+	ApplicationSetReasonCreateApplicationError           = "CreateApplicationError"
+	ApplicationSetReasonDeleteApplicationError           = "DeleteApplicationError"
+	ApplicationSetReasonRefreshApplicationError          = "RefreshApplicationError"
+	ApplicationSetReasonApplicationValidationError       = "ApplicationValidationError"
+)
+
 // ApplicationSetList contains a list of ApplicationSet
 // +kubebuilder:object:root=true
 type ApplicationSetList struct {
@@ -252,3 +314,42 @@ func (a *ApplicationSet) RefreshRequired() bool {
 	_, found := a.Annotations[common.AnnotationGitGeneratorRefresh]
 	return found
 }
+
+// SetConditions updates the applicationset status conditions for a subset of evaluated types.
+// If the applicationset has a pre-existing condition of a type that is not in the evaluated list,
+// it will be preserved. If the applicationset has a pre-existing condition of a type, status, reason that
+// is in the evaluated list, but not in the incoming conditions list, it will be removed.
+func (status *ApplicationSetStatus) SetConditions(conditions []ApplicationSetCondition, evaluatedTypes map[ApplicationSetConditionType]bool) {
+	applicationSetConditions := make([]ApplicationSetCondition, 0)
+	now := metav1.Now()
+	for i := range conditions {
+		condition := conditions[i]
+		if condition.LastTransitionTime == nil {
+			condition.LastTransitionTime = &now
+		}
+		eci := findConditionIndex(status.Conditions, condition.Type)
+		if eci >= 0 && status.Conditions[eci].Message == condition.Message && status.Conditions[eci].Reason == condition.Reason && status.Conditions[eci].Status == condition.Status {
+			// If we already have a condition of this type, status and reason, only update the timestamp if something
+			// has changed.
+			applicationSetConditions = append(applicationSetConditions, status.Conditions[eci])
+		} else {
+			// Otherwise we use the new incoming condition with an updated timestamp:
+			applicationSetConditions = append(applicationSetConditions, condition)
+		}
+	}
+	sort.Slice(applicationSetConditions, func(i, j int) bool {
+		left := applicationSetConditions[i]
+		right := applicationSetConditions[j]
+		return fmt.Sprintf("%s/%s/%s/%s/%v", left.Type, left.Message, left.Status, left.Reason, left.LastTransitionTime) < fmt.Sprintf("%s/%s/%s/%s/%v", right.Type, right.Message, right.Status, right.Reason, right.LastTransitionTime)
+	})
+	status.Conditions = applicationSetConditions
+}
+
+func findConditionIndex(conditions []ApplicationSetCondition, t ApplicationSetConditionType) int {
+	for i := range conditions {
+		if conditions[i].Type == t {
+			return i
+		}
+	}
+	return -1
+}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 5b2c46b..a767222 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -31,7 +31,7 @@ func (in *ApplicationSet) DeepCopyInto(out *ApplicationSet) {
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
 	in.Spec.DeepCopyInto(&out.Spec)
-	out.Status = in.Status
+	in.Status.DeepCopyInto(&out.Status)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSet.
@@ -97,6 +97,25 @@ func (in *ApplicationSetBaseGenerator) DeepCopy() *ApplicationSetBaseGenerator {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationSetCondition) DeepCopyInto(out *ApplicationSetCondition) {
+	*out = *in
+	if in.LastTransitionTime != nil {
+		in, out := &in.LastTransitionTime, &out.LastTransitionTime
+		*out = (*in).DeepCopy()
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetCondition.
+func (in *ApplicationSetCondition) DeepCopy() *ApplicationSetCondition {
+	if in == nil {
+		return nil
+	}
+	out := new(ApplicationSetCondition)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ApplicationSetGenerator) DeepCopyInto(out *ApplicationSetGenerator) {
 	*out = *in
@@ -210,6 +229,13 @@ func (in *ApplicationSetSpec) DeepCopy() *ApplicationSetSpec {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ApplicationSetStatus) DeepCopyInto(out *ApplicationSetStatus) {
 	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]ApplicationSetCondition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSetStatus.
diff --git a/manifests/crds/argoproj.io_applicationsets.yaml b/manifests/crds/argoproj.io_applicationsets.yaml
index 5be79af..b6a1bbf 100644
--- a/manifests/crds/argoproj.io_applicationsets.yaml
+++ b/manifests/crds/argoproj.io_applicationsets.yaml
@@ -7416,6 +7416,41 @@ spec:
             type: object
           status:
             description: ApplicationSetStatus defines the observed state of ApplicationSet
+            properties:
+              conditions:
+                description: 'INSERT ADDITIONAL STATUS FIELD - define observed state
+                  of cluster Important: Run "make" to regenerate code after modifying
+                  this file'
+                items:
+                  description: ApplicationSetCondition contains details about an applicationset
+                    condition, which is usally an error or warning
+                  properties:
+                    lastTransitionTime:
+                      description: LastTransitionTime is the time the condition was
+                        last observed
+                      format: date-time
+                      type: string
+                    message:
+                      description: Message contains human-readable message indicating
+                        details about condition
+                      type: string
+                    reason:
+                      description: Single word camelcase representing the reason for
+                        the status eg ErrorOccurred
+                      type: string
+                    status:
+                      description: True/False/Unknown
+                      type: string
+                    type:
+                      description: Type is an applicationset condition type
+                      type: string
+                  required:
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
             type: object
         required:
         - metadata
@@ -7423,6 +7458,8 @@ spec:
         type: object
     served: true
     storage: true
+    subresources:
+      status: {}
 status:
   acceptedNames:
     kind: ""
diff --git a/manifests/install-with-argo-cd.yaml b/manifests/install-with-argo-cd.yaml
index bf843f0..1b656cb 100644
--- a/manifests/install-with-argo-cd.yaml
+++ b/manifests/install-with-argo-cd.yaml
@@ -7085,6 +7085,35 @@ spec:
             type: object
           status:
             description: ApplicationSetStatus defines the observed state of ApplicationSet
+            properties:
+              conditions:
+                description: 'INSERT ADDITIONAL STATUS FIELD - define observed state of cluster Important: Run "make" to regenerate code after modifying this file'
+                items:
+                  description: ApplicationSetCondition contains details about an applicationset condition, which is usally an error or warning
+                  properties:
+                    lastTransitionTime:
+                      description: LastTransitionTime is the time the condition was last observed
+                      format: date-time
+                      type: string
+                    message:
+                      description: Message contains human-readable message indicating details about condition
+                      type: string
+                    reason:
+                      description: Single word camelcase representing the reason for the status eg ErrorOccurred
+                      type: string
+                    status:
+                      description: True/False/Unknown
+                      type: string
+                    type:
+                      description: Type is an applicationset condition type
+                      type: string
+                  required:
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
             type: object
         required:
         - metadata
@@ -7092,6 +7121,8 @@ spec:
         type: object
     served: true
     storage: true
+    subresources:
+      status: {}
 status:
   acceptedNames:
     kind: ""
diff --git a/manifests/install.yaml b/manifests/install.yaml
index 7f07444..573d645 100644
--- a/manifests/install.yaml
+++ b/manifests/install.yaml
@@ -5285,6 +5285,35 @@ spec:
             type: object
           status:
             description: ApplicationSetStatus defines the observed state of ApplicationSet
+            properties:
+              conditions:
+                description: 'INSERT ADDITIONAL STATUS FIELD - define observed state of cluster Important: Run "make" to regenerate code after modifying this file'
+                items:
+                  description: ApplicationSetCondition contains details about an applicationset condition, which is usally an error or warning
+                  properties:
+                    lastTransitionTime:
+                      description: LastTransitionTime is the time the condition was last observed
+                      format: date-time
+                      type: string
+                    message:
+                      description: Message contains human-readable message indicating details about condition
+                      type: string
+                    reason:
+                      description: Single word camelcase representing the reason for the status eg ErrorOccurred
+                      type: string
+                    status:
+                      description: True/False/Unknown
+                      type: string
+                    type:
+                      description: Type is an applicationset condition type
+                      type: string
+                  required:
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
             type: object
         required:
         - metadata
@@ -5292,6 +5321,8 @@ spec:
         type: object
     served: true
     storage: true
+    subresources:
+      status: {}
 status:
   acceptedNames:
     kind: ""
diff --git a/pkg/controllers/applicationset_controller.go b/pkg/controllers/applicationset_controller.go
index 4772f88..770c2bc 100644
--- a/pkg/controllers/applicationset_controller.go
+++ b/pkg/controllers/applicationset_controller.go
@@ -27,6 +27,7 @@ import (
 	"github.com/go-logr/logr"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -75,9 +76,11 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 	_ = log.WithField("applicationset", req.NamespacedName)
 
 	var applicationSetInfo argoprojiov1alpha1.ApplicationSet
+	parametersGenerated := false
+
 	if err := r.Get(ctx, req.NamespacedName, &applicationSetInfo); err != nil {
 		if client.IgnoreNotFound(err) != nil {
-			log.WithField("request", req).WithError(err).Infof("unable to get ApplicationSet")
+			log.WithError(err).Infof("unable to get ApplicationSet: '%v' ", err)
 		}
 		return ctrl.Result{}, client.IgnoreNotFound(err)
 	}
@@ -89,13 +92,23 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 
 	// Log a warning if there are unrecognized generators
 	utils.CheckInvalidGenerators(&applicationSetInfo)
-
 	// desiredApplications is the main list of all expected Applications from all generators in this appset.
-	desiredApplications, err := r.generateApplications(applicationSetInfo)
+	desiredApplications, applicationSetReason, err := r.generateApplications(applicationSetInfo)
 	if err != nil {
+		_ = r.setApplicationSetStatusCondition(ctx,
+			&applicationSetInfo,
+			argoprojiov1alpha1.ApplicationSetCondition{
+				Type:    argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
+				Message: err.Error(),
+				Reason:  string(applicationSetReason),
+				Status:  argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
+			}, parametersGenerated,
+		)
 		return ctrl.Result{}, err
 	}
 
+	parametersGenerated = true
+
 	validateErrors, err := r.validateGeneratedApplications(ctx, desiredApplications, applicationSetInfo, req.Namespace)
 	if err != nil {
 		// While some generators may return an error that requires user intervention,
@@ -106,6 +119,16 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		// Changes to watched resources will cause this to be reconciled sooner than
 		// the RequeueAfter time.
 		log.Errorf("error occurred during application validation: %s", err.Error())
+
+		_ = r.setApplicationSetStatusCondition(ctx,
+			&applicationSetInfo,
+			argoprojiov1alpha1.ApplicationSetCondition{
+				Type:    argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
+				Message: err.Error(),
+				Reason:  argoprojiov1alpha1.ApplicationSetReasonApplicationValidationError,
+				Status:  argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
+			}, parametersGenerated,
+		)
 		return ctrl.Result{RequeueAfter: ReconcileRequeueOnValidationError}, nil
 	}
 
@@ -125,17 +148,44 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		if len(validateErrors) > 1 {
 			message = fmt.Sprintf("%s (and %d more)", message, len(validateErrors)-1)
 		}
-		log.Errorf("error occurred during application validation: %s", message)
+		log.Errorf("validation error found during application validation: %s", message)
+		_ = r.setApplicationSetStatusCondition(ctx,
+			&applicationSetInfo,
+			argoprojiov1alpha1.ApplicationSetCondition{
+				Type:    argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
+				Message: message,
+				Reason:  argoprojiov1alpha1.ApplicationSetReasonApplicationValidationError,
+				Status:  argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
+			}, parametersGenerated,
+		)
 	}
 
 	if r.Policy.Update() {
 		err = r.createOrUpdateInCluster(ctx, applicationSetInfo, validApps)
 		if err != nil {
+			_ = r.setApplicationSetStatusCondition(ctx,
+				&applicationSetInfo,
+				argoprojiov1alpha1.ApplicationSetCondition{
+					Type:    argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
+					Message: err.Error(),
+					Reason:  argoprojiov1alpha1.ApplicationSetReasonUpdateApplicationError,
+					Status:  argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
+				}, parametersGenerated,
+			)
 			return ctrl.Result{}, err
 		}
 	} else {
 		err = r.createInCluster(ctx, applicationSetInfo, validApps)
 		if err != nil {
+			_ = r.setApplicationSetStatusCondition(ctx,
+				&applicationSetInfo,
+				argoprojiov1alpha1.ApplicationSetCondition{
+					Type:    argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
+					Message: err.Error(),
+					Reason:  argoprojiov1alpha1.ApplicationSetReasonCreateApplicationError,
+					Status:  argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
+				}, parametersGenerated,
+			)
 			return ctrl.Result{}, err
 		}
 	}
@@ -143,6 +193,15 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 	if r.Policy.Delete() {
 		err = r.deleteInCluster(ctx, applicationSetInfo, desiredApplications)
 		if err != nil {
+			_ = r.setApplicationSetStatusCondition(ctx,
+				&applicationSetInfo,
+				argoprojiov1alpha1.ApplicationSetCondition{
+					Type:    argoprojiov1alpha1.ApplicationSetConditionResourcesUpToDate,
+					Message: err.Error(),
+					Reason:  argoprojiov1alpha1.ApplicationSetReasonDeleteApplicationError,
+					Status:  argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
+				}, parametersGenerated,
+			)
 			return ctrl.Result{}, err
 		}
 	}
@@ -152,6 +211,15 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		err := r.Client.Update(ctx, &applicationSetInfo)
 		if err != nil {
 			log.Warnf("error occurred while updating ApplicationSet: %v", err)
+			_ = r.setApplicationSetStatusCondition(ctx,
+				&applicationSetInfo,
+				argoprojiov1alpha1.ApplicationSetCondition{
+					Type:    argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
+					Message: err.Error(),
+					Reason:  argoprojiov1alpha1.ApplicationSetReasonRefreshApplicationError,
+					Status:  argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
+				}, parametersGenerated,
+			)
 			return ctrl.Result{}, err
 		}
 	}
@@ -159,11 +227,127 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 	requeueAfter := r.getMinRequeueAfter(&applicationSetInfo)
 	log.WithField("requeueAfter", requeueAfter).Info("end reconcile")
 
+	if len(validateErrors) == 0 {
+		if err := r.setApplicationSetStatusCondition(ctx,
+			&applicationSetInfo,
+			argoprojiov1alpha1.ApplicationSetCondition{
+				Type:    argoprojiov1alpha1.ApplicationSetConditionResourcesUpToDate,
+				Message: "All applications have been generated successfully",
+				Reason:  argoprojiov1alpha1.ApplicationSetReasonApplicationSetUpToDate,
+				Status:  argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
+			}, parametersGenerated,
+		); err != nil {
+			return ctrl.Result{}, err
+		}
+	}
+
 	return ctrl.Result{
 		RequeueAfter: requeueAfter,
 	}, nil
 }
 
+func getParametersGeneratedCondition(parametersGenerated bool, message string) argoprojiov1alpha1.ApplicationSetCondition {
+	var paramtersGeneratedCondition argoprojiov1alpha1.ApplicationSetCondition
+	if parametersGenerated {
+		paramtersGeneratedCondition = argoprojiov1alpha1.ApplicationSetCondition{
+			Type:    argoprojiov1alpha1.ApplicationSetConditionParametersGenerated,
+			Message: "Successfully generated parameters for all Applications",
+			Reason:  argoprojiov1alpha1.ApplicationSetReasonParametersGenerated,
+			Status:  argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
+		}
+	} else {
+		paramtersGeneratedCondition = argoprojiov1alpha1.ApplicationSetCondition{
+			Type:    argoprojiov1alpha1.ApplicationSetConditionParametersGenerated,
+			Message: message,
+			Reason:  argoprojiov1alpha1.ApplicationSetReasonErrorOccurred,
+			Status:  argoprojiov1alpha1.ApplicationSetConditionStatusFalse,
+		}
+	}
+	return paramtersGeneratedCondition
+}
+
+func getResourceUpToDateCondition(errorOccurred bool, message string, reason string) argoprojiov1alpha1.ApplicationSetCondition {
+	var resourceUpToDateCondition argoprojiov1alpha1.ApplicationSetCondition
+	if errorOccurred {
+		resourceUpToDateCondition = argoprojiov1alpha1.ApplicationSetCondition{
+			Type:    argoprojiov1alpha1.ApplicationSetConditionResourcesUpToDate,
+			Message: message,
+			Reason:  reason,
+			Status:  argoprojiov1alpha1.ApplicationSetConditionStatusFalse,
+		}
+	} else {
+		resourceUpToDateCondition = argoprojiov1alpha1.ApplicationSetCondition{
+			Type:    argoprojiov1alpha1.ApplicationSetConditionResourcesUpToDate,
+			Message: "ApplicationSet up to date",
+			Reason:  argoprojiov1alpha1.ApplicationSetReasonApplicationSetUpToDate,
+			Status:  argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
+		}
+	}
+	return resourceUpToDateCondition
+}
+
+func (r *ApplicationSetReconciler) setApplicationSetStatusCondition(ctx context.Context, applicationSet *argoprojiov1alpha1.ApplicationSet, condition argoprojiov1alpha1.ApplicationSetCondition, paramtersGenerated bool) error {
+	// check if error occurred during reconcile process
+	errOccurred := condition.Type == argoprojiov1alpha1.ApplicationSetConditionErrorOccurred
+
+	var errOccurredCondition argoprojiov1alpha1.ApplicationSetCondition
+
+	if errOccurred {
+		errOccurredCondition = condition
+	} else {
+		errOccurredCondition = argoprojiov1alpha1.ApplicationSetCondition{
+			Type:    argoprojiov1alpha1.ApplicationSetConditionErrorOccurred,
+			Message: "Successfully generated parameters for all Applications",
+			Reason:  argoprojiov1alpha1.ApplicationSetReasonApplicationSetUpToDate,
+			Status:  argoprojiov1alpha1.ApplicationSetConditionStatusFalse,
+		}
+	}
+
+	paramtersGeneratedCondition := getParametersGeneratedCondition(paramtersGenerated, condition.Message)
+	resourceUpToDateCondition := getResourceUpToDateCondition(errOccurred, condition.Message, condition.Reason)
+
+	newConditions := []argoprojiov1alpha1.ApplicationSetCondition{errOccurredCondition, paramtersGeneratedCondition, resourceUpToDateCondition}
+
+	needToUpdateConditions := false
+	for _, condition := range newConditions {
+		// do nothing if appset already has same condition
+		for _, c := range applicationSet.Status.Conditions {
+			if c.Type == condition.Type && (c.Reason != condition.Reason || c.Status != condition.Status || c.Message != condition.Message) {
+				needToUpdateConditions = true
+				break
+			}
+		}
+	}
+	evaluatedTypes := map[argoprojiov1alpha1.ApplicationSetConditionType]bool{
+		argoprojiov1alpha1.ApplicationSetConditionErrorOccurred:       true,
+		argoprojiov1alpha1.ApplicationSetConditionParametersGenerated: true,
+		argoprojiov1alpha1.ApplicationSetConditionResourcesUpToDate:   true,
+	}
+
+	if needToUpdateConditions || len(applicationSet.Status.Conditions) < 3 {
+		// fetch updated Application Set object before updating it
+		namespacedName := types.NamespacedName{Namespace: applicationSet.Namespace, Name: applicationSet.Name}
+		if err := r.Get(ctx, namespacedName, applicationSet); err != nil {
+			if client.IgnoreNotFound(err) != nil {
+				return nil
+			}
+			return fmt.Errorf("error fetching updated application set: %v", err)
+		}
+
+		applicationSet.Status.SetConditions(
+			newConditions, evaluatedTypes,
+		)
+
+		// Update the newly fetched object with new set of conditions
+		err := r.Client.Status().Update(ctx, applicationSet)
+		if err != nil && !apierr.IsNotFound(err) {
+			return fmt.Errorf("unable to set application set condition: %v", err)
+		}
+	}
+
+	return 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) (map[int]error, error) {
@@ -238,10 +422,12 @@ func getTempApplication(applicationSetTemplate argoprojiov1alpha1.ApplicationSet
 	return &tmplApplication
 }
 
-func (r *ApplicationSetReconciler) generateApplications(applicationSetInfo argoprojiov1alpha1.ApplicationSet) ([]argov1alpha1.Application, error) {
+func (r *ApplicationSetReconciler) generateApplications(applicationSetInfo argoprojiov1alpha1.ApplicationSet) ([]argov1alpha1.Application, argoprojiov1alpha1.ApplicationSetReasonType, error) {
 	var res []argov1alpha1.Application
 
 	var firstError error
+	var applicationSetReason argoprojiov1alpha1.ApplicationSetReasonType
+
 	for _, requestedGenerator := range applicationSetInfo.Spec.Generators {
 		t, err := generators.Transform(requestedGenerator, r.Generators, applicationSetInfo.Spec.Template, &applicationSetInfo)
 		if err != nil {
@@ -249,6 +435,7 @@ func (r *ApplicationSetReconciler) generateApplications(applicationSetInfo argop
 				Error("error generating application from params")
 			if firstError == nil {
 				firstError = err
+				applicationSetReason = argoprojiov1alpha1.ApplicationSetReasonApplicationParamsGenerationError
 			}
 			continue
 		}
@@ -261,8 +448,10 @@ func (r *ApplicationSetReconciler) generateApplications(applicationSetInfo argop
 				if err != nil {
 					log.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator).
 						Error("error generating application from params")
+
 					if firstError == nil {
 						firstError = err
+						applicationSetReason = argoprojiov1alpha1.ApplicationSetReasonRenderTemplateParamsError
 					}
 					continue
 				}
@@ -272,10 +461,9 @@ func (r *ApplicationSetReconciler) generateApplications(applicationSetInfo argop
 
 		log.WithField("generator", requestedGenerator).Infof("generated %d applications", len(res))
 		log.WithField("generator", requestedGenerator).Debugf("apps from generator: %+v", res)
-
 	}
 
-	return res, firstError
+	return res, applicationSetReason, firstError
 }
 
 func (r *ApplicationSetReconciler) SetupWithManager(mgr ctrl.Manager) error {


Test patch:
diff --git a/api/v1alpha1/applicationset_types_test.go b/api/v1alpha1/applicationset_types_test.go
new file mode 100644
index 0000000..0b67dc9
--- /dev/null
+++ b/api/v1alpha1/applicationset_types_test.go
@@ -0,0 +1,133 @@
+package v1alpha1
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func testCond(t ApplicationSetConditionType, msg string, lastTransitionTime *metav1.Time, status ApplicationSetConditionStatus, reason string) ApplicationSetCondition {
+	return ApplicationSetCondition{
+		Type:               t,
+		Message:            msg,
+		LastTransitionTime: lastTransitionTime,
+		Status:             status,
+		Reason:             reason,
+	}
+}
+
+func newTestAppSet(name, namespace, repo string) *ApplicationSet {
+	a := &ApplicationSet{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Spec: ApplicationSetSpec{
+			Generators: []ApplicationSetGenerator{
+				{
+					Git: &GitGenerator{
+						RepoURL: repo,
+					},
+				},
+			},
+		},
+	}
+
+	return a
+}
+
+func TestSetConditions(t *testing.T) {
+	fiveMinsAgo := &metav1.Time{Time: time.Now().Add(-5 * time.Minute)}
+	tenMinsAgo := &metav1.Time{Time: time.Now().Add(-10 * time.Minute)}
+	tests := []struct {
+		name           string
+		existing       []ApplicationSetCondition
+		incoming       []ApplicationSetCondition
+		evaluatedTypes map[ApplicationSetConditionType]bool
+		expected       []ApplicationSetCondition
+		validate       func(*testing.T, *ApplicationSet)
+	}{
+		{
+			name:     "new conditions with lastTransitionTime",
+			existing: []ApplicationSetCondition{},
+			incoming: []ApplicationSetCondition{
+				testCond(ApplicationSetConditionErrorOccurred, "foo", fiveMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationValidationError),
+				testCond(ApplicationSetConditionResourcesUpToDate, "bar", tenMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationSetUpToDate),
+			},
+			evaluatedTypes: map[ApplicationSetConditionType]bool{
+				ApplicationSetConditionErrorOccurred:     true,
+				ApplicationSetConditionResourcesUpToDate: true,
+			},
+			expected: []ApplicationSetCondition{
+				testCond(ApplicationSetConditionErrorOccurred, "foo", fiveMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationValidationError),
+				testCond(ApplicationSetConditionResourcesUpToDate, "bar", tenMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationSetUpToDate),
+			},
+			validate: func(t *testing.T, a *ApplicationSet) {
+				assert.Equal(t, fiveMinsAgo, a.Status.Conditions[0].LastTransitionTime)
+				assert.Equal(t, tenMinsAgo, a.Status.Conditions[1].LastTransitionTime)
+			},
+		}, {
+			name:     "new conditions without lastTransitionTime",
+			existing: []ApplicationSetCondition{},
+			incoming: []ApplicationSetCondition{
+				testCond(ApplicationSetConditionErrorOccurred, "foo", nil, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationValidationError),
+				testCond(ApplicationSetConditionResourcesUpToDate, "bar", nil, ApplicationSetConditionStatusFalse, ApplicationSetReasonApplicationSetUpToDate),
+			},
+			evaluatedTypes: map[ApplicationSetConditionType]bool{
+				ApplicationSetConditionErrorOccurred:     true,
+				ApplicationSetConditionResourcesUpToDate: true,
+			},
+			expected: []ApplicationSetCondition{
+				testCond(ApplicationSetConditionErrorOccurred, "foo", nil, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationValidationError),
+				testCond(ApplicationSetConditionResourcesUpToDate, "bar", nil, ApplicationSetConditionStatusFalse, ApplicationSetReasonApplicationSetUpToDate),
+			},
+			validate: func(t *testing.T, a *ApplicationSet) {
+				// SetConditions should add timestamps for new conditions.
+				assert.True(t, a.Status.Conditions[0].LastTransitionTime.Time.After(fiveMinsAgo.Time))
+				assert.True(t, a.Status.Conditions[1].LastTransitionTime.Time.After(fiveMinsAgo.Time))
+			},
+		}, {
+			name: "condition cleared",
+			existing: []ApplicationSetCondition{
+				testCond(ApplicationSetConditionErrorOccurred, "foo", fiveMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationValidationError),
+				testCond(ApplicationSetConditionResourcesUpToDate, "bar", tenMinsAgo, ApplicationSetConditionStatusFalse, ApplicationSetReasonApplicationSetUpToDate),
+			},
+			incoming: []ApplicationSetCondition{
+				testCond(ApplicationSetConditionResourcesUpToDate, "bar", tenMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationSetUpToDate),
+			},
+			evaluatedTypes: map[ApplicationSetConditionType]bool{
+				ApplicationSetConditionErrorOccurred:     true,
+				ApplicationSetConditionResourcesUpToDate: true,
+			},
+			expected: []ApplicationSetCondition{
+				testCond(ApplicationSetConditionResourcesUpToDate, "bar", tenMinsAgo, ApplicationSetConditionStatusTrue, ApplicationSetReasonApplicationSetUpToDate),
+			},
+			validate: func(t *testing.T, a *ApplicationSet) {
+				assert.Equal(t, tenMinsAgo.Time, a.Status.Conditions[0].LastTransitionTime.Time)
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			testRepo := "https://github.com/org/repo"
+			namespace := "test"
+			a := newTestAppSet("sample-app-set", namespace, testRepo)
+			a.Status.Conditions = tt.existing
+			a.Status.SetConditions(tt.incoming, tt.evaluatedTypes)
+			assertConditions(t, tt.expected, a.Status.Conditions)
+			if tt.validate != nil {
+				tt.validate(t, a)
+			}
+		})
+	}
+}
+
+func assertConditions(t *testing.T, expected []ApplicationSetCondition, actual []ApplicationSetCondition) {
+	assert.Equal(t, len(expected), len(actual))
+	for i := range expected {
+		assert.Equal(t, expected[i].Type, actual[i].Type)
+		assert.Equal(t, expected[i].Message, actual[i].Message)
+	}
+}
diff --git a/pkg/controllers/applicationset_controller_test.go b/pkg/controllers/applicationset_controller_test.go
index 9a1e2a2..57731df 100644
--- a/pkg/controllers/applicationset_controller_test.go
+++ b/pkg/controllers/applicationset_controller_test.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"reflect"
 	"strings"
 	"testing"
 	"time"
@@ -25,6 +26,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
+	"github.com/argoproj-labs/applicationset/api/v1alpha1"
 	argoprojiov1alpha1 "github.com/argoproj-labs/applicationset/api/v1alpha1"
 	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake"
 	dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks"
@@ -77,8 +79,6 @@ func TestExtractApplications(t *testing.T) {
 	err = argov1alpha1.AddToScheme(scheme)
 	assert.Nil(t, err)
 
-	client := fake.NewClientBuilder().WithScheme(scheme).Build()
-
 	for _, c := range []struct {
 		name                string
 		params              []map[string]string
@@ -86,6 +86,7 @@ func TestExtractApplications(t *testing.T) {
 		generateParamsError error
 		rendererError       error
 		expectErr           bool
+		expectedReason      v1alpha1.ApplicationSetReasonType
 	}{
 		{
 			name:   "Generate two applications",
@@ -98,11 +99,13 @@ func TestExtractApplications(t *testing.T) {
 				},
 				Spec: argov1alpha1.ApplicationSpec{},
 			},
+			expectedReason: "",
 		},
 		{
 			name:                "Handles error from the generator",
 			generateParamsError: errors.New("error"),
 			expectErr:           true,
+			expectedReason:      v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
 		},
 		{
 			name:   "Handles error from the render",
@@ -115,8 +118,9 @@ func TestExtractApplications(t *testing.T) {
 				},
 				Spec: argov1alpha1.ApplicationSpec{},
 			},
-			rendererError: errors.New("error"),
-			expectErr:     true,
+			rendererError:  errors.New("error"),
+			expectErr:      true,
+			expectedReason: v1alpha1.ApplicationSetReasonRenderTemplateParamsError,
 		},
 	} {
 		cc := c
@@ -128,6 +132,15 @@ func TestExtractApplications(t *testing.T) {
 
 		t.Run(cc.name, func(t *testing.T) {
 
+			appSet := &argoprojiov1alpha1.ApplicationSet{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "name",
+					Namespace: "namespace",
+				},
+			}
+
+			client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(appSet).Build()
+
 			generatorMock := generatorMock{}
 			generator := argoprojiov1alpha1.ApplicationSetGenerator{
 				List: &argoprojiov1alpha1.ListGenerator{},
@@ -168,7 +181,7 @@ func TestExtractApplications(t *testing.T) {
 				KubeClientset: kubefake.NewSimpleClientset(),
 			}
 
-			got, err := r.generateApplications(argoprojiov1alpha1.ApplicationSet{
+			got, reason, err := r.generateApplications(argoprojiov1alpha1.ApplicationSet{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "name",
 					Namespace: "namespace",
@@ -185,6 +198,7 @@ func TestExtractApplications(t *testing.T) {
 				assert.NoError(t, err)
 			}
 			assert.Equal(t, expectedApps, got)
+			assert.Equal(t, cc.expectedReason, reason)
 			generatorMock.AssertNumberOfCalls(t, "GenerateParams", 1)
 
 			if cc.generateParamsError == nil {
@@ -280,7 +294,7 @@ func TestMergeTemplateApplications(t *testing.T) {
 				KubeClientset: kubefake.NewSimpleClientset(),
 			}
 
-			got, _ := r.generateApplications(argoprojiov1alpha1.ApplicationSet{
+			got, _, _ := r.generateApplications(argoprojiov1alpha1.ApplicationSet{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "name",
 					Namespace: "namespace",
@@ -1556,9 +1570,10 @@ func TestValidateGeneratedApplications(t *testing.T) {
 
 	// Test a subset of the validations that 'validateGeneratedApplications' performs
 	for _, cc := range []struct {
-		name           string
-		apps           []argov1alpha1.Application
-		expectedErrors []string
+		name             string
+		apps             []argov1alpha1.Application
+		expectedErrors   []string
+		validationErrors map[int]error
 	}{
 		{
 			name: "valid app should return true",
@@ -1580,7 +1595,8 @@ func TestValidateGeneratedApplications(t *testing.T) {
 					},
 				},
 			},
-			expectedErrors: []string{},
+			expectedErrors:   []string{},
+			validationErrors: map[int]error{},
 		},
 		{
 			name: "can't have both name and server defined",
@@ -1603,7 +1619,8 @@ func TestValidateGeneratedApplications(t *testing.T) {
 					},
 				},
 			},
-			expectedErrors: []string{"application destination can't have both name and server defined"},
+			expectedErrors:   []string{"application destination can't have both name and server defined"},
+			validationErrors: map[int]error{0: errors.New("application destination spec is invalid: application destination can't have both name and server defined: my-cluster my-server")},
 		},
 		{
 			name: "project mismatch should return error",
@@ -1625,7 +1642,8 @@ func TestValidateGeneratedApplications(t *testing.T) {
 					},
 				},
 			},
-			expectedErrors: []string{"application references project DOES-NOT-EXIST which does not exist"},
+			expectedErrors:   []string{"application references project DOES-NOT-EXIST which does not exist"},
+			validationErrors: map[int]error{0: errors.New("application references project DOES-NOT-EXIST which does not exist")},
 		},
 		{
 			name: "valid app should return true",
@@ -1647,7 +1665,8 @@ func TestValidateGeneratedApplications(t *testing.T) {
 					},
 				},
 			},
-			expectedErrors: []string{},
+			expectedErrors:   []string{},
+			validationErrors: map[int]error{},
 		},
 		{
 			name: "cluster should match",
@@ -1669,7 +1688,8 @@ func TestValidateGeneratedApplications(t *testing.T) {
 					},
 				},
 			},
-			expectedErrors: []string{"there are no clusters with this name: nonexistent-cluster"},
+			expectedErrors:   []string{"there are no clusters with this name: nonexistent-cluster"},
+			validationErrors: map[int]error{0: errors.New("application destination spec is invalid: unable to find destination server: there are no clusters with this name: nonexistent-cluster")},
 		},
 	} {
 
@@ -1716,7 +1736,7 @@ func TestValidateGeneratedApplications(t *testing.T) {
 
 			appSetInfo := argoprojiov1alpha1.ApplicationSet{}
 
-			validationErrors, err := r.validateGeneratedApplications(context.TODO(), cc.apps, appSetInfo, "namespace")
+			validationErrors, _ := r.validateGeneratedApplications(context.TODO(), cc.apps, appSetInfo, "namespace")
 			var errorMessages []string
 			for _, v := range validationErrors {
 				errorMessages = append(errorMessages, v.Error())
@@ -1733,6 +1753,17 @@ func TestValidateGeneratedApplications(t *testing.T) {
 					matched = matched || foundMatch
 				}
 				assert.True(t, matched, "An unexpected error occurrred: %v", err)
+				// validation message was returned: it should be expected
+				matched = false
+				foundMatch := reflect.DeepEqual(validationErrors, cc.validationErrors)
+				var message string
+				for _, v := range validationErrors {
+					message = v.Error()
+					break
+				}
+				assert.True(t, foundMatch, "Unble to locate validation message: %s", message)
+				matched = matched || foundMatch
+				assert.True(t, matched, "An unexpected error occurrred: %v", err)
 			}
 		})
 	}
@@ -1830,3 +1861,60 @@ func TestReconcilerValidationErrorBehaviour(t *testing.T) {
 	err = r.Client.Get(context.TODO(), crtclient.ObjectKey{Namespace: "argocd", Name: "bad-cluster"}, &app)
 	assert.Error(t, err)
 }
+
+func TestSetApplicationSetStatusCondition(t *testing.T) {
+	scheme := runtime.NewScheme()
+	err := argoprojiov1alpha1.AddToScheme(scheme)
+	assert.Nil(t, err)
+	err = argov1alpha1.AddToScheme(scheme)
+	assert.Nil(t, err)
+
+	appSet := argoprojiov1alpha1.ApplicationSet{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "name",
+			Namespace: "argocd",
+		},
+		Spec: argoprojiov1alpha1.ApplicationSetSpec{
+			Generators: []argoprojiov1alpha1.ApplicationSetGenerator{
+				{List: &argoprojiov1alpha1.ListGenerator{
+					Elements: []apiextensionsv1.JSON{{
+						Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
+					}},
+				}},
+			},
+			Template: argoprojiov1alpha1.ApplicationSetTemplate{},
+		},
+	}
+
+	appCondition := argoprojiov1alpha1.ApplicationSetCondition{
+		Type:    argoprojiov1alpha1.ApplicationSetConditionResourcesUpToDate,
+		Message: "All applications have been generated successfully",
+		Reason:  argoprojiov1alpha1.ApplicationSetReasonApplicationSetUpToDate,
+		Status:  argoprojiov1alpha1.ApplicationSetConditionStatusTrue,
+	}
+
+	kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
+	argoDBMock := dbmocks.ArgoDB{}
+	argoObjs := []runtime.Object{}
+
+	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet).Build()
+
+	r := ApplicationSetReconciler{
+		Log:      ctrl.Log.WithName("controllers").WithName("ApplicationSet"),
+		Client:   client,
+		Scheme:   scheme,
+		Renderer: &utils.Render{},
+		Recorder: record.NewFakeRecorder(1),
+		Generators: map[string]generators.Generator{
+			"List": generators.NewListGenerator(),
+		},
+		ArgoDB:           &argoDBMock,
+		ArgoAppClientset: appclientset.NewSimpleClientset(argoObjs...),
+		KubeClientset:    kubeclientset,
+	}
+
+	err = r.setApplicationSetStatusCondition(context.TODO(), &appSet, appCondition, true)
+	assert.Nil(t, err)
+
+	assert.Len(t, appSet.Status.Conditions, 3)
+}
diff --git a/test/e2e/applicationset/applicationset_test.go b/test/e2e/applicationset/applicationset_test.go
index c6ed5f7..b7d4fdb 100644
--- a/test/e2e/applicationset/applicationset_test.go
+++ b/test/e2e/applicationset/applicationset_test.go
@@ -14,6 +14,29 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
+var (
+	ExpectedConditions = []v1alpha1.ApplicationSetCondition{
+		{
+			Type:    v1alpha1.ApplicationSetConditionErrorOccurred,
+			Status:  v1alpha1.ApplicationSetConditionStatusFalse,
+			Message: "Successfully generated parameters for all Applications",
+			Reason:  v1alpha1.ApplicationSetReasonApplicationSetUpToDate,
+		},
+		{
+			Type:    v1alpha1.ApplicationSetConditionParametersGenerated,
+			Status:  v1alpha1.ApplicationSetConditionStatusTrue,
+			Message: "Successfully generated parameters for all Applications",
+			Reason:  v1alpha1.ApplicationSetReasonParametersGenerated,
+		},
+		{
+			Type:    v1alpha1.ApplicationSetConditionResourcesUpToDate,
+			Status:  v1alpha1.ApplicationSetConditionStatusTrue,
+			Message: "ApplicationSet up to date",
+			Reason:  v1alpha1.ApplicationSetReasonApplicationSetUpToDate,
+		},
+	}
+)
+
 func TestSimpleListGenerator(t *testing.T) {
 
 	expectedApp := argov1alpha1.Application{
@@ -97,6 +120,9 @@ func TestSimpleListGenerator(t *testing.T) {
 			appset.Spec.Template.Labels = map[string]string{"label-key": "label-value"}
 		}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewMetadata})).
 
+		// verify the ApplicationSet status conditions were set correctly
+		Expect(ApplicationSetHasConditions("simple-list-generator", ExpectedConditions)).
+
 		// Delete the ApplicationSet, and verify it deletes the Applications
 		When().
 		Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{*expectedAppNewMetadata}))
@@ -204,6 +230,9 @@ func TestSimpleGitDirectoryGenerator(t *testing.T) {
 			appset.Spec.Template.Labels = map[string]string{"label-key": "label-value"}
 		}).Then().Expect(ApplicationsExist(expectedAppsNewMetadata)).
 
+		// verify the ApplicationSet status conditions were set correctly
+		Expect(ApplicationSetHasConditions("simple-git-generator", ExpectedConditions)).
+
 		// Delete the ApplicationSet, and verify it deletes the Applications
 		When().
 		Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
@@ -310,6 +339,9 @@ func TestSimpleGitFilesGenerator(t *testing.T) {
 			appset.Spec.Template.Labels = map[string]string{"label-key": "label-value"}
 		}).Then().Expect(ApplicationsExist(expectedAppsNewMetadata)).
 
+		// verify the ApplicationSet status conditions were set correctly
+		Expect(ApplicationSetHasConditions("simple-git-generator", ExpectedConditions)).
+
 		// Delete the ApplicationSet, and verify it deletes the Applications
 		When().
 		Delete().Then().Expect(ApplicationsDoNotExist(expectedAppsNewNamespace))
diff --git a/test/e2e/fixture/applicationsets/actions.go b/test/e2e/fixture/applicationsets/actions.go
index 607478d..d4fcf77 100644
--- a/test/e2e/fixture/applicationsets/actions.go
+++ b/test/e2e/fixture/applicationsets/actions.go
@@ -403,15 +403,32 @@ func (a *Actions) get() (*v1alpha1.ApplicationSet, error) {
 func (a *Actions) Update(toUpdate func(*v1alpha1.ApplicationSet)) *Actions {
 	a.context.t.Helper()
 
-	appSet, err := a.get()
-	if err == nil {
-		toUpdate(appSet)
-		a.describeAction = fmt.Sprintf("updating ApplicationSet '%s'", appSet.Name)
+	timeout := 30 * time.Second
+
+	var mostRecentError error
+
+	for start := time.Now(); time.Since(start) < timeout; time.Sleep(3 * time.Second) {
+
+		appSet, err := a.get()
+		mostRecentError = err
+		if err == nil {
+			// Keep trying to update until it succeeds, or the test times out
+			toUpdate(appSet)
+			a.describeAction = fmt.Sprintf("updating ApplicationSet '%s'", appSet.Name)
+
+			fixtureClient := utils.GetE2EFixtureK8sClient()
+			_, err = fixtureClient.AppSetClientset.Update(context.Background(), utils.MustToUnstructured(&appSet), metav1.UpdateOptions{})
 
-		fixtureClient := utils.GetE2EFixtureK8sClient()
-		_, err = fixtureClient.AppSetClientset.Update(context.Background(), utils.MustToUnstructured(&appSet), metav1.UpdateOptions{})
+			if err != nil {
+				mostRecentError = err
+			} else {
+				mostRecentError = nil
+				break
+			}
+		}
 	}
-	a.lastOutput, a.lastError = "", err
+
+	a.lastOutput, a.lastError = "", mostRecentError
 	a.verifyAction()
 
 	return a
diff --git a/test/e2e/fixture/applicationsets/consequences.go b/test/e2e/fixture/applicationsets/consequences.go
index 323c69d..a530d3e 100644
--- a/test/e2e/fixture/applicationsets/consequences.go
+++ b/test/e2e/fixture/applicationsets/consequences.go
@@ -2,12 +2,15 @@ package applicationsets
 
 import (
 	"context"
+	"encoding/json"
 	"time"
 
+	"github.com/argoproj-labs/applicationset/api/v1alpha1"
 	"github.com/argoproj-labs/applicationset/test/e2e/fixture/applicationsets/utils"
 	argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/pkg/errors"
 	log "github.com/sirupsen/logrus"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -81,3 +84,28 @@ func (c *Consequences) apps() []argov1alpha1.Application {
 
 	return list.Items
 }
+
+func (c *Consequences) applicationSet(applicationSetName string) *v1alpha1.ApplicationSet {
+
+	fixtureClient := utils.GetE2EFixtureK8sClient()
+	list, err := fixtureClient.AppSetClientset.Get(context.Background(), c.actions.context.name, metav1.GetOptions{})
+	errors.CheckError(err)
+
+	var appSet v1alpha1.ApplicationSet
+
+	bytes, err := list.MarshalJSON()
+	if err != nil {
+		return &v1alpha1.ApplicationSet{}
+	}
+
+	err = json.Unmarshal(bytes, &appSet)
+	if err != nil {
+		return &v1alpha1.ApplicationSet{}
+	}
+
+	if appSet.Name == applicationSetName {
+		return &appSet
+	}
+
+	return &v1alpha1.ApplicationSet{}
+}
diff --git a/test/e2e/fixture/applicationsets/expectation.go b/test/e2e/fixture/applicationsets/expectation.go
index 82aa59c..20770fb 100644
--- a/test/e2e/fixture/applicationsets/expectation.go
+++ b/test/e2e/fixture/applicationsets/expectation.go
@@ -6,6 +6,7 @@ import (
 	"reflect"
 	"strings"
 
+	"github.com/argoproj-labs/applicationset/api/v1alpha1"
 	"github.com/argoproj-labs/applicationset/test/e2e/fixture/applicationsets/utils"
 	argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/gitops-engine/pkg/diff"
@@ -82,6 +83,28 @@ func ApplicationsExist(expectedApps []argov1alpha1.Application) Expectation {
 	}
 }
 
+// ApplicationSetHasConditions checks whether each of the 'expectedConditions' exist in the ApplicationSet status, and are
+// equivalent to provided values.
+func ApplicationSetHasConditions(applicationSetName string, expectedConditions []v1alpha1.ApplicationSetCondition) Expectation {
+	return func(c *Consequences) (state, string) {
+
+		// retrieve the application set
+		foundApplicationSet := c.applicationSet(applicationSetName)
+		if foundApplicationSet == nil {
+			return pending, fmt.Sprintf("application set '%s' not found", applicationSetName)
+		}
+
+		if !conditionsAreEqual(&expectedConditions, &foundApplicationSet.Status.Conditions) {
+			diff, err := getConditionDiff(expectedConditions, foundApplicationSet.Status.Conditions)
+			if err != nil {
+				return failed, err.Error()
+			}
+			return pending, fmt.Sprintf("application set conditions are not equal: '%s', diff: %s\n", expectedConditions, diff)
+		}
+		return succeeded, "application set successfully found"
+	}
+}
+
 // ApplicationsDoNotExist checks that each of the 'expectedApps' no longer exist in the namespace
 func ApplicationsDoNotExist(expectedApps []argov1alpha1.Application) Expectation {
 	return func(c *Consequences) (state, string) {
@@ -132,6 +155,22 @@ func getDiff(orig, new argov1alpha1.Application) (string, error) {
 
 }
 
+// getConditionDiff returns a string containing a comparison result of two ApplicationSetCondition (for test output/debug purposes)
+func getConditionDiff(orig, new []v1alpha1.ApplicationSetCondition) (string, error) {
+	var bytes []byte
+
+	for index, _ := range orig {
+		b, _, err := diff.CreateTwoWayMergePatch(orig[index], new[index], orig[index])
+		if err != nil {
+			return "", err
+		}
+		bytes = append(bytes, b...)
+	}
+
+	return string(bytes), nil
+
+}
+
 // filterFields returns a copy of Application, but with unnecessary (for testing) fields removed
 func filterFields(input argov1alpha1.Application) argov1alpha1.Application {
 
@@ -165,7 +204,29 @@ func filterFields(input argov1alpha1.Application) argov1alpha1.Application {
 	return output
 }
 
+// filterConditionFields returns a copy of ApplicationSetCondition, but with unnecessary (for testing) fields removed
+func filterConditionFields(input *[]v1alpha1.ApplicationSetCondition) *[]v1alpha1.ApplicationSetCondition {
+
+	var filteredConditions []v1alpha1.ApplicationSetCondition
+	for _, condition := range *input {
+		newCondition := &v1alpha1.ApplicationSetCondition{
+			Type:    condition.Type,
+			Status:  condition.Status,
+			Message: condition.Message,
+			Reason:  condition.Reason,
+		}
+		filteredConditions = append(filteredConditions, *newCondition)
+	}
+
+	return &filteredConditions
+}
+
 // appsAreEqual returns true if the apps are equal, comparing only fields of interest
 func appsAreEqual(one argov1alpha1.Application, two argov1alpha1.Application) bool {
 	return reflect.DeepEqual(filterFields(one), filterFields(two))
 }
+
+// conditionsAreEqual returns true if the appset status conditions are equal, comparing only fields of interest
+func conditionsAreEqual(one, two *[]v1alpha1.ApplicationSetCondition) bool {
+	return reflect.DeepEqual(filterConditionFields(one), filterConditionFields(two))
+}