From 2d85f1f78479721b795470be6cb4a04a40160884 Mon Sep 17 00:00:00 2001 From: Julien Poissonnier Date: Tue, 27 Aug 2024 14:27:59 +0200 Subject: [PATCH] Allow missing nodes in template 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. --- CHANGELOG_PENDING.md | 3 +++ pkg/pulumiyaml/analyser.go | 17 ++++++++++++++++ pkg/pulumiyaml/run.go | 10 ++++++++++ pkg/pulumiyaml/run_test.go | 40 ++++++++++++++++++++++++++++++++++++++ pkg/pulumiyaml/sort.go | 19 +++++++++++++++--- 5 files changed, 86 insertions(+), 3 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index db5b3070..68ef54bb 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -6,3 +6,6 @@ - Parse the items property on config type declarations to prevent diagnostic messages about unknown fields [#615](https://github.com/pulumi/pulumi-yaml/pull/615) + +- Allow missing nodes in template to enable walking templates without config + [#617](https://github.com/pulumi/pulumi-yaml/pull/617) diff --git a/pkg/pulumiyaml/analyser.go b/pkg/pulumiyaml/analyser.go index be2a7401..4abcda65 100644 --- a/pkg/pulumiyaml/analyser.go +++ b/pkg/pulumiyaml/analyser.go @@ -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. @@ -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, }) @@ -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 } @@ -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) { diff --git a/pkg/pulumiyaml/run.go b/pkg/pulumiyaml/run.go index d44a54c2..516d6fe3 100644 --- a/pkg/pulumiyaml/run.go +++ b/pkg/pulumiyaml/run.go @@ -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 { @@ -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) @@ -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() + } } } diff --git a/pkg/pulumiyaml/run_test.go b/pkg/pulumiyaml/run_test.go index 81697115..5d00c9e6 100644 --- a/pkg/pulumiyaml/run_test.go +++ b/pkg/pulumiyaml/run_test.go @@ -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, ":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() diff --git a/pkg/pulumiyaml/sort.go b/pkg/pulumiyaml/sort.go index 38c8c794..86d8a88b 100644 --- a/pkg/pulumiyaml/sort.go +++ b/pkg/pulumiyaml/sort.go @@ -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 @@ -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()