Skip to content

Commit

Permalink
Allow missing nodes in template
Browse files Browse the repository at this point in the history
When a node can not be found, add a missingNode as a placeholder. When
walking the graph, VisitMissing can customize the behaviour for these
nodes. For typechecking and program evaluation missing nodes are always
an error.

During `GetRequiredPlugins` we do not have the program's config
available to us. Previously we would abort the graph walk as soon as we
hit a missing configuration reference. With this PR, we ignore missing
nodes and continue the walk. This allows us detect plugins configured on
resources.
  • Loading branch information
julienp committed Aug 27, 2024
1 parent f20ccea commit c9261f6
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 3 deletions.
17 changes: 17 additions & 0 deletions pkg/pulumiyaml/analyser.go
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,12 @@ func (tc *typeCache) typeConfig(r *Runner, node configNode) bool {
return true
}

func (tc *typeCache) typeMissing(r *Runner, node missingNode) bool {
ctx := r.newContext(node)
ctx.errorf(node.key(), fmt.Sprintf("resource, variable, or config value %q not found", node.key().Value))
return false
}

// Checks for config type compatibility between types A and B, and if B can be assigned to A.
// Config types are compatible if
// - They are the same type.
Expand Down Expand Up @@ -1186,6 +1192,7 @@ func TypeCheck(r *Runner) (Typing, syntax.Diagnostics) {
VisitExpr: types.typeExpr,
VisitVariable: types.typeVariable,
VisitConfig: types.typeConfig,
VisitMissing: types.typeMissing,
VisitOutput: types.typeOutput,
})

Expand All @@ -1197,6 +1204,7 @@ type walker struct {
VisitVariable func(r *Runner, node variableNode) bool
VisitOutput func(r *Runner, node ast.PropertyMapEntry) bool
VisitResource func(r *Runner, node resourceNode) bool
VisitMissing func(r *Runner, node missingNode) bool
VisitExpr func(*evalContext, ast.Expr) bool
}

Expand Down Expand Up @@ -1324,6 +1332,15 @@ func (e walker) EvalResource(r *Runner, node resourceNode) bool {
return true
}

func (e walker) EvalMissing(r *Runner, node missingNode) bool {
if e.VisitMissing != nil {
if !e.VisitMissing(r, node) {
return false
}
}
return true
}

func (e walker) walkPropertyMap(ctx *evalContext, m ast.PropertyMapDecl) bool {
for _, prop := range m.Entries {
if !e.walk(ctx, prop.Key) {
Expand Down
10 changes: 10 additions & 0 deletions pkg/pulumiyaml/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,7 @@ type Evaluator interface {
EvalVariable(r *Runner, node variableNode) bool
EvalResource(r *Runner, node resourceNode) bool
EvalOutput(r *Runner, node ast.PropertyMapEntry) bool
EvalMissing(r *Runner, node missingNode) bool
}

type programEvaluator struct {
Expand Down Expand Up @@ -750,6 +751,11 @@ func (e programEvaluator) EvalResource(r *Runner, node resourceNode) bool {
return true
}

func (e programEvaluator) EvalMissing(r *Runner, node missingNode) bool {
e.error(node.key(), fmt.Sprintf("resource, variable, or config value %q not found", node.key().Value))
return false
}

func (e programEvaluator) EvalOutput(r *Runner, node ast.PropertyMapEntry) bool {
ctx := r.newContext(node)
out, ok := e.registerOutput(node)
Expand Down Expand Up @@ -885,6 +891,10 @@ func (r *Runner) Run(e Evaluator) syntax.Diagnostics {
if !e.EvalResource(r, kvp) {
return returnDiags()
}
case missingNode:
if !e.EvalMissing(r, kvp) {
return returnDiags()
}
}
}

Expand Down
40 changes: 40 additions & 0 deletions pkg/pulumiyaml/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2043,6 +2043,46 @@ resources:
assert.NoError(t, err)
}

func TestEvaluateMissingError(t *testing.T) {
t.Parallel()

text := `
name: test-missing-config-value
runtime: yaml
variables:
foo: ${someConfigValue}
`
tmpl := yamlTemplate(t, strings.TrimSpace(text))
err := pulumi.RunErr(func(ctx *pulumi.Context) error {
runner := newRunner(tmpl, newMockPackageMap())
err := runner.Evaluate(ctx)
assert.Len(t, err, 1)
assert.Equal(t, "<stdin>:4,8-8: resource, variable, or config value \"someConfigValue\" not found; ", err.Error())
return nil
}, pulumi.WithMocks("project", "stack", &testMonitor{}))
assert.NoError(t, err)
}

func TestRunMissingIgnore(t *testing.T) {
t.Parallel()

text := `
name: test-missing-config-value
runtime: yaml
variables:
foo: ${someConfigValue}
`
tmpl := yamlTemplate(t, strings.TrimSpace(text))
err := pulumi.RunErr(func(ctx *pulumi.Context) error {
runner := newRunner(tmpl, newMockPackageMap())
err := runner.Run(walker{})
assert.Len(t, err, 0)
assert.Equal(t, "no diagnostics", err.Error())
return nil
}, pulumi.WithMocks("project", "stack", &testMonitor{}))
assert.NoError(t, err)
}

func TestResourceWithAlias(t *testing.T) {
t.Parallel()

Expand Down
19 changes: 16 additions & 3 deletions pkg/pulumiyaml/sort.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ func (e configNodeProp) value() interface{} {
return e.v.V
}

type missingNode struct {
name *ast.StringExpr
}

func (e missingNode) key() *ast.StringExpr {
return e.name
}

func (missingNode) valueKind() string {
return "missing node"
}

func topologicallySortedResources(t *ast.TemplateDecl, externalConfig []configNode) ([]graphNode, syntax.Diagnostics) {
var diags syntax.Diagnostics

Expand Down Expand Up @@ -166,11 +178,12 @@ func topologicallySortedResources(t *ast.TemplateDecl, externalConfig []configNo

e, ok := intermediates[name.Value]
if !ok {
if e2, ok := intermediates[stripConfigNamespace(t.Name.Value, name.Value)]; ok {
s := stripConfigNamespace(t.Name.Value, name.Value)
if e2, ok := intermediates[s]; ok {
e = e2
} else {
diags.Extend(ast.ExprError(name, fmt.Sprintf("resource, variable, or config value %q not found", name.Value), ""))
return false
e = missingNode{name}
addIntermediate(name.Value, e)
}
}
kind := e.valueKind()
Expand Down

0 comments on commit c9261f6

Please sign in to comment.