Skip to content

Commit

Permalink
fix: evaluate step vars and outputs in indexer
Browse files Browse the repository at this point in the history
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
  • Loading branch information
hiddeco committed Jan 10, 2025
1 parent 7884c6e commit 0a9b704
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 54 deletions.
93 changes: 58 additions & 35 deletions internal/directives/promotions.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,26 @@ type PromotionStep struct {
Config []byte
}

// PromotionStepEnvOption is a functional option for customizing the
// environment of a PromotionStep built by BuildEnv.
type PromotionStepEnvOption func(map[string]any)

// StepWithVars returns a PromotionStepEnvOption that adds the provided vars to
// the environment of the PromotionStep.
func StepWithVars(vars map[string]any) PromotionStepEnvOption {
return func(env map[string]any) {
env["vars"] = vars
}
}

// StepWithSecrets returns a PromotionStepEnvOption that adds the provided secrets
// to the environment of the PromotionStep.
func StepWithSecrets(secrets map[string]map[string]string) PromotionStepEnvOption {
return func(env map[string]any) {
env["secrets"] = secrets
}
}

// GetTimeout returns the maximum interval the provided runner may spend
// attempting to execute the step before retries are abandoned and the entire
// Promotion is marked as failed. If the runner is a RetryableStepRunner, its
Expand All @@ -131,32 +151,23 @@ func (s *PromotionStep) GetErrorThreshold(runner any) uint32 {
return s.Retry.GetErrorThreshold(fallback)
}

// GetConfig returns the Config unmarshalled into a map. Any expr-lang
// expressions are evaluated in the context of the provided arguments
// prior to unmarshaling.
func (s *PromotionStep) GetConfig(
ctx context.Context,
cl client.Client,
// BuildEnv returns the environment for the PromotionStep. The environment
// includes the context of the Promotion, the outputs of the previous steps,
// and any additional options provided.
//
// The environment is a (nested) map of string keys to any values. The keys
// are used as variables in the PromotionStep configuration.
func (s *PromotionStep) BuildEnv(
promoCtx PromotionContext,
state State,
) (Config, error) {
if s.Config == nil {
return nil, nil
}

vars, err := s.GetVars(promoCtx, state)
if err != nil {
return nil, err
}

opts ...PromotionStepEnvOption,
) map[string]any {
env := map[string]any{
"ctx": map[string]any{
"project": promoCtx.Project,
"promotion": promoCtx.Promotion,
"stage": promoCtx.Stage,
},
"vars": vars,
"secrets": promoCtx.Secrets,
"outputs": state,
}

Expand All @@ -170,6 +181,34 @@ func (s *PromotionStep) GetConfig(
}
}

// Apply all provided options
for _, opt := range opts {
opt(env)
}

return env
}

// GetConfig returns the Config unmarshalled into a map. Any expr-lang
// expressions are evaluated in the context of the provided arguments
// prior to unmarshaling.
func (s *PromotionStep) GetConfig(
ctx context.Context,
cl client.Client,
promoCtx PromotionContext,
state State,
) (Config, error) {
if s.Config == nil {
return nil, nil
}

vars, err := s.GetVars(promoCtx, state)
if err != nil {
return nil, err
}

env := s.BuildEnv(promoCtx, state, StepWithVars(vars), StepWithSecrets(promoCtx.Secrets))

evaledCfgJSON, err := expressions.EvaluateJSONTemplate(
s.Config,
env,
Expand Down Expand Up @@ -219,25 +258,9 @@ func (s *PromotionStep) GetVars(
rawVars[v.Name] = v.Value
}

taskOutput := s.getTaskOutputs(state)

vars := make(map[string]any, len(rawVars))
for k, v := range rawVars {
env := map[string]any{
"ctx": map[string]any{
"project": promoCtx.Project,
"promotion": promoCtx.Promotion,
"stage": promoCtx.Stage,
},
"vars": vars,
"outputs": state,
}

if taskOutput != nil {
env["task"] = map[string]any{
"outputs": taskOutput,
}
}
env := s.BuildEnv(promoCtx, state)

newVar, err := expressions.EvaluateTemplate(v, env)
if err != nil {
Expand Down
35 changes: 18 additions & 17 deletions internal/indexer/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ func RunningPromotionsByArgoCDApplications(
return nil
}

// Build just enough context to extract the relevant config from the
// argocd-update promotion step.
promoCtx := directives.PromotionContext{
Project: promo.Namespace,
Stage: promo.Spec.Stage,
Promotion: promo.Name,
Vars: promo.Spec.Vars,
}

// Extract the Argo CD Applications from the promotion steps.
//
// TODO(hidde): This is not ideal as it requires parsing the directives and
Expand All @@ -155,20 +164,14 @@ func RunningPromotionsByArgoCDApplications(
if step.Uses != "argocd-update" || step.Config == nil {
continue
}

dirStep := directives.PromotionStep{
Kind: step.Uses,
Alias: step.As,
Vars: step.Vars,
Vars: step.Vars,
Config: step.Config.Raw,
}
// Build just enough context to extract the relevant config from the
// argocd-update promotion step.
promoCtx := directives.PromotionContext{
Project: promo.Namespace,
Stage: promo.Spec.Stage,
Promotion: promo.Name,
Vars: promo.Spec.Vars,
}

// As we are not evaluating expressions in the entire config, we do not
// pass any state.
vars, err := dirStep.GetVars(promoCtx, promo.Status.GetState())
Expand Down Expand Up @@ -215,14 +218,12 @@ func RunningPromotionsByArgoCDApplications(
for _, app := range appsList {
if app, ok := app.(map[string]any); ok {
if nameTemplate, ok := app["name"].(string); ok {
env := map[string]any{
"ctx": map[string]any{
"project": promoCtx.Project,
"promotion": promoCtx.Promotion,
"stage": promoCtx.Stage,
},
"vars": vars,
}
env := dirStep.BuildEnv(
promoCtx,
promo.Status.GetState(),
directives.StepWithVars(vars),
)

var namespace any = libargocd.Namespace()
if namespaceTemplate, ok := app["namespace"].(string); ok {
if namespace, err = expressions.EvaluateTemplate(namespaceTemplate, env); err != nil {
Expand Down
68 changes: 66 additions & 2 deletions internal/indexer/indexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,12 @@ func TestRunningPromotionsByArgoCDApplications(t *testing.T) {
},
Spec: kargoapi.PromotionSpec{
Stage: "fake-stage",
Vars: []kargoapi.PromotionVariable{
{
Name: "app",
Value: "fake-app-from-var",
},
},
Steps: []kargoapi.PromotionStep{
{
Uses: "argocd-update",
Expand All @@ -346,16 +352,74 @@ func TestRunningPromotionsByArgoCDApplications(t *testing.T) {
Raw: []byte(`{"apps":[{"name":"fake-app-${{ ctx.stage }}"}]}`),
},
},
{
Uses: "argocd-update",
Config: &apiextensionsv1.JSON{
// Note that this uses a variable within the expression
Raw: []byte(`{"apps":[{"name":"${{ vars.app }}"}]}`),
},
},
{
Uses: "argocd-update",
Vars: []kargoapi.PromotionVariable{
{
Name: "app",
Value: "fake-app-from-step-var",
},
},
Config: &apiextensionsv1.JSON{
// Note that this uses a step-level variable within the expression
Raw: []byte(`{"apps":[{"name":"${{ vars.app }}"}]}`),
},
},
{
Uses: "argocd-update",
Config: &apiextensionsv1.JSON{
// Note that this uses output from a (fake) previous step within the expression
Raw: []byte(`{"apps":[{"name":"fake-app-${{ outputs.push.branch }}"}]}`),
},
},
{
Uses: "argocd-update",
Vars: []kargoapi.PromotionVariable{
{
Name: "input",
Value: "${{ outputs.composition.name }}",
},
},
Config: &apiextensionsv1.JSON{
// Note that this uses output from a previous step through a variable
Raw: []byte(`{"apps":[{"name":"fake-app-${{ vars.input }}"}]}`),
},
},
{
Uses: "argocd-update",
As: "task-1::update",
Config: &apiextensionsv1.JSON{
// Note that this uses output from a "task" step within the expression
Raw: []byte(`{"apps":[{"name":"fake-app-${{ task.outputs.fake.name }}"}]}`),
},
},
},
},
Status: kargoapi.PromotionStatus{
Phase: kargoapi.PromotionPhaseRunning,
CurrentStep: 2, // Ensure all steps above are considered
Phase: kargoapi.PromotionPhaseRunning,
State: &apiextensionsv1.JSON{
// Mock the output of the previous steps
// nolint:lll
Raw: []byte(`{"push":{"branch":"from-branch"},"composition":{"name":"from-composition"},"task-1::fake":{"name":"from-task"}}`),
},
CurrentStep: 7, // Ensure all steps above are considered
},
},
expected: []string{
"fake-namespace:fake-app",
fmt.Sprintf("%s:%s", argocd.Namespace(), "fake-app-fake-stage"),
fmt.Sprintf("%s:%s", argocd.Namespace(), "fake-app-from-var"),
fmt.Sprintf("%s:%s", argocd.Namespace(), "fake-app-from-step-var"),
fmt.Sprintf("%s:%s", argocd.Namespace(), "fake-app-from-branch"),
fmt.Sprintf("%s:%s", argocd.Namespace(), "fake-app-from-composition"),
fmt.Sprintf("%s:%s", argocd.Namespace(), "fake-app-from-task"),
},
},
{
Expand Down

0 comments on commit 0a9b704

Please sign in to comment.