From 6f3a2202a23a1e46ba72ef55d4a6466c10941802 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Fri, 14 Jun 2024 18:06:14 +0200 Subject: [PATCH] fix(promo): notice changes to Argo CD source updates (#2157) Signed-off-by: Hidde Beydals --- .../argocd/api/v1alpha1/application_types.go | 30 + .../api/v1alpha1/zz_generated.deepcopy.go | 10 +- internal/controller/promotion/argocd.go | 241 +++-- internal/controller/promotion/argocd_test.go | 972 +++++++++++++----- 4 files changed, 860 insertions(+), 393 deletions(-) diff --git a/internal/controller/argocd/api/v1alpha1/application_types.go b/internal/controller/argocd/api/v1alpha1/application_types.go index 8edad01f5..ec245c1cb 100644 --- a/internal/controller/argocd/api/v1alpha1/application_types.go +++ b/internal/controller/argocd/api/v1alpha1/application_types.go @@ -1,6 +1,8 @@ package v1alpha1 import ( + "reflect" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -29,8 +31,34 @@ type ApplicationSource struct { Chart string `json:"chart,omitempty"` } +// Equals compares two instances of ApplicationSource and returns true if +// they are equal. +func (source *ApplicationSource) Equals(other *ApplicationSource) bool { + if source == nil && other == nil { + return true + } + if source == nil || other == nil { + return false + } + return reflect.DeepEqual(source.DeepCopy(), other.DeepCopy()) +} + type ApplicationSources []ApplicationSource +// Equals compares two instances of ApplicationSources and returns true if +// they are equal. +func (s ApplicationSources) Equals(other ApplicationSources) bool { + if len(s) != len(other) { + return false + } + for i := range s { + if !s[i].Equals(&other[i]) { + return false + } + } + return true +} + type RefreshType string const ( @@ -148,4 +176,6 @@ type OperationState struct { type SyncOperationResult struct { Revision string `json:"revision,omitempty"` + Source ApplicationSource `json:"source,omitempty"` + Sources ApplicationSources `json:"sources,omitempty"` } diff --git a/internal/controller/argocd/api/v1alpha1/zz_generated.deepcopy.go b/internal/controller/argocd/api/v1alpha1/zz_generated.deepcopy.go index 1266cc38f..7f5067a4a 100644 --- a/internal/controller/argocd/api/v1alpha1/zz_generated.deepcopy.go +++ b/internal/controller/argocd/api/v1alpha1/zz_generated.deepcopy.go @@ -377,7 +377,7 @@ func (in *OperationState) DeepCopyInto(out *OperationState) { if in.SyncResult != nil { in, out := &in.SyncResult, &out.SyncResult *out = new(SyncOperationResult) - **out = **in + (*in).DeepCopyInto(*out) } } @@ -439,6 +439,14 @@ func (in *SyncOperation) DeepCopy() *SyncOperation { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SyncOperationResult) DeepCopyInto(out *SyncOperationResult) { *out = *in + in.Source.DeepCopyInto(&out.Source) + if in.Sources != nil { + in, out := &in.Sources, &out.Sources + *out = make(ApplicationSources, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncOperationResult. diff --git a/internal/controller/promotion/argocd.go b/internal/controller/promotion/argocd.go index db6b4332f..c6d5414db 100644 --- a/internal/controller/promotion/argocd.go +++ b/internal/controller/promotion/argocd.go @@ -32,21 +32,29 @@ const ( type argoCDMechanism struct { argocdClient client.Client // These behaviors are overridable for testing purposes: + buildDesiredSourcesFn func( + app *argocd.Application, + update kargoapi.ArgoCDAppUpdate, + newFreight kargoapi.FreightReference, + ) (*argocd.ApplicationSource, argocd.ApplicationSources, error) mustPerformUpdateFn func( - ctx context.Context, + app *argocd.Application, update kargoapi.ArgoCDAppUpdate, newFreight kargoapi.FreightReference, + desiredSource *argocd.ApplicationSource, + desiredSources argocd.ApplicationSources, ) (argocd.OperationPhase, bool, error) - doSingleUpdateFn func( + updateApplicationSourcesFn func( ctx context.Context, - stageMeta metav1.ObjectMeta, - update kargoapi.ArgoCDAppUpdate, - newFreight kargoapi.FreightReference, + app *argocd.Application, + desiredSource *argocd.ApplicationSource, + desiredSources argocd.ApplicationSources, ) error - getArgoCDAppFn func( + getAuthorizedApplicationFn func( ctx context.Context, namespace string, name string, + stageMeta metav1.ObjectMeta, ) (*argocd.Application, error) applyArgoCDSourceUpdateFn func( argocd.ApplicationSource, @@ -68,9 +76,10 @@ func newArgoCDMechanism(argocdClient client.Client) Mechanism { a := &argoCDMechanism{ argocdClient: argocdClient, } + a.buildDesiredSourcesFn = a.buildDesiredSources a.mustPerformUpdateFn = a.mustPerformUpdate - a.doSingleUpdateFn = a.doSingleUpdate - a.getArgoCDAppFn = getApplicationFn(argocdClient) + a.updateApplicationSourcesFn = a.updateApplicationSources + a.getAuthorizedApplicationFn = a.getAuthorizedApplication a.applyArgoCDSourceUpdateFn = applyArgoCDSourceUpdate if argocdClient != nil { a.argoCDAppPatchFn = argocdClient.Patch @@ -110,8 +119,24 @@ func (a *argoCDMechanism) Promote( var updateResults = make([]argocd.OperationPhase, 0, len(updates)) for _, update := range updates { + // Retrieve the Argo CD Application. + app, err := a.getAuthorizedApplicationFn(ctx, update.AppNamespace, update.AppName, stage.ObjectMeta) + if err != nil { + return nil, newFreight, err + } + + // Build the desired source(s) for the Argo CD Application. + desiredSource, desiredSources, err := a.buildDesiredSourcesFn( + app, + update, + newFreight, + ) + if err != nil { + return nil, newFreight, err + } + // Check if the update needs to be performed and retrieve its phase. - phase, mustUpdate, err := a.mustPerformUpdateFn(ctx, update, newFreight) + phase, mustUpdate, err := a.mustPerformUpdateFn(app, update, newFreight, desiredSource, desiredSources) // If we have a phase, append it to the results. if phase != "" { @@ -140,12 +165,7 @@ func (a *argoCDMechanism) Promote( } // Perform the update. - if err := a.doSingleUpdateFn( - ctx, - stage.ObjectMeta, - update, - newFreight, - ); err != nil { + if err := a.updateApplicationSourcesFn(ctx, app, desiredSource, desiredSources); err != nil { return nil, newFreight, err } // As we have initiated an update, we should wait for it to complete. @@ -164,32 +184,53 @@ func (a *argoCDMechanism) Promote( return promo.Status.WithPhase(aggregatedPhase), newFreight, nil } -func (a *argoCDMechanism) mustPerformUpdate( - ctx context.Context, +// buildDesiredSources returns the desired source(s) for an Argo CD Application, +// by updating the current source(s) with the given source updates. +func (a *argoCDMechanism) buildDesiredSources( + app *argocd.Application, update kargoapi.ArgoCDAppUpdate, newFreight kargoapi.FreightReference, -) (phase argocd.OperationPhase, mustUpdate bool, err error) { - namespace := update.AppNamespace - if namespace == "" { - namespace = libargocd.Namespace() - } - app, err := a.getArgoCDAppFn(ctx, namespace, update.AppName) - if err != nil { - return "", false, fmt.Errorf( - "error finding Argo CD Application %q in namespace %q: %w", - update.AppName, - namespace, - err, - ) - } - if app == nil { - return "", false, fmt.Errorf( - "unable to find Argo CD Application %q in namespace %q", - update.AppName, - namespace, - ) +) (*argocd.ApplicationSource, argocd.ApplicationSources, error) { + desiredSource, desiredSources := app.Spec.Source.DeepCopy(), app.Spec.Sources.DeepCopy() + + for _, srcUpdate := range update.SourceUpdates { + if desiredSource != nil { + newSrc, err := a.applyArgoCDSourceUpdateFn(*desiredSource, newFreight, srcUpdate) + if err != nil { + return nil, nil, fmt.Errorf( + "error applying source update to Argo CD Application %q in namespace %q: %w", + update.AppName, + app.Namespace, + err, + ) + } + desiredSource = &newSrc + } + + for i, curSrc := range desiredSources { + newSrc, err := a.applyArgoCDSourceUpdateFn(curSrc, newFreight, srcUpdate) + if err != nil { + return nil, nil, fmt.Errorf( + "error applying source update to Argo CD Application %q in namespace %q: %w", + update.AppName, + app.Namespace, + err, + ) + } + desiredSources[i] = newSrc + } } + return desiredSource, desiredSources, nil +} + +func (a *argoCDMechanism) mustPerformUpdate( + app *argocd.Application, + update kargoapi.ArgoCDAppUpdate, + newFreight kargoapi.FreightReference, + desiredSource *argocd.ApplicationSource, + desiredSources argocd.ApplicationSources, +) (phase argocd.OperationPhase, mustUpdate bool, err error) { status := app.Status.OperationState if status == nil { // The application has no operation. @@ -217,13 +258,14 @@ func (a *argoCDMechanism) mustPerformUpdate( return status.Phase, false, nil } - // The operation has completed. Check if the desired revision was applied. - desiredRevision := libargocd.GetDesiredRevision(app, newFreight) if status.SyncResult == nil { // We do not have a sync result, so we cannot determine if the operation // was successful. The best recourse is to retry the operation. return "", true, errors.New("operation completed without a sync result") } + + // Check if the desired revision was applied. + desiredRevision := libargocd.GetDesiredRevision(app, newFreight) if desiredRevision != "" && status.SyncResult.Revision != desiredRevision { // The operation did not result in the desired revision being applied. // We should attempt to retry the operation. @@ -233,76 +275,39 @@ func (a *argoCDMechanism) mustPerformUpdate( ) } + // Check if the desired source(s) were applied. + if len(update.SourceUpdates) > 0 { + if (desiredSource != nil && !desiredSource.Equals(&status.SyncResult.Source)) || + !desiredSources.Equals(status.SyncResult.Sources) { + // The operation did not result in the desired source(s) being applied. + // We should attempt to retry the operation. + return "", true, fmt.Errorf( + "operation result source does not match desired source", + ) + } + } + // The operation has completed. return status.Phase, false, nil } -func (a *argoCDMechanism) doSingleUpdate( +func (a *argoCDMechanism) updateApplicationSources( ctx context.Context, - stageMeta metav1.ObjectMeta, - update kargoapi.ArgoCDAppUpdate, - newFreight kargoapi.FreightReference, + app *argocd.Application, + desiredSource *argocd.ApplicationSource, + desiredSources argocd.ApplicationSources, ) error { - namespace := update.AppNamespace - if namespace == "" { - namespace = libargocd.Namespace() - } - app, err := a.getArgoCDAppFn(ctx, namespace, update.AppName) - if err != nil { - return fmt.Errorf( - "error finding Argo CD Application %q in namespace %q: %w", - update.AppName, - namespace, - err, - ) - } - if app == nil { - return fmt.Errorf( - "unable to find Argo CD Application %q in namespace %q: %w", - update.AppName, - namespace, - err, - ) - } - // Make sure this is allowed! - if err = authorizeArgoCDAppUpdate(stageMeta, app.ObjectMeta); err != nil { - return err - } + // Create a patch for the Application. patch := client.MergeFrom(app.DeepCopy()) - for _, srcUpdate := range update.SourceUpdates { - if app.Spec.Source != nil { - var source argocd.ApplicationSource - if source, err = a.applyArgoCDSourceUpdateFn( - *app.Spec.Source, - newFreight, - srcUpdate, - ); err != nil { - return fmt.Errorf( - "error updating source of Argo CD Application %q in namespace %q: %w", - update.AppName, - namespace, - err, - ) - } - app.Spec.Source = &source - } - for i, source := range app.Spec.Sources { - if source, err = a.applyArgoCDSourceUpdateFn( - source, - newFreight, - srcUpdate, - ); err != nil { - return fmt.Errorf( - "error updating source(s) of Argo CD Application %q in namespace %q: %w", - update.AppName, - namespace, - err, - ) - } - app.Spec.Sources[i] = source - } - } + + // Initiate a "hard" refresh. app.ObjectMeta.Annotations[argocd.AnnotationKeyRefresh] = string(argocd.RefreshTypeHard) + + // Update the desired source(s) in the Argo CD Application. + app.Spec.Source = desiredSource.DeepCopy() + app.Spec.Sources = desiredSources.DeepCopy() + + // Initiate a new operation. app.Operation = &argocd.Operation{ InitiatedBy: argocd.OperationInitiator{ Username: applicationOperationInitiator, @@ -332,7 +337,9 @@ func (a *argoCDMechanism) doSingleUpdate( for _, source := range app.Spec.Sources { app.Operation.Sync.Revisions = append(app.Operation.Sync.Revisions, source.TargetRevision) } - if err = a.argoCDAppPatchFn( + + // Patch the Application with the changes from above. + if err := a.argoCDAppPatchFn( ctx, app, patch, @@ -411,20 +418,32 @@ func (a *argoCDMechanism) logAppEvent(ctx context.Context, app *argocd.Applicati } } -func getApplicationFn( - argocdClient client.Client, -) func( +// getAuthorizedApplication returns an Argo CD Application in the given namespace +// with the given name, if it is authorized for mutation by the Kargo Stage +// represented by stageMeta. +func (a *argoCDMechanism) getAuthorizedApplication( ctx context.Context, namespace string, name string, + stageMeta metav1.ObjectMeta, ) (*argocd.Application, error) { - return func( - ctx context.Context, - namespace string, - name string, - ) (*argocd.Application, error) { - return argocd.GetApplication(ctx, argocdClient, namespace, name) + if namespace == "" { + namespace = libargocd.Namespace() + } + + app, err := argocd.GetApplication(ctx, a.argocdClient, namespace, name) + if err != nil { + return nil, fmt.Errorf("error finding Argo CD Application %q in namespace %q: %w", name, namespace, err) } + if app == nil { + return nil, fmt.Errorf("unable to find Argo CD Application %q in namespace %q", name, namespace) + } + + if err = authorizeArgoCDAppUpdate(stageMeta, app.ObjectMeta); err != nil { + return nil, err + } + + return app, nil } // authorizeArgoCDAppUpdate returns an error if the Argo CD Application diff --git a/internal/controller/promotion/argocd_test.go b/internal/controller/promotion/argocd_test.go index 7468127cd..99a258480 100644 --- a/internal/controller/promotion/argocd_test.go +++ b/internal/controller/promotion/argocd_test.go @@ -16,6 +16,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/interceptor" kargoapi "github.com/akuity/kargo/api/v1alpha1" + libargocd "github.com/akuity/kargo/internal/argocd" argocd "github.com/akuity/kargo/internal/controller/argocd/api/v1alpha1" "github.com/akuity/kargo/internal/logging" ) @@ -26,9 +27,10 @@ func TestNewArgoCDMechanism(t *testing.T) { ) apm, ok := pm.(*argoCDMechanism) require.True(t, ok) + require.NotNil(t, apm.buildDesiredSourcesFn) require.NotNil(t, apm.mustPerformUpdateFn) - require.NotNil(t, apm.doSingleUpdateFn) - require.NotNil(t, apm.getArgoCDAppFn) + require.NotNil(t, apm.updateApplicationSourcesFn) + require.NotNil(t, apm.getAuthorizedApplicationFn) require.NotNil(t, apm.applyArgoCDSourceUpdateFn) require.NotNil(t, apm.argoCDAppPatchFn) } @@ -94,14 +96,104 @@ func TestArgoCDPromote(t *testing.T) { ) }, }, + { + name: "error retrieving authorized application", + promoMech: &argoCDMechanism{ + argocdClient: fake.NewClientBuilder().Build(), + getAuthorizedApplicationFn: func( + context.Context, + string, + string, + metav1.ObjectMeta, + ) (*argocd.Application, error) { + return nil, errors.New("something went wrong") + }, + }, + stage: &kargoapi.Stage{ + Spec: kargoapi.StageSpec{ + PromotionMechanisms: &kargoapi.PromotionMechanisms{ + ArgoCDAppUpdates: []kargoapi.ArgoCDAppUpdate{ + {}, + }, + }, + }, + }, + assertions: func( + t *testing.T, + _ *kargoapi.PromotionStatus, + newFreightIn kargoapi.FreightReference, + newFreightOut kargoapi.FreightReference, + err error, + ) { + require.ErrorContains(t, err, "something went wrong") + require.Equal(t, newFreightIn, newFreightOut) + }, + }, + { + name: "error building desired sources", + promoMech: &argoCDMechanism{ + argocdClient: fake.NewClientBuilder().Build(), + getAuthorizedApplicationFn: func( + context.Context, + string, + string, + metav1.ObjectMeta, + ) (*argocd.Application, error) { + return &argocd.Application{}, nil + }, + buildDesiredSourcesFn: func( + *argocd.Application, + kargoapi.ArgoCDAppUpdate, + kargoapi.FreightReference, + ) (*argocd.ApplicationSource, argocd.ApplicationSources, error) { + return nil, nil, errors.New("something went wrong") + }, + }, + stage: &kargoapi.Stage{ + Spec: kargoapi.StageSpec{ + PromotionMechanisms: &kargoapi.PromotionMechanisms{ + ArgoCDAppUpdates: []kargoapi.ArgoCDAppUpdate{ + {}, + }, + }, + }, + }, + assertions: func( + t *testing.T, + _ *kargoapi.PromotionStatus, + newFreightIn kargoapi.FreightReference, + newFreightOut kargoapi.FreightReference, + err error, + ) { + require.ErrorContains(t, err, "something went wrong") + require.Equal(t, newFreightIn, newFreightOut) + }, + }, { name: "error determining if update is necessary", promoMech: &argoCDMechanism{ argocdClient: fake.NewClientBuilder().Build(), - mustPerformUpdateFn: func( + getAuthorizedApplicationFn: func( context.Context, + string, + string, + metav1.ObjectMeta, + ) (*argocd.Application, error) { + return &argocd.Application{}, nil + }, + buildDesiredSourcesFn: func( + *argocd.Application, + kargoapi.ArgoCDAppUpdate, + kargoapi.FreightReference, + ) (*argocd.ApplicationSource, argocd.ApplicationSources, error) { + return nil, nil, nil + }, + mustPerformUpdateFn: func( + *argocd.Application, kargoapi.ArgoCDAppUpdate, kargoapi.FreightReference, + *argocd.ApplicationSource, + argocd.ApplicationSources, ) (argocd.OperationPhase, bool, error) { return "", false, errors.New("something went wrong") }, @@ -130,18 +222,35 @@ func TestArgoCDPromote(t *testing.T) { name: "determination error can be solved by applying update", promoMech: &argoCDMechanism{ argocdClient: fake.NewClientBuilder().Build(), - mustPerformUpdateFn: func( + getAuthorizedApplicationFn: func( context.Context, + string, + string, + metav1.ObjectMeta, + ) (*argocd.Application, error) { + return &argocd.Application{}, nil + }, + buildDesiredSourcesFn: func( + *argocd.Application, kargoapi.ArgoCDAppUpdate, kargoapi.FreightReference, + ) (*argocd.ApplicationSource, argocd.ApplicationSources, error) { + return nil, nil, nil + }, + mustPerformUpdateFn: func( + *argocd.Application, + kargoapi.ArgoCDAppUpdate, + kargoapi.FreightReference, + *argocd.ApplicationSource, + argocd.ApplicationSources, ) (argocd.OperationPhase, bool, error) { return "", true, fmt.Errorf("something went wrong") }, - doSingleUpdateFn: func( + updateApplicationSourcesFn: func( context.Context, - metav1.ObjectMeta, - kargoapi.ArgoCDAppUpdate, - kargoapi.FreightReference, + *argocd.Application, + *argocd.ApplicationSource, + argocd.ApplicationSources, ) error { return nil }, @@ -170,10 +279,27 @@ func TestArgoCDPromote(t *testing.T) { name: "must wait for update to complete", promoMech: &argoCDMechanism{ argocdClient: fake.NewClientBuilder().Build(), - mustPerformUpdateFn: func( + getAuthorizedApplicationFn: func( context.Context, + string, + string, + metav1.ObjectMeta, + ) (*argocd.Application, error) { + return &argocd.Application{}, nil + }, + buildDesiredSourcesFn: func( + *argocd.Application, + kargoapi.ArgoCDAppUpdate, + kargoapi.FreightReference, + ) (*argocd.ApplicationSource, argocd.ApplicationSources, error) { + return nil, nil, nil + }, + mustPerformUpdateFn: func( + *argocd.Application, kargoapi.ArgoCDAppUpdate, kargoapi.FreightReference, + *argocd.ApplicationSource, + argocd.ApplicationSources, ) (argocd.OperationPhase, bool, error) { return argocd.OperationRunning, false, nil }, @@ -203,10 +329,27 @@ func TestArgoCDPromote(t *testing.T) { name: "must wait for operation from different user to complete", promoMech: &argoCDMechanism{ argocdClient: fake.NewClientBuilder().Build(), - mustPerformUpdateFn: func( + getAuthorizedApplicationFn: func( context.Context, + string, + string, + metav1.ObjectMeta, + ) (*argocd.Application, error) { + return &argocd.Application{}, nil + }, + buildDesiredSourcesFn: func( + *argocd.Application, + kargoapi.ArgoCDAppUpdate, + kargoapi.FreightReference, + ) (*argocd.ApplicationSource, argocd.ApplicationSources, error) { + return nil, nil, nil + }, + mustPerformUpdateFn: func( + *argocd.Application, kargoapi.ArgoCDAppUpdate, kargoapi.FreightReference, + *argocd.ApplicationSource, + argocd.ApplicationSources, ) (argocd.OperationPhase, bool, error) { return argocd.OperationRunning, false, fmt.Errorf("waiting for operation to complete") }, @@ -236,18 +379,35 @@ func TestArgoCDPromote(t *testing.T) { name: "error applying update", promoMech: &argoCDMechanism{ argocdClient: fake.NewClientBuilder().Build(), - mustPerformUpdateFn: func( + getAuthorizedApplicationFn: func( context.Context, + string, + string, + metav1.ObjectMeta, + ) (*argocd.Application, error) { + return &argocd.Application{}, nil + }, + buildDesiredSourcesFn: func( + *argocd.Application, + kargoapi.ArgoCDAppUpdate, + kargoapi.FreightReference, + ) (*argocd.ApplicationSource, argocd.ApplicationSources, error) { + return nil, nil, nil + }, + mustPerformUpdateFn: func( + *argocd.Application, kargoapi.ArgoCDAppUpdate, kargoapi.FreightReference, + *argocd.ApplicationSource, + argocd.ApplicationSources, ) (argocd.OperationPhase, bool, error) { return "", true, nil }, - doSingleUpdateFn: func( + updateApplicationSourcesFn: func( context.Context, - metav1.ObjectMeta, - kargoapi.ArgoCDAppUpdate, - kargoapi.FreightReference, + *argocd.Application, + *argocd.ApplicationSource, + argocd.ApplicationSources, ) error { return errors.New("something went wrong") }, @@ -281,16 +441,35 @@ func TestArgoCDPromote(t *testing.T) { name: "failed and pending update", promoMech: &argoCDMechanism{ argocdClient: fake.NewClientBuilder().Build(), - mustPerformUpdateFn: func() func( + getAuthorizedApplicationFn: func( context.Context, + string, + string, + metav1.ObjectMeta, + ) (*argocd.Application, error) { + return &argocd.Application{}, nil + }, + buildDesiredSourcesFn: func( + *argocd.Application, + kargoapi.ArgoCDAppUpdate, + kargoapi.FreightReference, + ) (*argocd.ApplicationSource, argocd.ApplicationSources, error) { + return nil, nil, nil + }, + mustPerformUpdateFn: func() func( + *argocd.Application, kargoapi.ArgoCDAppUpdate, kargoapi.FreightReference, + *argocd.ApplicationSource, + argocd.ApplicationSources, ) (argocd.OperationPhase, bool, error) { var count uint return func( - context.Context, + *argocd.Application, kargoapi.ArgoCDAppUpdate, kargoapi.FreightReference, + *argocd.ApplicationSource, + argocd.ApplicationSources, ) (argocd.OperationPhase, bool, error) { count++ if count > 1 { @@ -299,11 +478,11 @@ func TestArgoCDPromote(t *testing.T) { return "", true, nil } }(), - doSingleUpdateFn: func( + updateApplicationSourcesFn: func( context.Context, - metav1.ObjectMeta, - kargoapi.ArgoCDAppUpdate, - kargoapi.FreightReference, + *argocd.Application, + *argocd.ApplicationSource, + argocd.ApplicationSources, ) error { return nil }, @@ -334,10 +513,27 @@ func TestArgoCDPromote(t *testing.T) { name: "operation phase aggregation error", promoMech: &argoCDMechanism{ argocdClient: fake.NewClientBuilder().Build(), - mustPerformUpdateFn: func( + getAuthorizedApplicationFn: func( context.Context, + string, + string, + metav1.ObjectMeta, + ) (*argocd.Application, error) { + return &argocd.Application{}, nil + }, + buildDesiredSourcesFn: func( + *argocd.Application, + kargoapi.ArgoCDAppUpdate, + kargoapi.FreightReference, + ) (*argocd.ApplicationSource, argocd.ApplicationSources, error) { + return nil, nil, nil + }, + mustPerformUpdateFn: func( + *argocd.Application, kargoapi.ArgoCDAppUpdate, kargoapi.FreightReference, + *argocd.ApplicationSource, + argocd.ApplicationSources, ) (argocd.OperationPhase, bool, error) { return "Unknown", false, nil }, @@ -366,10 +562,27 @@ func TestArgoCDPromote(t *testing.T) { name: "completed", promoMech: &argoCDMechanism{ argocdClient: fake.NewClientBuilder().Build(), - mustPerformUpdateFn: func( + getAuthorizedApplicationFn: func( context.Context, + string, + string, + metav1.ObjectMeta, + ) (*argocd.Application, error) { + return &argocd.Application{}, nil + }, + buildDesiredSourcesFn: func( + *argocd.Application, + kargoapi.ArgoCDAppUpdate, + kargoapi.FreightReference, + ) (*argocd.ApplicationSource, argocd.ApplicationSources, error) { + return nil, nil, nil + }, + mustPerformUpdateFn: func( + *argocd.Application, kargoapi.ArgoCDAppUpdate, kargoapi.FreightReference, + *argocd.ApplicationSource, + argocd.ApplicationSources, ) (argocd.OperationPhase, bool, error) { return argocd.OperationSucceeded, false, nil }, @@ -412,89 +625,255 @@ func TestArgoCDPromote(t *testing.T) { } } -func TestArgoCDMustPerformUpdate(t *testing.T) { +func TestArgoCDBuildDesiredSources(t *testing.T) { testCases := []struct { name string + reconciler *argoCDMechanism modifyApplication func(*argocd.Application) - newFreight kargoapi.FreightReference - interceptor interceptor.Funcs - assertions func(t *testing.T, phase argocd.OperationPhase, mustUpdate bool, err error) + update kargoapi.ArgoCDAppUpdate + assertions func( + t *testing.T, + oldSource, newSource *argocd.ApplicationSource, + oldSources, newSources argocd.ApplicationSources, + err error, + ) }{ { - name: "error getting Argo CD App", - interceptor: interceptor.Funcs{ - Get: func(context.Context, client.WithWatch, client.ObjectKey, client.Object, ...client.GetOption) error { - return errors.New("something went wrong") + name: "applies updates to source", + reconciler: &argoCDMechanism{ + applyArgoCDSourceUpdateFn: func( + src argocd.ApplicationSource, + _ kargoapi.FreightReference, + _ kargoapi.ArgoCDSourceUpdate, + ) (argocd.ApplicationSource, error) { + if src.RepoURL == "updated-url" { + src.TargetRevision = "updated-revision" + return src, nil + } + if src.RepoURL == "" { + src.RepoURL = "updated-url" + } + return src, nil }, }, - assertions: func(t *testing.T, phase argocd.OperationPhase, mustUpdate bool, err error) { - require.ErrorContains(t, err, "error finding Argo CD Application") - require.ErrorContains(t, err, "something went wrong") - require.Empty(t, phase) - require.False(t, mustUpdate) - }, - }, - { - name: "Argo CD App not found", modifyApplication: func(app *argocd.Application) { - app.ObjectMeta = metav1.ObjectMeta{} + app.Spec.Source = &argocd.ApplicationSource{} }, - assertions: func(t *testing.T, phase argocd.OperationPhase, mustUpdate bool, err error) { - require.ErrorContains(t, err, "unable to find Argo CD Application") - require.Empty(t, phase) - require.False(t, mustUpdate) + update: kargoapi.ArgoCDAppUpdate{ + SourceUpdates: []kargoapi.ArgoCDSourceUpdate{ + {}, {}, + }, }, - }, - { - name: "no operation state", - assertions: func(t *testing.T, phase argocd.OperationPhase, mustUpdate bool, err error) { + assertions: func( + t *testing.T, + oldSource, newSource *argocd.ApplicationSource, + oldSources, newSources argocd.ApplicationSources, + err error, + ) { require.NoError(t, err) - require.Empty(t, phase) - require.True(t, mustUpdate) + require.True(t, oldSources.Equals(newSources)) + + require.False(t, oldSource.Equals(newSource)) + require.Equal(t, "updated-url", newSource.RepoURL) + require.Equal(t, "updated-revision", newSource.TargetRevision) }, }, { - name: "pending operation initiated by different user", + name: "error applying update to source", + reconciler: &argoCDMechanism{ + applyArgoCDSourceUpdateFn: func( + argocd.ApplicationSource, + kargoapi.FreightReference, + kargoapi.ArgoCDSourceUpdate, + ) (argocd.ApplicationSource, error) { + return argocd.ApplicationSource{}, errors.New("something went wrong") + }, + }, modifyApplication: func(app *argocd.Application) { - app.Status.OperationState = &argocd.OperationState{ - Phase: argocd.OperationRunning, - Operation: argocd.Operation{ - InitiatedBy: argocd.OperationInitiator{ - Username: "someone-else", - }, - }, - } + app.Spec.Source = &argocd.ApplicationSource{} }, - assertions: func(t *testing.T, phase argocd.OperationPhase, mustUpdate bool, err error) { - require.ErrorContains(t, err, "current operation was not initiated by") - require.ErrorContains(t, err, "waiting for operation to complete") - require.Equal(t, argocd.OperationRunning, phase) - require.False(t, mustUpdate) + update: kargoapi.ArgoCDAppUpdate{ + SourceUpdates: []kargoapi.ArgoCDSourceUpdate{ + {}, + }, + }, + assertions: func( + t *testing.T, + _, newSource *argocd.ApplicationSource, + _, newSources argocd.ApplicationSources, + err error, + ) { + require.ErrorContains(t, err, "something went wrong") + require.Nil(t, newSource) + require.Nil(t, newSources) }, }, { - name: "completed operation initiated by different user", + name: "applies updates to sources", + reconciler: &argoCDMechanism{ + applyArgoCDSourceUpdateFn: func( + src argocd.ApplicationSource, + _ kargoapi.FreightReference, + _ kargoapi.ArgoCDSourceUpdate, + ) (argocd.ApplicationSource, error) { + if src.RepoURL == "url-1" { + src.TargetRevision = "updated-revision-1" + return src, nil + } + if src.RepoURL == "url-2" { + src.TargetRevision = "updated-revision-2" + return src, nil + } + return src, nil + }, + }, modifyApplication: func(app *argocd.Application) { - app.Status.OperationState = &argocd.OperationState{ - Phase: argocd.OperationSucceeded, - Operation: argocd.Operation{ - InitiatedBy: argocd.OperationInitiator{ - Username: "someone-else", - }, + app.Spec.Sources = argocd.ApplicationSources{ + { + RepoURL: "url-1", + }, + { + RepoURL: "url-2", }, } }, - assertions: func(t *testing.T, phase argocd.OperationPhase, mustUpdate bool, err error) { + update: kargoapi.ArgoCDAppUpdate{ + SourceUpdates: []kargoapi.ArgoCDSourceUpdate{ + {}, + }, + }, + assertions: func( + t *testing.T, + oldSource, newSource *argocd.ApplicationSource, + oldSources, newSources argocd.ApplicationSources, + err error, + ) { require.NoError(t, err) - require.True(t, mustUpdate) - require.Empty(t, phase) + require.True(t, oldSource.Equals(newSource)) + require.False(t, oldSources.Equals(newSources)) + + require.Equal(t, 2, len(newSources)) + require.Equal(t, "updated-revision-1", newSources[0].TargetRevision) + require.Equal(t, "updated-revision-2", newSources[1].TargetRevision) }, }, { - name: "pending operation initiated by us", - modifyApplication: func(app *argocd.Application) { - app.Status.OperationState = &argocd.OperationState{ - Phase: argocd.OperationRunning, + name: "error applying update to sources", + reconciler: &argoCDMechanism{ + applyArgoCDSourceUpdateFn: func( + argocd.ApplicationSource, + kargoapi.FreightReference, + kargoapi.ArgoCDSourceUpdate, + ) (argocd.ApplicationSource, error) { + return argocd.ApplicationSource{}, errors.New("something went wrong") + }, + }, + modifyApplication: func(app *argocd.Application) { + app.Spec.Sources = argocd.ApplicationSources{ + {}, + } + }, + update: kargoapi.ArgoCDAppUpdate{ + SourceUpdates: []kargoapi.ArgoCDSourceUpdate{ + {}, + }, + }, + assertions: func( + t *testing.T, + _, newSource *argocd.ApplicationSource, + _, newSources argocd.ApplicationSources, + err error, + ) { + require.ErrorContains(t, err, "something went wrong") + require.Nil(t, newSource) + require.Nil(t, newSources) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + app := &argocd.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-app", + Namespace: "fake-namespace", + }, + } + if testCase.modifyApplication != nil { + testCase.modifyApplication(app) + } + + oldSource, oldSources := app.Spec.Source.DeepCopy(), app.Spec.Sources.DeepCopy() + newSource, newSources, err := testCase.reconciler.buildDesiredSources( + app, + testCase.update, + kargoapi.FreightReference{}, + ) + testCase.assertions(t, oldSource, newSource, oldSources, newSources, err) + }) + } +} + +func TestArgoCDMustPerformUpdate(t *testing.T) { + testCases := []struct { + name string + modifyApplication func(*argocd.Application) + update kargoapi.ArgoCDAppUpdate + newFreight kargoapi.FreightReference + desiredSource *argocd.ApplicationSource + desiredSources argocd.ApplicationSources + assertions func(t *testing.T, phase argocd.OperationPhase, mustUpdate bool, err error) + }{ + { + name: "no operation state", + assertions: func(t *testing.T, phase argocd.OperationPhase, mustUpdate bool, err error) { + require.NoError(t, err) + require.Empty(t, phase) + require.True(t, mustUpdate) + }, + }, + { + name: "pending operation initiated by different user", + modifyApplication: func(app *argocd.Application) { + app.Status.OperationState = &argocd.OperationState{ + Phase: argocd.OperationRunning, + Operation: argocd.Operation{ + InitiatedBy: argocd.OperationInitiator{ + Username: "someone-else", + }, + }, + } + }, + assertions: func(t *testing.T, phase argocd.OperationPhase, mustUpdate bool, err error) { + require.ErrorContains(t, err, "current operation was not initiated by") + require.ErrorContains(t, err, "waiting for operation to complete") + require.Equal(t, argocd.OperationRunning, phase) + require.False(t, mustUpdate) + }, + }, + { + name: "completed operation initiated by different user", + modifyApplication: func(app *argocd.Application) { + app.Status.OperationState = &argocd.OperationState{ + Phase: argocd.OperationSucceeded, + Operation: argocd.Operation{ + InitiatedBy: argocd.OperationInitiator{ + Username: "someone-else", + }, + }, + } + }, + assertions: func(t *testing.T, phase argocd.OperationPhase, mustUpdate bool, err error) { + require.NoError(t, err) + require.True(t, mustUpdate) + require.Empty(t, phase) + }, + }, + { + name: "pending operation initiated by us", + modifyApplication: func(app *argocd.Application) { + app.Status.OperationState = &argocd.OperationState{ + Phase: argocd.OperationRunning, Operation: argocd.Operation{ InitiatedBy: argocd.OperationInitiator{ Username: applicationOperationInitiator, @@ -588,6 +967,83 @@ func TestArgoCDMustPerformUpdate(t *testing.T) { require.True(t, mustUpdate) }, }, + { + name: "desired source does not match operation state", + modifyApplication: func(app *argocd.Application) { + app.Spec.Source = &argocd.ApplicationSource{ + RepoURL: "https://github.com/universe/42", + } + app.Status.OperationState = &argocd.OperationState{ + Phase: argocd.OperationSucceeded, + Operation: argocd.Operation{ + InitiatedBy: argocd.OperationInitiator{ + Username: applicationOperationInitiator, + }, + }, + SyncResult: &argocd.SyncOperationResult{ + Revision: "fake-revision", + Source: argocd.ApplicationSource{ + RepoURL: "https://github.com/different/universe", + }, + }, + } + }, + update: kargoapi.ArgoCDAppUpdate{ + SourceUpdates: []kargoapi.ArgoCDSourceUpdate{ + {}, + }, + }, + desiredSource: &argocd.ApplicationSource{ + RepoURL: "http://github.com/universe/42", + }, + assertions: func(t *testing.T, phase argocd.OperationPhase, mustUpdate bool, err error) { + require.ErrorContains(t, err, "does not match desired source") + require.Empty(t, phase) + require.True(t, mustUpdate) + }, + }, + { + name: "desired sources do not match operation state", + modifyApplication: func(app *argocd.Application) { + app.Spec.Sources = argocd.ApplicationSources{ + { + RepoURL: "https://github.com/universe/42", + }, + } + app.Status.OperationState = &argocd.OperationState{ + Phase: argocd.OperationSucceeded, + Operation: argocd.Operation{ + InitiatedBy: argocd.OperationInitiator{ + Username: applicationOperationInitiator, + }, + }, + SyncResult: &argocd.SyncOperationResult{ + Revision: "fake-revision", + Sources: argocd.ApplicationSources{ + { + RepoURL: "https://github.com/different/universe", + }, + }, + }, + } + }, + update: kargoapi.ArgoCDAppUpdate{ + SourceUpdates: []kargoapi.ArgoCDSourceUpdate{ + {}, + }, + }, + desiredSource: &argocd.ApplicationSource{}, + desiredSources: argocd.ApplicationSources{ + { + RepoURL: "https://github.com/universe/42", + }, + }, + assertions: func(t *testing.T, phase argocd.OperationPhase, mustUpdate bool, err error) { + require.ErrorContains(t, err, "does not match desired source") + require.Empty(t, phase) + require.True(t, mustUpdate) + }, + }, { name: "operation completed", modifyApplication: func(app *argocd.Application) { @@ -637,199 +1093,34 @@ func TestArgoCDMustPerformUpdate(t *testing.T) { testCase.modifyApplication(app) } - c := fake.NewClientBuilder(). - WithScheme(scheme). - WithObjects(app). - WithInterceptorFuncs(testCase.interceptor). - Build() - - mechanism := newArgoCDMechanism(c) + mechanism := newArgoCDMechanism(fake.NewClientBuilder().WithScheme(scheme).Build()) argocdMech, ok := mechanism.(*argoCDMechanism) require.True(t, ok) phase, mustUpdate, err := argocdMech.mustPerformUpdate( - context.Background(), - kargoapi.ArgoCDAppUpdate{ - AppName: "fake-name", - AppNamespace: "fake-namespace", - }, + app, + testCase.update, testCase.newFreight, + testCase.desiredSource, + testCase.desiredSources, ) testCase.assertions(t, phase, mustUpdate, err) }) } } -func TestArgoCDDoSingleUpdate(t *testing.T) { +func TestArgoCDUpdateApplicationSources(t *testing.T) { testCases := []struct { - name string - promoMech *argoCDMechanism - stageMeta metav1.ObjectMeta - update kargoapi.ArgoCDAppUpdate - assertions func(*testing.T, error) + name string + promoMech *argoCDMechanism + app *argocd.Application + desiredSource *argocd.ApplicationSource + desiredSources argocd.ApplicationSources + assertions func(*testing.T, error) }{ - { - name: "error getting Argo CD App", - promoMech: &argoCDMechanism{ - getArgoCDAppFn: func( - context.Context, - string, - string, - ) (*argocd.Application, error) { - return nil, errors.New("something went wrong") - }, - }, - assertions: func(t *testing.T, err error) { - require.ErrorContains(t, err, "error finding Argo CD Application") - require.ErrorContains(t, err, "something went wrong") - }, - }, - { - name: "Argo CD App not found", - promoMech: &argoCDMechanism{ - getArgoCDAppFn: func( - context.Context, - string, - string, - ) (*argocd.Application, error) { - return nil, nil - }, - }, - assertions: func(t *testing.T, err error) { - require.ErrorContains(t, err, "unable to find Argo CD Application") - }, - }, - { - name: "update not authorized", - promoMech: &argoCDMechanism{ - getArgoCDAppFn: func( - context.Context, - string, - string, - ) (*argocd.Application, error) { - return &argocd.Application{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-name", - Namespace: "fake-namespace", - // The annotations that would permit this are missing - }, - }, nil - }, - }, - stageMeta: metav1.ObjectMeta{ - Name: "fake-name", - Namespace: "fake-namespace", - }, - assertions: func(t *testing.T, err error) { - require.ErrorContains(t, err, "does not permit mutation by") - }, - }, - { - name: "error updating app.Spec.Source", - promoMech: &argoCDMechanism{ - getArgoCDAppFn: func( - context.Context, - string, - string, - ) (*argocd.Application, error) { - return &argocd.Application{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-name", - Namespace: "fake-namespace", - Annotations: map[string]string{ - authorizedStageAnnotationKey: "fake-namespace:fake-name", - }, - }, - Spec: argocd.ApplicationSpec{ - Source: &argocd.ApplicationSource{}, - }, - }, nil - }, - applyArgoCDSourceUpdateFn: func( - argocd.ApplicationSource, - kargoapi.FreightReference, - kargoapi.ArgoCDSourceUpdate, - ) (argocd.ApplicationSource, error) { - return argocd.ApplicationSource{}, errors.New("something went wrong") - }, - }, - stageMeta: metav1.ObjectMeta{ - Name: "fake-name", - Namespace: "fake-namespace", - }, - update: kargoapi.ArgoCDAppUpdate{ - SourceUpdates: []kargoapi.ArgoCDSourceUpdate{ - {}, - }, - }, - assertions: func(t *testing.T, err error) { - require.ErrorContains(t, err, "error updating source of Argo CD Application") - require.ErrorContains(t, err, "something went wrong") - }, - }, - { - name: "error updating app.Spec.Sources", - promoMech: &argoCDMechanism{ - getArgoCDAppFn: func( - context.Context, - string, - string, - ) (*argocd.Application, error) { - return &argocd.Application{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-name", - Namespace: "fake-namespace", - Annotations: map[string]string{ - authorizedStageAnnotationKey: "fake-namespace:fake-name", - }, - }, - Spec: argocd.ApplicationSpec{ - Sources: []argocd.ApplicationSource{ - {}, - }, - }, - }, nil - }, - applyArgoCDSourceUpdateFn: func( - argocd.ApplicationSource, - kargoapi.FreightReference, - kargoapi.ArgoCDSourceUpdate, - ) (argocd.ApplicationSource, error) { - return argocd.ApplicationSource{}, errors.New("something went wrong") - }, - }, - stageMeta: metav1.ObjectMeta{ - Name: "fake-name", - Namespace: "fake-namespace", - }, - update: kargoapi.ArgoCDAppUpdate{ - SourceUpdates: []kargoapi.ArgoCDSourceUpdate{ - {}, - }, - }, - assertions: func(t *testing.T, err error) { - require.ErrorContains(t, err, "error updating source(s) of Argo CD Application") - require.ErrorContains(t, err, "something went wrong") - }, - }, { name: "error patching Application", promoMech: &argoCDMechanism{ - getArgoCDAppFn: func( - context.Context, - string, - string, - ) (*argocd.Application, error) { - return &argocd.Application{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-name", - Namespace: "fake-namespace", - Annotations: map[string]string{ - authorizedStageAnnotationKey: "fake-namespace:fake-name", - }, - }, - }, nil - }, argoCDAppPatchFn: func( context.Context, client.Object, @@ -839,9 +1130,14 @@ func TestArgoCDDoSingleUpdate(t *testing.T) { return errors.New("something went wrong") }, }, - stageMeta: metav1.ObjectMeta{ - Name: "fake-name", - Namespace: "fake-namespace", + app: &argocd.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-name", + Namespace: "fake-namespace", + Annotations: map[string]string{ + authorizedStageAnnotationKey: "fake-namespace:fake-name", + }, + }, }, assertions: func(t *testing.T, err error) { require.ErrorContains(t, err, "error patching Argo CD Application") @@ -851,21 +1147,6 @@ func TestArgoCDDoSingleUpdate(t *testing.T) { { name: "success", promoMech: &argoCDMechanism{ - getArgoCDAppFn: func( - context.Context, - string, - string, - ) (*argocd.Application, error) { - return &argocd.Application{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-name", - Namespace: "fake-namespace", - Annotations: map[string]string{ - authorizedStageAnnotationKey: "fake-namespace:fake-name", - }, - }, - }, nil - }, argoCDAppPatchFn: func( context.Context, client.Object, @@ -876,9 +1157,14 @@ func TestArgoCDDoSingleUpdate(t *testing.T) { }, logAppEventFn: func(context.Context, *argocd.Application, string, string, string) {}, }, - stageMeta: metav1.ObjectMeta{ - Name: "fake-name", - Namespace: "fake-namespace", + app: &argocd.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-name", + Namespace: "fake-namespace", + Annotations: map[string]string{ + authorizedStageAnnotationKey: "fake-namespace:fake-name", + }, + }, }, assertions: func(t *testing.T, err error) { require.NoError(t, err) @@ -889,11 +1175,11 @@ func TestArgoCDDoSingleUpdate(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { testCase.assertions( t, - testCase.promoMech.doSingleUpdate( + testCase.promoMech.updateApplicationSources( context.Background(), - testCase.stageMeta, - testCase.update, - kargoapi.FreightReference{}, + testCase.app, + testCase.desiredSource, + testCase.desiredSources, ), ) }) @@ -987,6 +1273,130 @@ func TestLogAppEvent(t *testing.T) { } } +func TestArgoCDGetAuthorizedApplication(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, argocd.AddToScheme(scheme)) + + testCases := []struct { + name string + obj *argocd.Application + appName string + appNamespace string + interceptor interceptor.Funcs + stageMeta metav1.ObjectMeta + assertions func(*testing.T, *argocd.Application, error) + }{ + { + name: "error getting Application", + appNamespace: "fake-namespace", + appName: "fake-name", + interceptor: interceptor.Funcs{ + Get: func( + context.Context, + client.WithWatch, + client.ObjectKey, + client.Object, + ...client.GetOption, + ) error { + return errors.New("something went wrong") + }, + }, + assertions: func(t *testing.T, app *argocd.Application, err error) { + require.ErrorContains(t, err, "error finding Argo CD Application") + require.ErrorContains(t, err, "something went wrong") + require.Nil(t, app) + }, + }, + { + name: "Application not found", + appNamespace: "fake-namespace", + appName: "fake-name", + assertions: func(t *testing.T, app *argocd.Application, err error) { + require.ErrorContains(t, err, "unable to find Argo CD Application") + require.Nil(t, app) + }, + }, + { + name: "Application not authorized for Stage", + appNamespace: "fake-namespace", + appName: "fake-name", + obj: &argocd.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-name", + Namespace: "fake-namespace", + }, + }, + assertions: func(t *testing.T, app *argocd.Application, err error) { + require.ErrorContains(t, err, "does not permit mutation by Kargo Stage") + require.Nil(t, app) + }, + }, + { + name: "success", + appNamespace: "fake-namespace", + appName: "fake-name", + obj: &argocd.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-name", + Namespace: "fake-namespace", + Annotations: map[string]string{ + authorizedStageAnnotationKey: "fake-namespace:fake-stage", + }, + }, + }, + stageMeta: metav1.ObjectMeta{ + Name: "fake-stage", + Namespace: "fake-namespace", + }, + assertions: func(t *testing.T, app *argocd.Application, err error) { + require.NoError(t, err) + require.NotNil(t, app) + }, + }, + { + name: "success with default namespace", + appName: "fake-name", + obj: &argocd.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-name", + Namespace: libargocd.Namespace(), + Annotations: map[string]string{ + authorizedStageAnnotationKey: "*:fake-stage", + }, + }, + }, + stageMeta: metav1.ObjectMeta{ + Name: "fake-stage", + Namespace: "fake-namespace", + }, + assertions: func(t *testing.T, app *argocd.Application, err error) { + require.NoError(t, err) + require.NotNil(t, app) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithInterceptorFuncs(testCase.interceptor) + + if testCase.obj != nil { + c.WithObjects(testCase.obj) + } + + app, err := (&argoCDMechanism{argocdClient: c.Build()}).getAuthorizedApplication( + context.Background(), + testCase.appNamespace, + testCase.appName, + testCase.stageMeta, + ) + testCase.assertions(t, app, err) + }) + } +} + func TestAuthorizeArgoCDAppUpdate(t *testing.T) { permErr := "does not permit mutation" parseErr := "unable to parse"