@@ -2,10 +2,12 @@ package api
22
33import (
44 "context"
5+ "errors"
56 "fmt"
67 "strings"
78 "time"
89
10+ "github.com/oklog/ulid/v2"
911 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1012 "k8s.io/apimachinery/pkg/types"
1113 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -14,6 +16,77 @@ import (
1416 "github.com/akuity/kargo/pkg/server/user"
1517)
1618
19+ // PromotionAliasSeparator is the separator used in the alias of an inflated
20+ // PromotionTask step to separate the task alias from the step alias.
21+ const PromotionAliasSeparator = "::"
22+
23+ const (
24+ // promotionNameSeparator separates components of a Promotion name.
25+ promotionNameSeparator = "."
26+
27+ // promotionShortHashLength is the length of the short Freight hash
28+ // embedded in a generated Promotion name.
29+ promotionShortHashLength = 7
30+
31+ // maxStageNamePrefixForPromotionName is the maximum length of the Stage
32+ // name used as the prefix of a generated Promotion name before it would
33+ // exceed the Kubernetes resource name limit of 253.
34+ maxStageNamePrefixForPromotionName = 253 -
35+ len (promotionNameSeparator ) - ulid .EncodedSize -
36+ len (promotionNameSeparator ) - promotionShortHashLength
37+ )
38+
39+ // NewMinimalPromotion constructs a Promotion containing only the fields that
40+ // callers (API server endpoints, the Stage controller's auto-promote loop) are
41+ // responsible for setting. The Promotion defaulting webhook fills in the rest:
42+ // name, steps copied from the Stage's PromotionTemplate, etc.
43+ func NewMinimalPromotion (
44+ stage * kargoapi.Stage ,
45+ freightName string ,
46+ ) * kargoapi.Promotion {
47+ return & kargoapi.Promotion {
48+ ObjectMeta : metav1.ObjectMeta {
49+ Namespace : stage .Namespace ,
50+ // The defaulting webhook overwrites this. We set it here only so that the
51+ // Kubernetes API server has a name to work with before admission runs.
52+ GenerateName : "promo-" ,
53+ },
54+ Spec : kargoapi.PromotionSpec {
55+ Stage : stage .Name ,
56+ Freight : freightName ,
57+ },
58+ }
59+ }
60+
61+ // GeneratePromotionName generates a name for a Promotion by combining the
62+ // Stage name, a ULID, and a short hash of the Freight.
63+ //
64+ // The name has the format of:
65+ //
66+ // <stage-name>.<ulid>.<short-hash>
67+ //
68+ // Promotion sorting and comparison logic elsewhere in Kargo relies on names
69+ // in this format -- the embedded ULID makes lex order match creation order.
70+ // Callers that need a Promotion name should always use this function.
71+ func GeneratePromotionName (stageName , freight string ) string {
72+ if stageName == "" || freight == "" {
73+ return ""
74+ }
75+
76+ shortHash := freight
77+ if len (shortHash ) > promotionShortHashLength {
78+ shortHash = shortHash [0 :promotionShortHashLength ]
79+ }
80+
81+ shortStageName := stageName
82+ if len (stageName ) > maxStageNamePrefixForPromotionName {
83+ shortStageName = shortStageName [0 :maxStageNamePrefixForPromotionName ]
84+ }
85+
86+ parts := []string {shortStageName , ulid .Make ().String (), shortHash }
87+ return strings .ToLower (strings .Join (parts , promotionNameSeparator ))
88+ }
89+
1790// GetPromotion returns a pointer to the Promotion resource specified by the
1891// namespacedName argument. If no such resource is found, nil is returned
1992// instead.
@@ -164,3 +237,175 @@ func IsCurrentStepRunning(promo *kargoapi.Promotion) bool {
164237 int64 (len (promo .Status .StepExecutionMetadata )) == promo .Status .CurrentStep + 1 &&
165238 promo .Status .StepExecutionMetadata [promo .Status .CurrentStep ].Status == kargoapi .PromotionStepStatusRunning
166239}
240+
241+ // InflateSteps inflates the given Promotion's steps in place by resolving any
242+ // references to (Cluster)PromotionTasks and expanding them into their
243+ // individual steps.
244+ func InflateSteps (
245+ ctx context.Context ,
246+ c client.Client ,
247+ promo * kargoapi.Promotion ,
248+ ) error {
249+ steps := make ([]kargoapi.PromotionStep , 0 , len (promo .Spec .Steps ))
250+ for i , step := range promo .Spec .Steps {
251+ switch {
252+ case step .Task != nil :
253+ alias := step .GetAlias (i )
254+ taskSteps , err := inflateTaskSteps (
255+ ctx ,
256+ c ,
257+ promo .Namespace ,
258+ alias ,
259+ promo .Spec .Vars ,
260+ step ,
261+ )
262+ if err != nil {
263+ return fmt .Errorf (
264+ "inflate tasks steps for task %q (%q): %w" ,
265+ step .Task .Name , alias , err ,
266+ )
267+ }
268+ steps = append (steps , taskSteps ... )
269+ default :
270+ step .As = step .GetAlias (i )
271+ steps = append (steps , step )
272+ }
273+ }
274+ promo .Spec .Steps = steps
275+ return nil
276+ }
277+
278+ // inflateTaskSteps inflates the PromotionSteps for the given PromotionStep
279+ // that references a (Cluster)PromotionTask. The task is retrieved and its
280+ // steps are inflated with the given task inputs.
281+ func inflateTaskSteps (
282+ ctx context.Context ,
283+ c client.Client ,
284+ project , taskAlias string ,
285+ promoVars []kargoapi.ExpressionVariable ,
286+ taskStep kargoapi.PromotionStep ,
287+ ) ([]kargoapi.PromotionStep , error ) {
288+ task , err := getPromotionTaskSpec (ctx , c , project , taskStep .Task )
289+ if err != nil {
290+ return nil , err
291+ }
292+
293+ vars , err := promotionTaskVarsToStepVars (task .Vars , promoVars , taskStep .Vars )
294+ if err != nil {
295+ return nil , err
296+ }
297+
298+ var steps []kargoapi.PromotionStep
299+ for i := range task .Steps {
300+ // Copy the step as-is.
301+ step := & task .Steps [i ]
302+
303+ // Ensures we have a unique alias for each step within the context of
304+ // the Promotion.
305+ step .As = generatePromotionTaskStepAlias (taskAlias , step .GetAlias (i ))
306+
307+ // With the variables validated and mapped, they are now available to
308+ // the Config of the step during the Promotion execution.
309+ step .Vars = append (vars , step .Vars ... )
310+
311+ // Append the inflated step to the list of steps.
312+ steps = append (steps , * step )
313+ }
314+ return steps , nil
315+ }
316+
317+ // getPromotionTaskSpec retrieves the PromotionTaskSpec for the given
318+ // PromotionTaskReference.
319+ func getPromotionTaskSpec (
320+ ctx context.Context ,
321+ c client.Client ,
322+ project string ,
323+ ref * kargoapi.PromotionTaskReference ,
324+ ) (* kargoapi.PromotionTaskSpec , error ) {
325+ var spec kargoapi.PromotionTaskSpec
326+
327+ if ref == nil {
328+ return nil , errors .New ("missing task reference" )
329+ }
330+
331+ switch ref .Kind {
332+ case "PromotionTask" , "" :
333+ task := & kargoapi.PromotionTask {}
334+ if err := c .Get (ctx , client.ObjectKey {Namespace : project , Name : ref .Name }, task ); err != nil {
335+ return nil , err
336+ }
337+ spec = task .Spec
338+ case "ClusterPromotionTask" :
339+ task := & kargoapi.ClusterPromotionTask {}
340+ if err := c .Get (ctx , client.ObjectKey {Name : ref .Name }, task ); err != nil {
341+ return nil , err
342+ }
343+ spec = task .Spec
344+ default :
345+ return nil , fmt .Errorf ("unknown task reference kind %q" , ref .Kind )
346+ }
347+
348+ return & spec , nil
349+ }
350+
351+ // generatePromotionTaskStepAlias generates an alias for a PromotionTask step
352+ // by combining the task alias and the step alias.
353+ func generatePromotionTaskStepAlias (taskAlias , stepAlias string ) string {
354+ return fmt .Sprintf ("%s%s%s" , taskAlias , PromotionAliasSeparator , stepAlias )
355+ }
356+
357+ // promotionTaskVarsToStepVars validates the presence of the PromotionTask
358+ // variables and maps them to variables which can be used by the inflated
359+ // PromotionStep.
360+ func promotionTaskVarsToStepVars (
361+ taskVars , promoVars , stepVars []kargoapi.ExpressionVariable ,
362+ ) ([]kargoapi.ExpressionVariable , error ) {
363+ // Promotion variables can be used to set (or override) the variables
364+ // required by the PromotionTask, but they are not inflated into the
365+ // variables for the step. This map is used to check if a variable is
366+ // set on the Promotion, to avoid overriding it with the default value
367+ // and to validate that the variable is set.
368+ promoVarsMap := make (map [string ]struct {}, len (promoVars ))
369+ for _ , v := range promoVars {
370+ if v .Value != "" {
371+ promoVarsMap [v .Name ] = struct {}{}
372+ }
373+ }
374+
375+ // Step variables are inflated into the variables for the step. This map
376+ // is used to ensure all variables required by the PromotionTask without
377+ // a default value are set.
378+ stepVarsMap := make (map [string ]struct {}, len (stepVars ))
379+ for _ , v := range stepVars {
380+ if v .Value != "" {
381+ stepVarsMap [v .Name ] = struct {}{}
382+ }
383+ }
384+
385+ var vars []kargoapi.ExpressionVariable
386+
387+ // Set the PromotionTask variable default values, but only if the variable
388+ // is not set on the Promotion.
389+ for _ , v := range taskVars {
390+ // Variable is set on the Promotion, we do not need to set the default.
391+ if _ , ok := promoVarsMap [v .Name ]; ok {
392+ continue
393+ }
394+
395+ // Set the variable if it has a default value.
396+ if v .Value != "" {
397+ vars = append (vars , v )
398+ continue
399+ }
400+
401+ // If not, the variable must be set in the step variables.
402+ if _ , ok := stepVarsMap [v .Name ]; ! ok {
403+ return nil , fmt .Errorf ("missing value for variable %q" , v .Name )
404+ }
405+ }
406+
407+ // Set the step variables.
408+ vars = append (vars , stepVars ... )
409+
410+ return vars , nil
411+ }
0 commit comments