diff --git a/api/v1alpha1/stage_types.go b/api/v1alpha1/stage_types.go index 9f5343519b..d2912f045f 100644 --- a/api/v1alpha1/stage_types.go +++ b/api/v1alpha1/stage_types.go @@ -154,8 +154,12 @@ type Stage struct { // orchestrates the promotion of Freight from one or more upstream Stages to // one or more downstream Stages. func (s *Stage) IsControlFlow() bool { + if s == nil { + return false + } + switch { - case s.Spec.PromotionTemplateRef != nil && len(s.Spec.PromotionTemplate.Name) > 0: + case s.Spec.PromotionTemplateRef != nil && len(s.Spec.PromotionTemplateRef.Name) > 0: return false case s.Spec.PromotionTemplate != nil && len(s.Spec.PromotionTemplate.Spec.Steps) > 0: return false diff --git a/internal/api/promote_downstream_v1alpha1.go b/internal/api/promote_downstream_v1alpha1.go index 7b745df629..cedc05f27b 100644 --- a/internal/api/promote_downstream_v1alpha1.go +++ b/internal/api/promote_downstream_v1alpha1.go @@ -154,7 +154,7 @@ func (s *server) PromoteDownstream( var ok bool if template, ok = templates[downstream.Spec.PromotionTemplateRef.Name]; !ok { template = &kargoapi.PromotionTemplate{} - if err = s.client.Get(ctx, types.NamespacedName{ + if err = s.getPromotionTemplateFn(ctx, types.NamespacedName{ Namespace: downstream.Namespace, Name: downstream.Spec.PromotionTemplateRef.Name, }, template); err != nil { diff --git a/internal/api/promote_downstream_v1alpha1_test.go b/internal/api/promote_downstream_v1alpha1_test.go index 1c30b91fc2..893ce68f11 100644 --- a/internal/api/promote_downstream_v1alpha1_test.go +++ b/internal/api/promote_downstream_v1alpha1_test.go @@ -442,6 +442,93 @@ func TestPromoteDownstream(t *testing.T) { require.Equal(t, "not authorized", err.Error()) }, }, + { + name: "PromotionTemplate reference not found", + req: &svcv1alpha1.PromoteDownstreamRequest{ + Project: "fake-project", + Stage: "fake-stage", + Freight: "fake-freight", + }, + server: &server{ + validateProjectExistsFn: func(context.Context, string) error { + return nil + }, + getStageFn: func( + context.Context, + client.Client, + types.NamespacedName, + ) (*kargoapi.Stage, error) { + return &kargoapi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "fake-project", + Name: "fake-stage", + }, + Spec: testStageSpec, + }, nil + }, + getFreightByNameOrAliasFn: func( + context.Context, + client.Client, + string, string, string, + ) (*kargoapi.Freight, error) { + return &kargoapi.Freight{ + Status: kargoapi.FreightStatus{ + VerifiedIn: map[string]kargoapi.VerifiedStage{ + "fake-stage": {}, + }, + }, + }, nil + }, + getPromotionTemplateFn: func( + context.Context, + client.ObjectKey, + client.Object, + ...client.GetOption, + ) error { + return errors.New("not found") + }, + findDownstreamStagesFn: func( + context.Context, + *kargoapi.Stage, + kargoapi.FreightOrigin, + ) ([]kargoapi.Stage, error) { + return []kargoapi.Stage{ + { + Spec: kargoapi.StageSpec{ + PromotionTemplateRef: &kargoapi.PromotionTemplateReference{ + Name: "fake-promotion-template", + }, + }, + }, + }, nil + }, + authorizeFn: func( + context.Context, + string, + schema.GroupVersionResource, + string, + client.ObjectKey, + ) error { + return nil + }, + createPromotionFn: func( + context.Context, + client.Object, + ...client.CreateOption, + ) error { + return nil + }, + }, + assertions: func( + t *testing.T, + _ *fakeevent.EventRecorder, + _ *connect.Response[svcv1alpha1.PromoteDownstreamResponse], + err error, + ) { + require.ErrorContains(t, err, "get PromotionTemplate") + require.ErrorContains(t, err, "not found") + }, + }, { name: "error creating Promotion", req: &svcv1alpha1.PromoteDownstreamRequest{ diff --git a/internal/api/promote_to_stage_v1alpha1.go b/internal/api/promote_to_stage_v1alpha1.go index 6522309b39..341f763e6e 100644 --- a/internal/api/promote_to_stage_v1alpha1.go +++ b/internal/api/promote_to_stage_v1alpha1.go @@ -125,7 +125,7 @@ func (s *server) PromoteToStage( } template = &kargoapi.PromotionTemplate{} - if err = s.client.Get(ctx, types.NamespacedName{ + if err = s.getPromotionTemplateFn(ctx, types.NamespacedName{ Namespace: project, Name: stage.Spec.PromotionTemplateRef.Name, }, template); err != nil { diff --git a/internal/api/promote_to_stage_v1alpha1_test.go b/internal/api/promote_to_stage_v1alpha1_test.go index c25b50c194..81bc507dfc 100644 --- a/internal/api/promote_to_stage_v1alpha1_test.go +++ b/internal/api/promote_to_stage_v1alpha1_test.go @@ -81,8 +81,7 @@ func TestPromoteToStage(t *testing.T) { _ *connect.Response[svcv1alpha1.PromoteToStageResponse], err error, ) { - require.Error(t, err) - require.Equal(t, "something went wrong", err.Error()) + require.ErrorContains(t, err, "something went wrong") }, }, { @@ -110,8 +109,7 @@ func TestPromoteToStage(t *testing.T) { _ *connect.Response[svcv1alpha1.PromoteToStageResponse], err error, ) { - require.Error(t, err) - require.Equal(t, "get stage: something went wrong", err.Error()) + require.ErrorContains(t, err, "get stage: something went wrong") }, }, { @@ -181,8 +179,7 @@ func TestPromoteToStage(t *testing.T) { _ *connect.Response[svcv1alpha1.PromoteToStageResponse], err error, ) { - require.Error(t, err) - require.Equal(t, "get freight: something went wrong", err.Error()) + require.ErrorContains(t, err, "get freight: something went wrong") }, }, { @@ -320,8 +317,77 @@ func TestPromoteToStage(t *testing.T) { _ *connect.Response[svcv1alpha1.PromoteToStageResponse], err error, ) { - require.Error(t, err) - require.Equal(t, "not authorized", err.Error()) + require.ErrorContains(t, err, "not authorized") + }, + }, + { + name: "PromotionTemplate not found", + req: &svcv1alpha1.PromoteToStageRequest{ + Project: "fake-project", + Stage: "fake-stage", + Freight: "fake-freight", + }, + server: &server{ + validateProjectExistsFn: func(context.Context, string) error { + return nil + }, + getStageFn: func( + context.Context, + client.Client, + types.NamespacedName, + ) (*kargoapi.Stage, error) { + return &kargoapi.Stage{ + Spec: kargoapi.StageSpec{ + PromotionTemplateRef: &kargoapi.PromotionTemplateReference{ + Name: "fake-promotion-template", + }, + RequestedFreight: testStageSpec.RequestedFreight, + }, + }, nil + }, + getPromotionTemplateFn: func( + context.Context, + client.ObjectKey, + client.Object, + ...client.GetOption, + ) error { + return errors.New("not found") + }, + getFreightByNameOrAliasFn: func( + context.Context, + client.Client, + string, string, string, + ) (*kargoapi.Freight, error) { + return &kargoapi.Freight{}, nil + }, + isFreightAvailableFn: func(*kargoapi.Stage, *kargoapi.Freight) bool { + return true + }, + authorizeFn: func( + context.Context, + string, + schema.GroupVersionResource, + string, + client.ObjectKey, + ) error { + return nil + }, + createPromotionFn: func( + context.Context, + client.Object, + ...client.CreateOption, + ) error { + return nil + }, + }, + assertions: func( + t *testing.T, + _ *fakeevent.EventRecorder, + _ *connect.Response[svcv1alpha1.PromoteToStageResponse], + err error, + ) { + require.ErrorContains(t, err, "get PromotionTemplate") + require.ErrorContains(t, err, "not found") }, }, { @@ -377,8 +443,7 @@ func TestPromoteToStage(t *testing.T) { _ *connect.Response[svcv1alpha1.PromoteToStageResponse], err error, ) { - require.Error(t, err) - require.Equal(t, "create promotion: something went wrong", err.Error()) + require.ErrorContains(t, err, "create promotion: something went wrong") }, }, { diff --git a/internal/api/server.go b/internal/api/server.go index 0a53283f23..3d9a1d5bfa 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -69,6 +69,12 @@ type server struct { client.Client, types.NamespacedName, ) (*kargoapi.Stage, error) + getPromotionTemplateFn func( + context.Context, + client.ObjectKey, + client.Object, + ...client.GetOption, + ) error getFreightByNameOrAliasFn func( ctx context.Context, c client.Client, @@ -175,6 +181,7 @@ func NewServer( s.getFreightByNameOrAliasFn = kargoapi.GetFreightByNameOrAlias s.isFreightAvailableFn = kargoapi.IsFreightAvailable s.createPromotionFn = kubeClient.Create + s.getPromotionTemplateFn = kubeClient.Get s.findDownstreamStagesFn = s.findDownstreamStages s.listFreightFn = kubeClient.List s.getAvailableFreightForStageFn = s.getAvailableFreightForStage