diff --git a/README.md b/README.md index d46d546e..ab17574d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ apiVersion: deployment.skyscanner.net/v1alpha1 kind: ProgressiveRollout metadata: name: myprogressiverollout - namespace: argoc + namespace: argocd spec: # a reference to the target ApplicationSet sourceRef: @@ -33,17 +33,17 @@ spec: # human friendly name - name: two clusters as canary in EMEA # how many targets to update in parallel - # can be an integer or %. Default to 1 + # can be an integer or %. maxParallel: 2 # how many targets to update from the selector result - # can be an integer or %. Default to 100%. + # can be an integer or %. maxTargets: 2 # which targets to update targets: clusters: selector: matchLabels: - area: emea + area: emea - name: rollout to remaining clusters maxParallel: 25% maxTargets: 100% @@ -62,10 +62,10 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) ## Development +### Local development with Kubebuilder + 1. Install `pre-commit`: see -1. Install `kind`: see -1. Install `ArgoCD`: see -1. Install `ApplicationSet` controller: see +1. Install `kubebuilder`: see 1. Install `ArgoCD Application` API pkg: see `hack/install-argocd-application.sh` ### Update ArgoCD Application API package diff --git a/api/v1alpha1/progressiverollout_types.go b/api/v1alpha1/progressiverollout_types.go index a5f5835b..563c383c 100644 --- a/api/v1alpha1/progressiverollout_types.go +++ b/api/v1alpha1/progressiverollout_types.go @@ -90,8 +90,8 @@ func (in *ProgressiveRollout) NewStatusCondition(t string, s metav1.ConditionSta } } -// IsOwnedBy returns true if the ProgressiveRollout object has a reference to one of the owners -func (in *ProgressiveRollout) IsOwnedBy(owners []metav1.OwnerReference) bool { +// Owns returns true if the ProgressiveRollout object has a reference to one of the owners +func (in *ProgressiveRollout) Owns(owners []metav1.OwnerReference) bool { for _, owner := range owners { if owner.Kind == in.Spec.SourceRef.Kind && owner.APIVersion == *in.Spec.SourceRef.APIGroup && owner.Name == in.Spec.SourceRef.Name { return true diff --git a/api/v1alpha1/progressiverollout_types_test.go b/api/v1alpha1/progressiverollout_types_test.go index e475b6d1..2897db5b 100644 --- a/api/v1alpha1/progressiverollout_types_test.go +++ b/api/v1alpha1/progressiverollout_types_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -func TestHasOwnerReference(t *testing.T) { +func TestOwns(t *testing.T) { testCases := []struct { ownerReferences []metav1.OwnerReference expected bool @@ -45,8 +45,8 @@ func TestHasOwnerReference(t *testing.T) { }} for _, testCase := range testCases { - got := pr.IsOwnedBy(testCase.ownerReferences) - g := NewGomegaWithT(t) + got := pr.Owns(testCase.ownerReferences) + g := NewWithT(t) g.Expect(got).To(Equal(testCase.expected)) } } diff --git a/controllers/progressiverollout_controller.go b/controllers/progressiverollout_controller.go index 25c90fd0..671d83cc 100644 --- a/controllers/progressiverollout_controller.go +++ b/controllers/progressiverollout_controller.go @@ -19,6 +19,9 @@ package controllers import ( "context" "fmt" + + deploymentskyscannernetv1alpha1 "github.com/Skyscanner/argocd-progressive-rollout/api/v1alpha1" + "github.com/Skyscanner/argocd-progressive-rollout/internal/scheduler" "github.com/Skyscanner/argocd-progressive-rollout/internal/utils" argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" "github.com/go-logr/logr" @@ -34,8 +37,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" - - deploymentskyscannernetv1alpha1 "github.com/Skyscanner/argocd-progressive-rollout/api/v1alpha1" ) // ProgressiveRolloutReconciler reconciles a ProgressiveRollout object @@ -51,6 +52,7 @@ type ProgressiveRolloutReconciler struct { // +kubebuilder:rbac:groups="argoproj.io",resources=applications,verbs=get;list;watch // +kubebuilder:rbac:groups="argoproj.io",resources=applications/status,verbs=get;list;watch +// Reconcile performs the reconciling for a single named ProgressiveRollout object func (r *ProgressiveRolloutReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("progressiverollout", req.NamespacedName) @@ -60,24 +62,63 @@ func (r *ProgressiveRolloutReconciler) Reconcile(ctx context.Context, req ctrl.R log.Error(err, "unable to fetch ProgressiveRollout") return ctrl.Result{}, client.IgnoreNotFound(err) } - // Always log the ApplicationSet owner + log = r.Log.WithValues("applicationset", pr.Spec.SourceRef.Name) for _, stage := range pr.Spec.Stages { log = r.Log.WithValues("stage", stage.Name) - targets, err := r.GetTargetClusters(stage.Targets.Clusters.Selector) + // Get the clusters to update + clusters, err := r.getClustersFromSelector(stage.Targets.Clusters.Selector) if err != nil { - log.Error(err, "unable to fetch targets") + log.Error(err, "unable to fetch clusters") + return ctrl.Result{}, err + } + r.Log.V(1).Info("clusters selected", "clusters", fmt.Sprintf("%v", clusters.Items)) + + // Get the Applications owned by the ProgressiveRollout targeting the clusters + apps, err := r.getOwnedAppsFromClusters(clusters, pr) + if err != nil { + log.Error(err, "unable to fetch apps") + return ctrl.Result{}, err + } + r.Log.V(1).Info("apps selected", "apps", fmt.Sprintf("%v", apps)) + + // Remove the annotation from the OutOfSync Applications before passing them to the Scheduler + // This action allows the Scheduler to keep track at which stage an Application has been synced. + outOfSyncApps := utils.FilterAppsBySyncStatusCode(apps, argov1alpha1.SyncStatusCodeOutOfSync) + if err = r.removeAnnotationFromApps(&outOfSyncApps, utils.ProgressiveRolloutSyncedAtStageKey); err != nil { return ctrl.Result{}, err } - r.Log.V(1).Info("targets selected", "targets", targets.Items) - r.Log.Info("stage completed") + + // Get the Applications to update + scheduledApps := scheduler.Scheduler(apps, stage) + + for _, s := range scheduledApps { + // TODO: add sync method here + r.Log.Info("syncing app", "app", s) + } + + if scheduler.IsStageFailed(apps, stage) { + // TODO: updated status + r.Log.Info("stage failed") + return ctrl.Result{}, nil + } + + if scheduler.IsStageComplete(apps, stage) { + // TODO: update status + r.Log.Info("stage completed") + } else { + // TODO: update status + r.Log.Info("stage in progress") + // Stage in progress, we reconcile again until the stage is completed or failed + return ctrl.Result{Requeue: true}, nil + } } log.Info("all stages completed") - // Rollout completed + // Progressive rollout completed completed := pr.NewStatusCondition(deploymentskyscannernetv1alpha1.CompletedCondition, metav1.ConditionTrue, deploymentskyscannernetv1alpha1.StagesCompleteReason, "All stages completed") apimeta.SetStatusCondition(pr.GetStatusConditions(), completed) if err := r.Client.Status().Update(ctx, &pr); err != nil { @@ -88,6 +129,7 @@ func (r *ProgressiveRolloutReconciler) Reconcile(ctx context.Context, req ctrl.R return ctrl.Result{}, nil } +// SetupWithManager adds the reconciler to the manager, so that it gets started when the manager is started. func (r *ProgressiveRolloutReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&deploymentskyscannernetv1alpha1.ProgressiveRollout{}). @@ -100,6 +142,7 @@ func (r *ProgressiveRolloutReconciler) SetupWithManager(mgr ctrl.Manager) error Complete(r) } +// requestsForApplicationChange returns a reconcile request for a Progressive Rollout object when an Application change func (r *ProgressiveRolloutReconciler) requestsForApplicationChange(o client.Object) []reconcile.Request { /* @@ -124,7 +167,7 @@ func (r *ProgressiveRolloutReconciler) requestsForApplicationChange(o client.Obj } for _, pr := range list.Items { - if pr.IsOwnedBy(app.GetOwnerReferences()) { + if pr.Owns(app.GetOwnerReferences()) { requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ Namespace: pr.Namespace, Name: pr.Name, @@ -135,6 +178,7 @@ func (r *ProgressiveRolloutReconciler) requestsForApplicationChange(o client.Obj return requests } +// requestsForSecretChange returns a reconcile request for a Progressive Rollout object when a Secret change func (r *ProgressiveRolloutReconciler) requestsForSecretChange(o client.Object) []reconcile.Request { /* @@ -174,7 +218,7 @@ func (r *ProgressiveRolloutReconciler) requestsForSecretChange(o client.Object) for _, pr := range prList.Items { for _, app := range appList.Items { - if app.Spec.Destination.Server == string(s.Data["server"]) && pr.IsOwnedBy(app.GetOwnerReferences()) { + if app.Spec.Destination.Server == string(s.Data["server"]) && pr.Owns(app.GetOwnerReferences()) { /* Consider the following scenario: - 2 Applications @@ -198,8 +242,8 @@ func (r *ProgressiveRolloutReconciler) requestsForSecretChange(o client.Object) return requests } -// GetTargetClusters returns a list of ArgoCD clusters matching the provided label selector -func (r *ProgressiveRolloutReconciler) GetTargetClusters(selector metav1.LabelSelector) (corev1.SecretList, error) { +// getClustersFromSelector returns a list of ArgoCD clusters matching the provided label selector +func (r *ProgressiveRolloutReconciler) getClustersFromSelector(selector metav1.LabelSelector) (corev1.SecretList, error) { secrets := corev1.SecretList{} ctx := context.Background() @@ -220,3 +264,43 @@ func (r *ProgressiveRolloutReconciler) GetTargetClusters(selector metav1.LabelSe return secrets, nil } + +// getOwnedAppsFromClusters returns a list of Applications targeting the specified clusters and owned by the specified ProgressiveRollout +func (r *ProgressiveRolloutReconciler) getOwnedAppsFromClusters(clusters corev1.SecretList, pr deploymentskyscannernetv1alpha1.ProgressiveRollout) ([]argov1alpha1.Application, error) { + apps := []argov1alpha1.Application{{}} + appList := argov1alpha1.ApplicationList{} + ctx := context.Background() + + if err := r.List(ctx, &appList); err != nil { + r.Log.Error(err, "failed to list Application") + return apps, err + } + + for _, c := range clusters.Items { + for _, app := range appList.Items { + if pr.Owns(app.GetOwnerReferences()) && string(c.Data["server"]) == app.Spec.Destination.Server { + apps = append(apps, app) + } + } + } + + utils.SortAppsByName(&apps) + + return apps, nil +} + +// removeAnnotationFromApps remove an annotation from the given Applications +func (r *ProgressiveRolloutReconciler) removeAnnotationFromApps(apps *[]argov1alpha1.Application, annotation string) error { + ctx := context.Background() + + for _, app := range *apps { + if _, ok := app.Annotations[annotation]; ok { + delete(app.Annotations, annotation) + if err := r.Client.Update(ctx, &app); err != nil { + r.Log.Error(err, "failed to update Application", "app", app.Name) + return err + } + } + } + return nil +} diff --git a/controllers/progressiverollout_controller_test.go b/controllers/progressiverollout_controller_test.go index a131f8fe..7e3ac5de 100644 --- a/controllers/progressiverollout_controller_test.go +++ b/controllers/progressiverollout_controller_test.go @@ -219,14 +219,44 @@ var _ = Describe("ProgressiveRollout Controller", func() { Describe("Reconciliation loop", func() { It("should reconcile", func() { - By("creating a progressive rollout object") + By("creating an ArgoCD cluster") + cluster := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "single-stage-cluster", Namespace: namespace, Labels: map[string]string{utils.ArgoCDSecretTypeLabel: utils.ArgoCDSecretTypeCluster}}, + Data: map[string][]byte{ + "server": []byte("https://single-stage-pr.kubernetes.io"), + }, + } + Expect(k8sClient.Create(ctx, cluster)).To(Succeed()) + + By("creating an application targeting the cluster") + singleStageApp := &argov1alpha1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "single-stage-app", + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: utils.AppSetAPIGroup, + Kind: utils.AppSetKind, + Name: "single-stage-appset", + UID: uuid.NewUUID(), + }}, + }, + Spec: argov1alpha1.ApplicationSpec{Destination: argov1alpha1.ApplicationDestination{ + Server: "https://single-stage-pr.kubernetes.io", + Namespace: namespace, + Name: "remote-cluster", + }}, + Status: argov1alpha1.ApplicationStatus{Sync: argov1alpha1.SyncStatus{Status: argov1alpha1.SyncStatusCodeOutOfSync}}, + } + Expect(k8sClient.Create(ctx, singleStageApp)).To(Succeed()) + + By("creating a progressive rollout") singleStagePR = &deploymentskyscannernetv1alpha1.ProgressiveRollout{ ObjectMeta: metav1.ObjectMeta{Name: "single-stage-pr", Namespace: namespace}, Spec: deploymentskyscannernetv1alpha1.ProgressiveRolloutSpec{ SourceRef: corev1.TypedLocalObjectReference{ APIGroup: &appSetAPIRef, - Kind: "", - Name: "", + Kind: utils.AppSetKind, + Name: "single-stage-appset", }, Stages: []deploymentskyscannernetv1alpha1.ProgressiveRolloutStage{{ Name: "stage 1", diff --git a/go.mod b/go.mod index 020cccbb..a2b8e0b3 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/argoproj/argo-cd v0.8.1-0.20210218202601-6de3cf44a4cb + github.com/argoproj/gitops-engine v0.2.1-0.20210129183711-c5b7114c501f github.com/go-logr/logr v0.3.0 github.com/onsi/ginkgo v1.14.2 github.com/onsi/gomega v1.10.3 diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go new file mode 100644 index 00000000..7c6f5a97 --- /dev/null +++ b/internal/scheduler/scheduler.go @@ -0,0 +1,78 @@ +package scheduler + +import ( + deploymentskyscannernetv1alpha1 "github.com/Skyscanner/argocd-progressive-rollout/api/v1alpha1" + "github.com/Skyscanner/argocd-progressive-rollout/internal/utils" + argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" + "github.com/argoproj/gitops-engine/pkg/health" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// Scheduler returns a list of apps to sync for a given stage +func Scheduler(apps []argov1alpha1.Application, stage deploymentskyscannernetv1alpha1.ProgressiveRolloutStage) []argov1alpha1.Application { + + /* + The Scheduler takes: + - a ProgressiveRolloutStage object + - the Applications selected with the clusters selector + + The Scheduler splits the Applications in the following groups: + - OutOfSync Applications: those are the Applications to update during the stage. + - syncedInCurrentStage Applications: those are Application that synced during the current stage. Those Applications count against the number of clusters to update. + - progressingApps: those are Applications that are still in progress updating. Those Applications count against the number of clusters to update in parallel. + + Why does the Scheduler need an annotation? + Consider the scenario where we have 5 Applications - 4 OutOfSync and 1 Synced - and a stage with maxTargets = 3. + If we don't keep track on which stage the Application synced, we can't compute how many applications we have to update in the current stage. + Without the annotation, it would be impossible for the scheduler to know if the Application synced at this stage - and so we have only 2 Applications left to sync. + */ + + var scheduledApps []argov1alpha1.Application + outOfSyncApps := utils.FilterAppsBySyncStatusCode(apps, argov1alpha1.SyncStatusCodeOutOfSync) + // If there are no OutOfSync Applications, return + if len(outOfSyncApps) == 0 { + return scheduledApps + } + + syncedInCurrentStage := utils.GetSyncedAppsByStage(apps, stage.Name) + progressingApps := utils.GetAppsByHealthStatusCode(apps, health.HealthStatusProgressing) + + maxTargets, err := intstr.GetScaledValueFromIntOrPercent(&stage.MaxTargets, len(outOfSyncApps), false) + if err != nil { + return scheduledApps + } + maxParallel, err := intstr.GetScaledValueFromIntOrPercent(&stage.MaxParallel, maxTargets, false) + if err != nil { + return scheduledApps + } + + // Validation should never allow the user to explicitly use zero values for maxTargets or maxParallel. + // Due the rounding down when scaled, they might resolve to 0. + // If one of them resolve to 0, we set it to 1. + if maxTargets == 0 { + maxTargets = 1 + } + if maxParallel == 0 { + maxParallel = 1 + } + + // If we already synced the desired number of Applications, return + if maxTargets == len(syncedInCurrentStage) { + return scheduledApps + } + + for i := 0; i < maxParallel-len(progressingApps); i++ { + scheduledApps = append(scheduledApps, outOfSyncApps[i]) + } + return scheduledApps +} + +func IsStageComplete(apps []argov1alpha1.Application, stage deploymentskyscannernetv1alpha1.ProgressiveRolloutStage) bool { + //TODO: add logic + return true +} + +func IsStageFailed(apps []argov1alpha1.Application, stage deploymentskyscannernetv1alpha1.ProgressiveRolloutStage) bool { + // TODO: add logic + return false +} diff --git a/internal/scheduler/scheduler_test.go b/internal/scheduler/scheduler_test.go new file mode 100644 index 00000000..8d087759 --- /dev/null +++ b/internal/scheduler/scheduler_test.go @@ -0,0 +1,709 @@ +package scheduler + +import ( + deploymentskyscannernetv1alpha1 "github.com/Skyscanner/argocd-progressive-rollout/api/v1alpha1" + "github.com/Skyscanner/argocd-progressive-rollout/internal/utils" + argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" + "github.com/argoproj/gitops-engine/pkg/health" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "testing" +) + +func TestScheduler(t *testing.T) { + namespace := "default" + stageName := "stage" + testCases := []struct { + name string + apps []argov1alpha1.Application + stage deploymentskyscannernetv1alpha1.ProgressiveRolloutStage + expected []argov1alpha1.Application + }{ + { + name: "Applications: outOfSync 3, syncedInCurrentStage 0, progressing 0, | Stage: maxTargets 2, maxParallel 2 | Expected: scheduled 2", + apps: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-two", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-three", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + stage: deploymentskyscannernetv1alpha1.ProgressiveRolloutStage{ + Name: stageName, + MaxParallel: intstr.Parse("2"), + MaxTargets: intstr.Parse("3"), + Targets: deploymentskyscannernetv1alpha1.ProgressiveRolloutTargets{}, + }, + expected: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-three", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + }, + { + name: "Applications: outOfSync 3, syncedInCurrentStage 1, progressing 1, | Stage: maxTargets 5, maxParallel 2 | Expected: scheduled 1", + apps: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-two", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-three", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-four", + Namespace: namespace, + Annotations: map[string]string{utils.ProgressiveRolloutSyncedAtStageKey: stageName}, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-five", + Namespace: namespace, + Annotations: map[string]string{utils.ProgressiveRolloutSyncedAtStageKey: stageName}, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced, + }, + Health: argov1alpha1.HealthStatus{Status: health.HealthStatusProgressing}, + }, + }, + }, + stage: deploymentskyscannernetv1alpha1.ProgressiveRolloutStage{ + Name: stageName, + MaxParallel: intstr.Parse("2"), + MaxTargets: intstr.Parse("3"), + Targets: deploymentskyscannernetv1alpha1.ProgressiveRolloutTargets{}, + }, + expected: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + }, + { + name: "Applications: outOfSync 5, syncedInCurrentStage 0, progressing 0, | Stage: maxTargets 3, maxParallel 2 | Expected: scheduled 2", + apps: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-two", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-three", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-four", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-five", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + stage: deploymentskyscannernetv1alpha1.ProgressiveRolloutStage{ + Name: stageName, + MaxParallel: intstr.Parse("2"), + MaxTargets: intstr.Parse("3"), + Targets: deploymentskyscannernetv1alpha1.ProgressiveRolloutTargets{}, + }, + expected: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-five", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-four", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + }, + { + name: "Applications: outOfSync 5, syncedInCurrentStage 0, progressing 0, | Stage: maxTargets 50%, maxParallel 100% | Expected: scheduled 2", + apps: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-two", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-three", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-four", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-five", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + stage: deploymentskyscannernetv1alpha1.ProgressiveRolloutStage{ + Name: stageName, + MaxParallel: intstr.Parse("100%"), + MaxTargets: intstr.Parse("50%"), + Targets: deploymentskyscannernetv1alpha1.ProgressiveRolloutTargets{}, + }, + expected: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-five", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-four", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + }, + { + name: "Applications: outOfSync 2, syncedInCurrentStage 3, progressing 0, | Stage: maxTargets 3, maxParallel 1 | Expected: scheduled 0", + apps: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-two", + Namespace: namespace, + Annotations: map[string]string{ + utils.ProgressiveRolloutSyncedAtStageKey: stageName, + }, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced, + }, + Health: argov1alpha1.HealthStatus{ + Status: health.HealthStatusHealthy}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-three", + Namespace: namespace, + Annotations: map[string]string{ + utils.ProgressiveRolloutSyncedAtStageKey: stageName, + }, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced, + }, + Health: argov1alpha1.HealthStatus{ + Status: health.HealthStatusHealthy}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-four", + Namespace: namespace, + Annotations: map[string]string{ + utils.ProgressiveRolloutSyncedAtStageKey: stageName, + }, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced, + }, + Health: argov1alpha1.HealthStatus{ + Status: health.HealthStatusHealthy}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-five", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + stage: deploymentskyscannernetv1alpha1.ProgressiveRolloutStage{ + Name: stageName, + MaxParallel: intstr.Parse("1"), + MaxTargets: intstr.Parse("3"), + Targets: deploymentskyscannernetv1alpha1.ProgressiveRolloutTargets{}, + }, + expected: nil, + }, + { + name: "Applications: outOfSync 1, syncedInCurrentStage 0, progressing 0, | Stage: maxTargets 1, maxParallel 1 | Expected: scheduled 1", + apps: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + stage: deploymentskyscannernetv1alpha1.ProgressiveRolloutStage{ + Name: stageName, + MaxParallel: intstr.Parse("1"), + MaxTargets: intstr.Parse("1"), + Targets: deploymentskyscannernetv1alpha1.ProgressiveRolloutTargets{}, + }, + expected: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + }, + { + name: "Applications: outOfSync 5, syncedInCurrentStage 0, progressing 0, | Stage: maxTargets 3, maxParallel 3 | Expected: scheduled 3", + apps: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-two", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-three", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-four", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-five", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + stage: deploymentskyscannernetv1alpha1.ProgressiveRolloutStage{ + Name: stageName, + MaxParallel: intstr.Parse("3"), + MaxTargets: intstr.Parse("3"), + Targets: deploymentskyscannernetv1alpha1.ProgressiveRolloutTargets{}, + }, + expected: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-five", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-four", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + }, + { + name: "Applications: outOfSync 2, syncedInCurrentStage 0, progressing 0, | Stage: maxTargets 10%, maxParallel 10% | Expected: scheduled 1", + apps: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-two", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + stage: deploymentskyscannernetv1alpha1.ProgressiveRolloutStage{ + Name: stageName, + MaxParallel: intstr.Parse("10%"), + MaxTargets: intstr.Parse("10%"), + Targets: deploymentskyscannernetv1alpha1.ProgressiveRolloutTargets{}, + }, + expected: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + }, + + { + name: "Applications: outOfSync 2, syncedInCurrentStage 0, progressing 0, syncedInPreviousStage 1 | Stage: maxTargets 2, maxParallel 2 | Expected: scheduled 2", + apps: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-two", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-three", + Namespace: namespace, + Annotations: map[string]string{utils.ProgressiveRolloutSyncedAtStageKey: "previous-stage"}, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced, + }, + }, + }, + }, + stage: deploymentskyscannernetv1alpha1.ProgressiveRolloutStage{ + Name: stageName, + MaxParallel: intstr.Parse("2"), + MaxTargets: intstr.Parse("2"), + Targets: deploymentskyscannernetv1alpha1.ProgressiveRolloutTargets{}, + }, + expected: []argov1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-one", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "app-two", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + }, + }, + }, + }, + + { + name: "Applications: outOfSync 0, syncedInCurrentStage 0, progressing 0, | Stage: maxTargets 3, maxParallel 3 | Expected: scheduled 0", + apps: nil, + stage: deploymentskyscannernetv1alpha1.ProgressiveRolloutStage{ + Name: stageName, + MaxParallel: intstr.Parse("3"), + MaxTargets: intstr.Parse("3"), + Targets: deploymentskyscannernetv1alpha1.ProgressiveRolloutTargets{}, + }, + expected: nil, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + utils.SortAppsByName(&testCase.apps) + got := Scheduler(testCase.apps, testCase.stage) + g := NewWithT(t) + g.Expect(got).To(Equal(testCase.expected)) + }) + } +} diff --git a/internal/utils/consts.go b/internal/utils/consts.go index 82b9d554..aa145009 100644 --- a/internal/utils/consts.go +++ b/internal/utils/consts.go @@ -1,8 +1,9 @@ package utils const ( - ArgoCDSecretTypeLabel = "argocd.argoproj.io/secret-type" - ArgoCDSecretTypeCluster = "cluster" - AppSetKind = "ApplicationSet" - AppSetAPIGroup = "argoproj.io/v1alpha1" + ArgoCDSecretTypeLabel = "argocd.argoproj.io/secret-type" + ArgoCDSecretTypeCluster = "cluster" + AppSetKind = "ApplicationSet" + AppSetAPIGroup = "argoproj.io/v1alpha1" + ProgressiveRolloutSyncedAtStageKey = "apr.skyscanner.net/syncedAtStage" ) diff --git a/internal/utils/utils.go b/internal/utils/utils.go index c171b9d1..877f7216 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -1,6 +1,8 @@ package utils import ( + argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" + "github.com/argoproj/gitops-engine/pkg/health" corev1 "k8s.io/api/core/v1" "sort" ) @@ -15,3 +17,48 @@ func IsArgoCDCluster(labels map[string]string) bool { func SortSecretsByName(secrets *corev1.SecretList) { sort.SliceStable(secrets.Items, func(i, j int) bool { return secrets.Items[i].Name < secrets.Items[j].Name }) } + +// SortAppsByName sort the Application slice in place by the app name +func SortAppsByName(apps *[]argov1alpha1.Application) { + sort.SliceStable(*apps, func(i, j int) bool { return (*apps)[i].Name < (*apps)[j].Name }) +} + +// FilterAppsBySyncStatusCode returns the Applications matching the specified sync status code +func FilterAppsBySyncStatusCode(apps []argov1alpha1.Application, code argov1alpha1.SyncStatusCode) []argov1alpha1.Application { + var result []argov1alpha1.Application + + for _, app := range apps { + if app.Status.Sync.Status == code { + result = append(result, app) + } + } + + return result +} + +// GetAppsByHealthStatusCode returns the Applications matching the specified health status code +func GetAppsByHealthStatusCode(apps []argov1alpha1.Application, code health.HealthStatusCode) []argov1alpha1.Application { + var result []argov1alpha1.Application + + for _, app := range apps { + if app.Status.Health.Status == code { + result = append(result, app) + } + } + + return result +} + +// GetSyncedAppsByStage returns the Applications that synced during the given stage +func GetSyncedAppsByStage(apps []argov1alpha1.Application, name string) []argov1alpha1.Application { + var result []argov1alpha1.Application + + for _, app := range apps { + val, ok := app.Annotations[ProgressiveRolloutSyncedAtStageKey] + if ok && val == name && app.Status.Sync.Status == argov1alpha1.SyncStatusCodeSynced && app.Status.Health.Status == health.HealthStatusHealthy { + result = append(result, app) + } + } + + return result +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index f914a54b..f92f5c37 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -1,6 +1,8 @@ package utils import ( + argov1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" + "github.com/argoproj/gitops-engine/pkg/health" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -29,7 +31,7 @@ func TestIsArgoCDCluster(t *testing.T) { for _, testCase := range testCases { got := IsArgoCDCluster(testCase.labels) - g := NewGomegaWithT(t) + g := NewWithT(t) g.Expect(got).To(Equal(testCase.expected)) } } @@ -54,7 +56,208 @@ func TestSortSecretsByName(t *testing.T) { }, { ObjectMeta: metav1.ObjectMeta{Name: "clusterC", Namespace: namespace}, }}}} - g := NewGomegaWithT(t) + g := NewWithT(t) SortSecretsByName(testCase.secretList) g.Expect(testCase.secretList).Should(Equal(testCase.expected)) } + +func TestSortAppsByName(t *testing.T) { + namespace := "default" + testCase := struct { + apps *[]argov1alpha1.Application + expected *[]argov1alpha1.Application + }{ + apps: &[]argov1alpha1.Application{{ + ObjectMeta: metav1.ObjectMeta{Name: "appA", Namespace: namespace}}, { + ObjectMeta: metav1.ObjectMeta{Name: "appC", Namespace: namespace}}, { + ObjectMeta: metav1.ObjectMeta{Name: "appB", Namespace: namespace}}}, + expected: &[]argov1alpha1.Application{{ + ObjectMeta: metav1.ObjectMeta{Name: "appA", Namespace: namespace}}, { + ObjectMeta: metav1.ObjectMeta{Name: "appB", Namespace: namespace}}, { + ObjectMeta: metav1.ObjectMeta{Name: "appC", Namespace: namespace}}}, + } + + g := NewWithT(t) + SortAppsByName(testCase.apps) + g.Expect(testCase.apps).Should(Equal(testCase.expected)) +} + +func TestGetSyncedAppsByStage(t *testing.T) { + namespace := "default" + stage := "test-stage" + testCases := []struct { + name string + apps []argov1alpha1.Application + stage string + expected []argov1alpha1.Application + }{ + { + name: "Correct annotation, stage, sync status, health status", + apps: []argov1alpha1.Application{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "appA", + Namespace: namespace, + Annotations: map[string]string{ + ProgressiveRolloutSyncedAtStageKey: stage, + }}, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced}, + Health: argov1alpha1.HealthStatus{ + Status: health.HealthStatusHealthy, + }, + }, + }}, + stage: stage, + expected: []argov1alpha1.Application{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "appA", + Namespace: namespace, + Annotations: map[string]string{ + ProgressiveRolloutSyncedAtStageKey: stage, + }}, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced}, + Health: argov1alpha1.HealthStatus{ + Status: health.HealthStatusHealthy, + }, + }, + }}, + }, + { + name: "Correct annotation, sync status, health status. Incorrect annotation value", + apps: []argov1alpha1.Application{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "appA", + Namespace: namespace, + Annotations: map[string]string{ + ProgressiveRolloutSyncedAtStageKey: "wrong-stage", + }}, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced}, + Health: argov1alpha1.HealthStatus{ + Status: health.HealthStatusHealthy, + }, + }}, + }, + stage: stage, + expected: nil, + }, + { + name: "Correct annotation, value, sync status. Incorrect health status", + apps: []argov1alpha1.Application{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "appA", + Namespace: namespace, + Annotations: map[string]string{ + ProgressiveRolloutSyncedAtStageKey: stage, + }}, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced}, + Health: argov1alpha1.HealthStatus{ + Status: health.HealthStatusProgressing, + }, + }}, + }, + stage: stage, + expected: nil, + }, + { + name: "Correct annotation, value and health status. Incorrect sync status", + apps: []argov1alpha1.Application{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "appA", + Namespace: namespace, + Annotations: map[string]string{ + ProgressiveRolloutSyncedAtStageKey: stage, + }}, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync}, + Health: argov1alpha1.HealthStatus{ + Status: health.HealthStatusHealthy, + }}, + }, + }, + stage: stage, + expected: nil, + }, + { + name: "Missing annotation", + apps: []argov1alpha1.Application{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "appA", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced}, + Health: argov1alpha1.HealthStatus{ + Status: health.HealthStatusHealthy, + }}, + }, + }, + stage: stage, + expected: nil, + }, + { + name: "2 Applications: 1 with correct annotation, stage, sync status, health status. 1 with incorrect data", + apps: []argov1alpha1.Application{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "appA", + Namespace: namespace, + Annotations: map[string]string{ + ProgressiveRolloutSyncedAtStageKey: stage, + }}, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced}, + Health: argov1alpha1.HealthStatus{ + Status: health.HealthStatusHealthy, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "appB", + Namespace: namespace, + }, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeOutOfSync, + }, + Health: argov1alpha1.HealthStatus{ + Status: health.HealthStatusProgressing, + }, + }, + }}, + stage: stage, + expected: []argov1alpha1.Application{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "appA", + Namespace: namespace, + Annotations: map[string]string{ + ProgressiveRolloutSyncedAtStageKey: stage, + }}, + Status: argov1alpha1.ApplicationStatus{ + Sync: argov1alpha1.SyncStatus{ + Status: argov1alpha1.SyncStatusCodeSynced}, + Health: argov1alpha1.HealthStatus{ + Status: health.HealthStatusHealthy, + }, + }, + }}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + g := NewWithT(t) + got := GetSyncedAppsByStage(testCase.apps, testCase.stage) + g.Expect(got).Should(Equal(testCase.expected)) + }) + } +}