diff --git a/api/v1alpha1/generated.proto b/api/v1alpha1/generated.proto index 0d85e4ce7..e83123021 100644 --- a/api/v1alpha1/generated.proto +++ b/api/v1alpha1/generated.proto @@ -1469,6 +1469,9 @@ message StageList { // StageSpec describes the sources of Freight used by a Stage and how to // incorporate Freight into the Stage. +// +// +kubebuilder:validation:XValidation:rule="(has(self.promotionTemplate) || has(self.promotionMechanisms))",message="one of promotionTemplate or promotionMechanisms must be specified" +// +kubebuilder:validation:XValidation:rule="(has(self.promotionTemplate) && !has(self.promotionMechanisms)) || (!has(self.promotionTemplate) && has(self.promotionMechanisms))",message="only one of promotionTemplate or promotionMechanisms can be specified" message StageSpec { // Shard is the name of the shard that this Stage belongs to. This is an // optional field. If not specified, the Stage will belong to the default @@ -1499,6 +1502,8 @@ message StageSpec { // utility of this is to allow multiple downstream Stages to subscribe to a // single upstream Stage where they may otherwise have subscribed to multiple // upstream Stages. + // + // Deprecated: Use PromotionTemplate instead. optional PromotionMechanisms promotionMechanisms = 2; // Verification describes how to verify a Stage's current Freight is fit for diff --git a/api/v1alpha1/stage_types.go b/api/v1alpha1/stage_types.go index c3096c65a..1c7d33051 100644 --- a/api/v1alpha1/stage_types.go +++ b/api/v1alpha1/stage_types.go @@ -167,6 +167,9 @@ func (s *Stage) GetStatus() *StageStatus { // StageSpec describes the sources of Freight used by a Stage and how to // incorporate Freight into the Stage. +// +// +kubebuilder:validation:XValidation:rule="(has(self.promotionTemplate) || has(self.promotionMechanisms))",message="one of promotionTemplate or promotionMechanisms must be specified" +// +kubebuilder:validation:XValidation:rule="(has(self.promotionTemplate) && !has(self.promotionMechanisms)) || (!has(self.promotionTemplate) && has(self.promotionMechanisms))",message="only one of promotionTemplate or promotionMechanisms can be specified" type StageSpec struct { // Shard is the name of the shard that this Stage belongs to. This is an // optional field. If not specified, the Stage will belong to the default @@ -194,6 +197,8 @@ type StageSpec struct { // utility of this is to allow multiple downstream Stages to subscribe to a // single upstream Stage where they may otherwise have subscribed to multiple // upstream Stages. + // + // Deprecated: Use PromotionTemplate instead. PromotionMechanisms *PromotionMechanisms `json:"promotionMechanisms,omitempty" protobuf:"bytes,2,opt,name=promotionMechanisms"` // Verification describes how to verify a Stage's current Freight is fit for // promotion downstream. diff --git a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml index a53d11a24..e18aa4c59 100644 --- a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml +++ b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml @@ -65,6 +65,9 @@ spec: utility of this is to allow multiple downstream Stages to subscribe to a single upstream Stage where they may otherwise have subscribed to multiple upstream Stages. + + + Deprecated: Use PromotionTemplate instead. properties: argoCDAppUpdates: description: |- @@ -1051,6 +1054,13 @@ spec: required: - requestedFreight type: object + x-kubernetes-validations: + - message: one of promotionTemplate or promotionMechanisms must be specified + rule: (has(self.promotionTemplate) || has(self.promotionMechanisms)) + - message: only one of promotionTemplate or promotionMechanisms can be + specified + rule: (has(self.promotionTemplate) && !has(self.promotionMechanisms)) + || (!has(self.promotionTemplate) && has(self.promotionMechanisms)) status: description: Status describes the Stage's current and recent Freight, health, and more. diff --git a/internal/api/promote_downstream_v1alpha1.go b/internal/api/promote_downstream_v1alpha1.go index f35599973..1ab805b9f 100644 --- a/internal/api/promote_downstream_v1alpha1.go +++ b/internal/api/promote_downstream_v1alpha1.go @@ -134,7 +134,7 @@ func (s *server) PromoteDownstream( createdPromos := make([]*kargoapi.Promotion, 0, len(downstreams)) for _, downstream := range downstreams { newPromo := kargo.NewPromotion(ctx, downstream, freight.Name) - if downstream.Spec.PromotionMechanisms == nil { + if downstream.Spec.PromotionMechanisms == nil { // nolint: staticcheck // Avoid creating a Promotion if the downstream Stage has no // PromotionMechanisms, and is a "control flow" Stage. continue diff --git a/internal/argocd/health.go b/internal/argocd/health.go index 55e45ada8..bf8143f3e 100644 --- a/internal/argocd/health.go +++ b/internal/argocd/health.go @@ -57,6 +57,7 @@ func (h *applicationHealth) EvaluateHealth( ctx context.Context, stage *kargoapi.Stage, ) *kargoapi.Health { + // nolint: staticcheck if stage.Spec.PromotionMechanisms == nil || len(stage.Spec.PromotionMechanisms.ArgoCDAppUpdates) == 0 { return nil @@ -72,11 +73,15 @@ func (h *applicationHealth) EvaluateHealth( } health := kargoapi.Health{ - Status: kargoapi.HealthStateHealthy, - ArgoCDApps: make([]kargoapi.ArgoCDAppStatus, len(stage.Spec.PromotionMechanisms.ArgoCDAppUpdates)), - Issues: make([]string, 0), + Status: kargoapi.HealthStateHealthy, + ArgoCDApps: make( + []kargoapi.ArgoCDAppStatus, + len(stage.Spec.PromotionMechanisms.ArgoCDAppUpdates), // nolint: staticcheck + ), + Issues: make([]string, 0), } + // nolint: staticcheck for i := range stage.Spec.PromotionMechanisms.ArgoCDAppUpdates { update := &stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[i] namespace := update.AppNamespace diff --git a/internal/argocd/health_test.go b/internal/argocd/health_test.go index 1cbdb50c3..91dde0a56 100644 --- a/internal/argocd/health_test.go +++ b/internal/argocd/health_test.go @@ -607,7 +607,7 @@ func TestApplicationHealth_GetApplicationHealth(t *testing.T) { state, healthStatus, syncStatus, err := h.GetApplicationHealth( context.Background(), stage, - &stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0], + &stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0], // nolint: staticcheck client.ObjectKey{ Namespace: app.Namespace, Name: app.Name, @@ -657,7 +657,7 @@ func TestApplicationHealth_GetApplicationHealth(t *testing.T) { _, _, _, err := h.GetApplicationHealth( context.Background(), testStage, - &testStage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0], + &testStage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0], // nolint: staticcheck client.ObjectKey{ Namespace: "fake-namespace", Name: "fake-name", diff --git a/internal/argocd/revision_test.go b/internal/argocd/revision_test.go index 2ae4f9ad7..7afbb33ea 100644 --- a/internal/argocd/revision_test.go +++ b/internal/argocd/revision_test.go @@ -142,7 +142,7 @@ func TestGetDesiredRevisions(t *testing.T) { context.Background(), nil, // No client is needed as long as we're always explicit about origins stage, - &stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0], + &stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0], // nolint: staticcheck testCase.app, testCase.freight, ) diff --git a/internal/controller/freight/origins.go b/internal/controller/freight/origins.go index db285459f..849c4dd4b 100644 --- a/internal/controller/freight/origins.go +++ b/internal/controller/freight/origins.go @@ -38,7 +38,7 @@ func getDesiredOriginInternal( case *kargoapi.Stage: // Stage is not technically a promotion mechanism, but it is a convenient // entry point for the recursion. - subMechs = []any{m.Spec.PromotionMechanisms} + subMechs = []any{m.Spec.PromotionMechanisms} // nolint: staticcheck case *kargoapi.PromotionMechanisms: origin = m.Origin subMechs = make([]any, len(m.GitRepoUpdates)+len(m.ArgoCDAppUpdates)) diff --git a/internal/controller/freight/origins_test.go b/internal/controller/freight/origins_test.go index 68d015db1..0557ee839 100644 --- a/internal/controller/freight/origins_test.go +++ b/internal/controller/freight/origins_test.go @@ -334,6 +334,7 @@ func TestGetDesiredOrigin(t *testing.T) { }, }, } + // nolint: staticcheck return m, &m.Spec.PromotionMechanisms.GitRepoUpdates[0].Kustomize.Images[0] }, }, @@ -353,6 +354,7 @@ func TestGetDesiredOrigin(t *testing.T) { }, }, } + // nolint: staticcheck return m, &m.Spec.PromotionMechanisms.GitRepoUpdates[0].Kustomize.Images[0] }, }, diff --git a/internal/controller/promotion/argocd.go b/internal/controller/promotion/argocd.go index 20d2c6741..636c8475b 100644 --- a/internal/controller/promotion/argocd.go +++ b/internal/controller/promotion/argocd.go @@ -107,7 +107,7 @@ func (a *argoCDMechanism) Promote( stage *kargoapi.Stage, promo *kargoapi.Promotion, ) error { - updates := stage.Spec.PromotionMechanisms.ArgoCDAppUpdates + updates := stage.Spec.PromotionMechanisms.ArgoCDAppUpdates // nolint: staticcheck if len(updates) == 0 { promo.Status.Phase = kargoapi.PromotionPhaseSucceeded diff --git a/internal/controller/promotion/argocd_test.go b/internal/controller/promotion/argocd_test.go index 8cfcf3822..421aa6439 100644 --- a/internal/controller/promotion/argocd_test.go +++ b/internal/controller/promotion/argocd_test.go @@ -813,7 +813,7 @@ func TestArgoCDBuildDesiredSources(t *testing.T) { desiredSources, err := testCase.reconciler.buildDesiredSources( context.Background(), stage, - &stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0], + &stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0], // nolint: staticcheck app, []kargoapi.FreightReference{}, ) @@ -1164,7 +1164,7 @@ func TestArgoCDMustPerformUpdate(t *testing.T) { phase, mustUpdate, err := argocdMech.mustPerformUpdate( context.Background(), stage, - &stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0], + &stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0], // nolint: staticcheck app, freight, testCase.desiredSources, @@ -1841,7 +1841,7 @@ func TestApplyArgoCDSourceUpdate(t *testing.T) { updatedSource, err := mech.applyArgoCDSourceUpdate( context.Background(), stage, - &stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0].SourceUpdates[0], + &stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0].SourceUpdates[0], // nolint: staticcheck testCase.source, testCase.freight, ) @@ -1895,7 +1895,7 @@ func TestBuildKustomizeImagesForArgoCDAppSource(t *testing.T) { result, err := mech.buildKustomizeImagesForArgoCDAppSource( context.Background(), stage, - stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0].SourceUpdates[0].Kustomize, + stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0].SourceUpdates[0].Kustomize, // nolint: staticcheck freight, ) require.NoError(t, err) @@ -1984,7 +1984,7 @@ func TestBuildHelmParamChangesForArgoCDAppSource(t *testing.T) { result, err := mech.buildHelmParamChangesForArgoCDAppSource( context.Background(), stage, - stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0].SourceUpdates[0].Helm, + stage.Spec.PromotionMechanisms.ArgoCDAppUpdates[0].SourceUpdates[0].Helm, // nolint: staticcheck freight, ) require.NoError(t, err) diff --git a/internal/controller/promotion/composite.go b/internal/controller/promotion/composite.go index 0307e15a0..bba86facd 100644 --- a/internal/controller/promotion/composite.go +++ b/internal/controller/promotion/composite.go @@ -44,7 +44,7 @@ func (c *compositeMechanism) Promote( stage *kargoapi.Stage, promo *kargoapi.Promotion, ) error { - if stage.Spec.PromotionMechanisms == nil { + if stage.Spec.PromotionMechanisms == nil { // nolint: staticcheck promo.Status.Phase = kargoapi.PromotionPhaseSucceeded return nil } diff --git a/internal/controller/promotion/git.go b/internal/controller/promotion/git.go index a6d882c67..4c42b17cd 100644 --- a/internal/controller/promotion/git.go +++ b/internal/controller/promotion/git.go @@ -128,6 +128,7 @@ func (g *gitMechanism) Promote( stage *kargoapi.Stage, promo *kargoapi.Promotion, ) error { + // nolint: staticcheck updates := g.selectUpdatesFn(stage.Spec.PromotionMechanisms.GitRepoUpdates) if len(updates) == 0 { diff --git a/internal/controller/promotion/git_test.go b/internal/controller/promotion/git_test.go index caaab946a..654e3de7f 100644 --- a/internal/controller/promotion/git_test.go +++ b/internal/controller/promotion/git_test.go @@ -478,7 +478,7 @@ func TestGetReadRef(t *testing.T) { context.Background(), fake.NewFakeClient(), stage, - &stage.Spec.PromotionMechanisms.GitRepoUpdates[0], + &stage.Spec.PromotionMechanisms.GitRepoUpdates[0], // nolint: staticcheck testCase.freight, ) testCase.assertions(t, readBranch, commit, err) diff --git a/internal/controller/promotion/helm_test.go b/internal/controller/promotion/helm_test.go index 74a548f0a..ef438998e 100644 --- a/internal/controller/promotion/helm_test.go +++ b/internal/controller/promotion/helm_test.go @@ -309,6 +309,7 @@ func TestHelmerApply(t *testing.T) { }, }, } + // nolint: staticcheck changes, err := testCase.helmer.apply( context.Background(), stage, @@ -376,7 +377,7 @@ func TestBuildValuesFilesChanges(t *testing.T) { result, changeSummary, err := h.buildValuesFilesChanges( context.Background(), stage, - stage.Spec.PromotionMechanisms.GitRepoUpdates[0].Helm, + stage.Spec.PromotionMechanisms.GitRepoUpdates[0].Helm, // nolint: staticcheck []kargoapi.FreightReference{{ Origin: testOrigin, Images: []kargoapi.Image{ @@ -525,7 +526,7 @@ func TestBuildChartDependencyChanges(t *testing.T) { result, changeSummary, err := h.buildChartDependencyChanges( context.Background(), stage, - stage.Spec.PromotionMechanisms.GitRepoUpdates[0].Helm, + stage.Spec.PromotionMechanisms.GitRepoUpdates[0].Helm, // nolint: staticcheck freight, testDir, ) diff --git a/internal/controller/promotion/kustomize_test.go b/internal/controller/promotion/kustomize_test.go index f6b4cb0a7..de529f4e7 100644 --- a/internal/controller/promotion/kustomize_test.go +++ b/internal/controller/promotion/kustomize_test.go @@ -239,7 +239,7 @@ func TestKustomizerApply(t *testing.T) { changes, err := testCase.kustomizer.apply( context.Background(), stage, - &stage.Spec.PromotionMechanisms.GitRepoUpdates[0], + &stage.Spec.PromotionMechanisms.GitRepoUpdates[0], // nolint: staticcheck []kargoapi.FreightReference{{ Images: []kargoapi.Image{ { diff --git a/internal/controller/promotion/render_test.go b/internal/controller/promotion/render_test.go index 7929b72cc..19404d94a 100644 --- a/internal/controller/promotion/render_test.go +++ b/internal/controller/promotion/render_test.go @@ -239,7 +239,7 @@ func TestKargoRenderApply(t *testing.T) { changes, err := testCase.renderer.apply( context.Background(), stage, - &stage.Spec.PromotionMechanisms.GitRepoUpdates[0], + &stage.Spec.PromotionMechanisms.GitRepoUpdates[0], // nolint: staticcheck testCase.newFreight, testSourceCommitID, "", // Home directory is not used by this implementation diff --git a/internal/kubeclient/indexer.go b/internal/kubeclient/indexer.go index 618480b41..31c30bbbf 100644 --- a/internal/kubeclient/indexer.go +++ b/internal/kubeclient/indexer.go @@ -155,10 +155,13 @@ func indexStagesByArgoCDApplications(shardName string) client.IndexerFunc { } stage := obj.(*kargoapi.Stage) // nolint: forcetypeassert + // nolint: staticcheck if stage.Spec.PromotionMechanisms == nil || len(stage.Spec.PromotionMechanisms.ArgoCDAppUpdates) == 0 { return nil } + // nolint: staticcheck apps := make([]string, len(stage.Spec.PromotionMechanisms.ArgoCDAppUpdates)) + // nolint: staticcheck for i, appCheck := range stage.Spec.PromotionMechanisms.ArgoCDAppUpdates { namespace := appCheck.AppNamespace if namespace == "" { @@ -280,13 +283,16 @@ func indexRunningPromotionsByArgoCDApplications( return nil } + // nolint: staticcheck if stage.Spec.PromotionMechanisms == nil || len(stage.Spec.PromotionMechanisms.ArgoCDAppUpdates) == 0 { // If the Stage has no Argo CD Application promotion mechanisms, // then we have nothing to index. return nil } + // nolint: staticcheck res := make([]string, len(stage.Spec.PromotionMechanisms.ArgoCDAppUpdates)) + // nolint: staticcheck for i, appUpdate := range stage.Spec.PromotionMechanisms.ArgoCDAppUpdates { namespace := appUpdate.AppNamespace if namespace == "" { diff --git a/internal/webhook/promotion/webhook.go b/internal/webhook/promotion/webhook.go index e8efaa0ea..a223550e0 100644 --- a/internal/webhook/promotion/webhook.go +++ b/internal/webhook/promotion/webhook.go @@ -178,6 +178,7 @@ func (w *webhook) Default(ctx context.Context, obj runtime.Object) error { promo.Namespace, ) } + // nolint: staticcheck if len(promo.Spec.Steps) == 0 && stage.Spec.PromotionMechanisms == nil { return fmt.Errorf( "Stage %q in namespace %q has no PromotionMechanisms", diff --git a/internal/webhook/stage/webhook.go b/internal/webhook/stage/webhook.go index 2cd2b3423..810f31d4f 100644 --- a/internal/webhook/stage/webhook.go +++ b/internal/webhook/stage/webhook.go @@ -199,7 +199,7 @@ func (w *webhook) validateSpec( errs, w.validatePromotionMechanisms( f.Child("promotionMechanisms"), - spec.PromotionMechanisms, + spec.PromotionMechanisms, // nolint: staticcheck )..., ) } diff --git a/internal/webhook/stage/webhook_test.go b/internal/webhook/stage/webhook_test.go index e1f2bd60f..71113cc0b 100644 --- a/internal/webhook/stage/webhook_test.go +++ b/internal/webhook/stage/webhook_test.go @@ -1053,7 +1053,7 @@ func TestValidateSpec(t *testing.T) { { Type: field.ErrorTypeInvalid, Field: "spec.promotionMechanisms", - BadValue: spec.PromotionMechanisms, + BadValue: spec.PromotionMechanisms, // nolint: staticcheck Detail: "at least one of " + "spec.promotionMechanisms.gitRepoUpdates or " + "spec.promotionMechanisms.argoCDAppUpdates must be non-empty", diff --git a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json index f10bd803a..0046f0a0b 100644 --- a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json +++ b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json @@ -17,7 +17,7 @@ "description": "Spec describes sources of Freight used by the Stage and how to incorporate\nFreight into the Stage.", "properties": { "promotionMechanisms": { - "description": "PromotionMechanisms describes how to incorporate Freight into the Stage.\nThis is an optional field as it is sometimes useful to aggregates available\nFreight from multiple upstream Stages without performing any actions. The\nutility of this is to allow multiple downstream Stages to subscribe to a\nsingle upstream Stage where they may otherwise have subscribed to multiple\nupstream Stages.", + "description": "PromotionMechanisms describes how to incorporate Freight into the Stage.\nThis is an optional field as it is sometimes useful to aggregates available\nFreight from multiple upstream Stages without performing any actions. The\nutility of this is to allow multiple downstream Stages to subscribe to a\nsingle upstream Stage where they may otherwise have subscribed to multiple\nupstream Stages.\n\n\nDeprecated: Use PromotionTemplate instead.", "properties": { "argoCDAppUpdates": { "description": "ArgoCDAppUpdates describes updates that should be applied to Argo CD\nApplication resources to incorporate Freight into the Stage. This field is\noptional, as such actions are not required in all cases. Note that all\nupdates specified by the GitRepoUpdates field, if any, are applied BEFORE\nthese.", @@ -832,7 +832,17 @@ "required": [ "requestedFreight" ], - "type": "object" + "type": "object", + "x-kubernetes-validations": [ + { + "message": "one of promotionTemplate or promotionMechanisms must be specified", + "rule": "(has(self.promotionTemplate) || has(self.promotionMechanisms))" + }, + { + "message": "only one of promotionTemplate or promotionMechanisms can be specified", + "rule": "(has(self.promotionTemplate) && !has(self.promotionMechanisms)) || (!has(self.promotionTemplate) && has(self.promotionMechanisms))" + } + ] }, "status": { "description": "Status describes the Stage's current and recent Freight, health, and more.", diff --git a/ui/src/gen/v1alpha1/generated_pb.ts b/ui/src/gen/v1alpha1/generated_pb.ts index 80b942917..c66563a7a 100644 --- a/ui/src/gen/v1alpha1/generated_pb.ts +++ b/ui/src/gen/v1alpha1/generated_pb.ts @@ -4439,6 +4439,9 @@ export class StageList extends Message { * StageSpec describes the sources of Freight used by a Stage and how to * incorporate Freight into the Stage. * + * +kubebuilder:validation:XValidation:rule="(has(self.promotionTemplate) || has(self.promotionMechanisms))",message="one of promotionTemplate or promotionMechanisms must be specified" + * +kubebuilder:validation:XValidation:rule="(has(self.promotionTemplate) && !has(self.promotionMechanisms)) || (!has(self.promotionTemplate) && has(self.promotionMechanisms))",message="only one of promotionTemplate or promotionMechanisms can be specified" + * * @generated from message github.com.akuity.kargo.api.v1alpha1.StageSpec */ export class StageSpec extends Message { @@ -4485,6 +4488,8 @@ export class StageSpec extends Message { * single upstream Stage where they may otherwise have subscribed to multiple * upstream Stages. * + * Deprecated: Use PromotionTemplate instead. + * * @generated from field: optional github.com.akuity.kargo.api.v1alpha1.PromotionMechanisms promotionMechanisms = 2; */ promotionMechanisms?: PromotionMechanisms;