Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: (cluster) Promotion tasks #3121

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open

Conversation

hiddeco
Copy link
Contributor

@hiddeco hiddeco commented Dec 11, 2024

This pull request introduces the PromotionTask and ClusterPromotionTask resources to Kargo. These new resource types allow you to streamline Stage resources by bundling commonly shared promotion steps into reusable tasks, which can be referenced in promotion templates.

PromotionTask example

---
apiVersion: kargo.akuity.io/v1alpha1
kind: PromotionTask
metadata:
  name: open-pr-and-wait
  namespace: my-project
spec:
  vars:
  - name: repoURL
  - name: sourceBranch
  - name: targetBranch
    value: main
  steps:
  - uses: git-open-pr
    as: open-pr
    config:
      repoURL: ${{ vars.repoURL }}
      createTargetBranch: true
      sourceBranch: ${{ vars.sourceBranch }}
      targetBranch: ${{ vars.targetBranch }}
  - uses: git-wait-for-pr
    as: wait-for-pr
    config:
      repoURL: ${{ vars.repoURL }}
      prNumber: ${{ task.outputs['open-pr'].prNumber }}
  - uses: compose-output
    as: output
    config:
      mergeCommit: ${{ task.outputs['wait-for-pr'].commit }}

Stage example

---
apiVersion: kargo.akuity.io/v1alpha1
kind: Stage
metadata:
  name: staging
  namespace: my-project
spec:
  requestedFreight:
  - origin:
      kind: Warehouse
      name: kargo-demo
    sources:
      direct: true
  promotionTemplate:
    spec:
      vars:
      - name: repoURL
        value: https://github.com/example/repository.git
      - name: srcPath
        value: ./src
      - name: outPath
        value: ./out
      - name: targetBranch
        value: stage/${{ ctx.stage }}
      steps:
      # ...omitted for brevity
      - uses: git-push
        as: push
        config:
          path: ${{ vars.outPath }}
          generateTargetBranch: true
      - task:
          name: open-pr-and-wait
          kind: ClusterPromotionTask
        as: pr
        vars:
        - name: sourceBranch
          value: ${{ outputs.push.branch }}

Full example for testing

https://gist.githubusercontent.com/hiddeco/df2da6a88ad1966642ed6ee94bad9787/raw/a886da867108c4ef970dec2b137e81f6a4b1f6fe/tasks.yaml

Copy link

netlify bot commented Dec 11, 2024

Deploy Preview for docs-kargo-io ready!

Name Link
🔨 Latest commit 2ca2524
🔍 Latest deploy log https://app.netlify.com/sites/docs-kargo-io/deploys/6781133e5754630008f417fd
😎 Deploy Preview https://deploy-preview-3121.docs.kargo.io
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@hiddeco hiddeco added this to the v1.2.0 milestone Dec 11, 2024
@hiddeco hiddeco self-assigned this Dec 11, 2024
Copy link

codecov bot commented Dec 11, 2024

Codecov Report

Attention: Patch coverage is 76.84887% with 72 lines in your changes missing coverage. Please review.

Project coverage is 51.49%. Comparing base (db17a2a) to head (8a79c51).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/directives/promotions.go 66.15% 18 Missing and 4 partials ⚠️
internal/directives/output_composer.go 33.33% 16 Missing ⚠️
internal/webhook/promotiontask/webhook.go 33.33% 12 Missing ⚠️
api/v1alpha1/promotion_types.go 0.00% 8 Missing ⚠️
internal/directives/simple_engine_promote.go 53.33% 6 Missing and 1 partial ⚠️
cmd/controlplane/webhooks.go 0.00% 3 Missing ⚠️
internal/kargo/promotion_builder.go 97.95% 2 Missing and 1 partial ⚠️
internal/controller/promotions/promotions.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3121      +/-   ##
==========================================
+ Coverage   51.27%   51.49%   +0.21%     
==========================================
  Files         285      288       +3     
  Lines       25706    25944     +238     
==========================================
+ Hits        13182    13360     +178     
- Misses      11824    11881      +57     
- Partials      700      703       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
- `PromotionTemplate` steps can contain either a reference to `uses` OR
  to a `task`.
- `PromotionTask` steps can not contain a reference to another `task`,
  but must contain a `uses` reference.
- `Promotion` steps can not contain a reference to a `task` (as we
  expect a `Promotion` to contain deflated tasks), but must contain
  a `uses` reference.
- Small other tweaks, to e.g. ensure we limit the max lenght of fields
  that contain name references to Kubernetes objects.

Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
@hiddeco hiddeco marked this pull request as ready for review December 18, 2024 23:31
@hiddeco hiddeco requested a review from a team as a code owner December 18, 2024 23:31
@@ -168,7 +168,7 @@ func RunningPromotionsByArgoCDApplications(
Promotion: promo.Name,
Vars: promo.Spec.Vars,
}
vars, err := dirStep.GetVars(promoCtx)
vars, err := dirStep.GetVars(promoCtx, promoCtx.State)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we only built out enough of promoCtx to get us through this, promotCtx.State will be empty. I suppose that's actually ok in this (pardon the overload) context?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think so but it may have been better to use an explicit nil here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicit nil sgtm, maybe with a brief comment about why that arg is unimportant in this context?

Really this is a nit... so however you want to deal with it is fine, even if it's do nothing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon closer inspection, I do think we need to pass some state here. In addition, I have also a vague suspicion we may actually need the outputs as well while evaluating the individual fields because theoretically, the values can not just originate from variables.

@@ -0,0 +1,264 @@
package kargo
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't introduced by this PR, but this internal/kargo package has been bugging me for a while because I don't feel like the package has a clearly defined purpose. (Maybe it does, and I'm failing to see it.)

If I'm not wrong, there's certainly no need to untangle this knot to get this PR merged, but (again, only if I'm right), I wonder if there's any other place that might be appropriate for the new functionality added to the package, in particular NewPromotionBuilder().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only alternative I can think of is inventing a new promotion package, but I do not really like this alternative either...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can put a pin in it. Fortunately there are only a couple packages like this that seem to have collected unrelated bits of functionality over time for lack of a better home.

if downstream.Spec.PromotionTemplate != nil &&
len(downstream.Spec.PromotionTemplate.Spec.Steps) == 0 {
// Avoid creating a Promotion if the downstream Stage has no promotion
// steps and is therefore a "control flow" Stage.
continue
}
if err := s.createPromotionFn(ctx, &newPromo); err != nil {
newPromo, err := kargo.NewPromotionBuilder(s.client).Build(ctx, downstream, freight.Name)
Copy link
Member

@krancour krancour Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a new problem, but the PR's making me more aware of it...

It seems that now, as before, we build out a Promo's steps from the template in three places:

  1. Promote to Stage API endpoint
  2. Promote to downstream Stages API endpoint
  3. Auto-Promotion by the Stage reconciler

In all cases, a Promo's steps are defined when it's created.

I'll avoid the question of whether it's better to copy/expand steps when the Promo is created vs being done by the Promo reconciler when the Promo's started (because I could see it both ways and we may have to confront this eventually), but the weird side effect of the Promo reconciler not doing it is that you can't create a Promo with kubectl unless you defined all the steps yourself. There's no way to say: "This Stage, this piece of Freight, and let the controller fill in the steps."

Should we at least have the Promo reconciler build/expand steps just-in-time if a work on a Promo starts and its steps are empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an excellent point, but I wonder if instead of doing this — wouldn't it be better to introduce a modifying webhook?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I like that idea best, actually. In this case, we could drop the kargo.NewPromotionBuilder(s.client).Build() from three locations it's in now and just let the mutating webhook handle for all cases? That sounds like a win.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would still need some sort of builder, I think. But the inflation would happen as a secondary step in a single place.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh... I didn't mean drop the builder. Just drop three calls to it in favor of one.

// InflateSteps inflates the Promotion steps by resolving any references to
// PromotionTasks and expanding them into their individual steps. The inflated
// steps are then set on the Promotion, replacing the original steps.
func (b *PromotionBuilder) InflateSteps(ctx context.Context, promo *kargoapi.Promotion) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making sure I understand the rational for having InflateSteps() separate from Build()...

I presume that by not having Build() automatically inflate, we are preserving the possibility of a Promo having been defined manually and submitted via kubectl, in which case, building isn't necessary, but expansion is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build is for creating a Promotion based on a Stage (or rather, the Promotion template within). Which we do in several places, either through the API or CLI.

InflateSteps is what always happens on the server-side, and ensures that even if a Promotion would be created through kubectl (as you suggest) — we still handle the core logic of correctly inflating any references we find into actual steps.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok... so I agree with the rationale for keeping them separate, but is there any reason we inflate server side, but we don't build server side?

Could the mutating webhook we discussed build if steps == nil and then inflate unconditionally? As opposed to calling build in several different places?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this offline, and partly because of debugging reasons and introducing too many sprinkles of magic. It is better to keep the builder in place so you have some (meaningful) YAML to hold on to.

Comment on lines +228 to +236
promoVarsMap := make(map[string]kargoapi.PromotionVariable, len(promoVars))
for _, v := range promoVars {
promoVarsMap[v.Name] = v
}

stepVarsMap := make(map[string]kargoapi.PromotionVariable, len(stepVars))
for _, v := range stepVars {
stepVarsMap[v.Name] = v
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any edge case here where we lose anything important through the use of maps?

I'm thinking something like this, which seems dumb but possible, might be such a case?

vars:
- name: foo
  value: bar
- name: foo
  value: ${{ vars.foo + vars.foo }}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second would clobber the first in the map, but the result would be an error, I think, instead of "barbar".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still an issue, the plan is to inflate them as:

  1. task-level vars
  2. task-step-level vars

Which would allow the task-step-level to overwrite (or utilize) the task-level variable.

The only concern I have with this approach is that the promotion-level variables then can not overwrite the task-level (default) variables, because they are evaluated before any of the variables in the above list (and a task-level variable with the same name would thus overwrite this value).

promoCtx PromotionContext,
state State,
) (map[string]any, error) {
var rawVars = make(map[string]string, len(promoCtx.Vars))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question about maps as here.

// +kubebuilder:object:root=true
// +kubebuilder:printcolumn:name=Age,type=date,JSONPath=`.metadata.creationTimestamp`

type ClusterPromotionTask struct {
Copy link
Member

@jessesuen jessesuen Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I know ClusterPromotionTask is technically accurate, I wonder if we want to term this "GlobalPromotionTask". We already refer to credentials and ServiceAccounts that are to be used in a global capacity as "global" credentials and global serviceaccounts, and this would be along those lines.

The "cluster" is less meaningful since kargo is not really used in a workload cluster.

@krancour krancour mentioned this pull request Dec 31, 2024
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants