-
Notifications
You must be signed in to change notification settings - Fork 160
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
base: main
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for docs-kargo-io ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
Codecov ReportAttention: Patch coverage is
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. |
242b4e6
to
ffc480b
Compare
f766300
to
62940e6
Compare
Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
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]>
Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
84869c9
to
05224bf
Compare
Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
internal/indexer/indexer.go
Outdated
@@ -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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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()
.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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:
- Promote to Stage API endpoint
- Promote to downstream Stages API endpoint
- 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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
Signed-off-by: Hidde Beydals <[email protected]>
774406c
to
7884c6e
Compare
// 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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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 | ||
} |
There was a problem hiding this comment.
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 }}
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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:
- task-level vars
- 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)) |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
Signed-off-by: Hidde Beydals <[email protected]>
0a9b704
to
2ca2524
Compare
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
exampleStage
exampleFull example for testing
https://gist.githubusercontent.com/hiddeco/df2da6a88ad1966642ed6ee94bad9787/raw/a886da867108c4ef970dec2b137e81f6a4b1f6fe/tasks.yaml