Skip to content
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

Eject with missing nodes #393

Merged
merged 7 commits into from
Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 2 additions & 30 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,6 @@
### Improvements

- Ensure that constant values passed to enums are valid.
[#357](https://github.com/pulumi/pulumi-yaml/pull/357)

- Warn on non camelCase names.
[#362](https://github.com/pulumi/pulumi-yaml/pull/362)

- Recognize the new core project-level `config` block.
[#369](https://github.com/pulumi/pulumi-yaml/pull/369)
- Allow ejecting when relying on `config` nodes.
[#393](https://github.com/pulumi/pulumi-yaml/pull/393)

### Bug Fixes

- Allow interpolations for `AssetOrArchive` function values
[#341](https://github.com/pulumi/pulumi-yaml/pull/341)

- Clarify the lifetimes when calling `codegen.Eject`. This is a breaking change to the
`codegen.Eject` API.
[#358](https://github.com/pulumi/pulumi-yaml/pull/358)

- Quote generated strings that could be numbers.
[#363](https://github.com/pulumi/pulumi-yaml/issues/363)

- Respect import option on resource.
[#367](https://github.com/pulumi/pulumi-yaml/issues/367)

- Discover Invokes during `GetReferencedPlugins`.
[#381](https://github.com/pulumi/pulumi-yaml/pull/381)

- Escaped interpolated strings now remove one extra dollar sign.
[#382](https://github.com/pulumi/pulumi-yaml/pull/382)

- Only insert "id" in ejected resource refs when the receiver type is a string.
[#389](https://github.com/pulumi/pulumi-yaml/pull/389)
44 changes: 24 additions & 20 deletions pkg/pulumiyaml/codegen/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,56 +53,60 @@ func ConvertTemplateIL(template *ast.TemplateDecl, loader schema.ReferenceLoader

pkgLoader := pulumiyaml.NewPackageLoaderFromSchemaLoader(loader)
// nil runner passed in since template is not executed and we can use pkgLoader
_, tdiags, err := pulumiyaml.PrepareTemplate(template, nil, pkgLoader)
r, tdiags, err := pulumiyaml.PrepareTemplate(template, nil, pkgLoader)
if err != nil {
return "", diags, err
}
diags = diags.Extend(tdiags.HCL())
if diags.HasErrors() {
return "", diags, nil
}

pulumiyaml.InjectMissingNodes(r, template)

templateBody, tdiags := ImportTemplate(template, pkgLoader)
diags = diags.Extend(tdiags.HCL())
if diags.HasErrors() {
return "", diags, nil
if templateBody == nil {
// This is a irrecoverable error, so we make sure the error field is non-nil
return "", diags, diags
}
programText := fmt.Sprintf("%v", templateBody)

return programText, nil, nil
if programText == "" {
return "", diags, diags
}

return programText, diags, nil
}

func EjectProgram(template *ast.TemplateDecl, loader schema.ReferenceLoader) (*pcl.Program, hcl.Diagnostics, error) {
programText, diags, err := ConvertTemplateIL(template, loader)
if err != nil {
return nil, diags, err
}
if diags.HasErrors() {
return nil, diags, fmt.Errorf("internal error: %w", diags)
programText, yamlDiags, err := ConvertTemplateIL(template, loader)
if err != nil || programText == "" {
return nil, yamlDiags, err
}

parser := hclsyntax.NewParser()
if err := parser.ParseFile(strings.NewReader(programText), "program.pp"); err != nil {
return nil, diags, err
return nil, yamlDiags, err
}
diags = diags.Extend(parser.Diagnostics)
diags := parser.Diagnostics
if diags.HasErrors() {
return nil, diags, nil
return nil, append(yamlDiags, diags...), diags
}

bindOpts := []pcl.BindOption{
pcl.SkipResourceTypechecking,
pcl.AllowMissingProperties,
pcl.AllowMissingVariables,
}
bindOpts = append(bindOpts, pcl.Loader(loader))
program, pdiags, err := pcl.BindProgram(parser.Files, bindOpts...)
diags = diags.Extend(pdiags)
if err != nil {
return nil, diags, err
return nil, append(yamlDiags, diags...), err
}
if pdiags.HasErrors() {
return nil, diags, fmt.Errorf("internal error: %w", pdiags)
if pdiags.HasErrors() || program == nil {
return nil, append(yamlDiags, diags...), fmt.Errorf("internal error: %w", pdiags)
}

return program, diags, nil
return program, append(yamlDiags, diags...), nil
}

// ConvertTemplate converts a Pulumi YAML template to a target language using PCL as an intermediate representation.
Expand Down
16 changes: 12 additions & 4 deletions pkg/pulumiyaml/codegen/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (imp *importer) importRef(node ast.Expr, name string, environment map[strin
}

return &model.ScopeTraversalExpression{
Traversal: hcl.Traversal{hcl.TraverseRoot{Name: name}},
Traversal: hcl.Traversal{hcl.TraverseRoot{Name: camel(makeLegalIdentifier(name))}},
Parts: []model.Traversable{model.DynamicType},
}, syntax.Diagnostics{ast.ExprError(node, fmt.Sprintf("unknown config, variable, or resource '%v'", name), "")}
}
Expand Down Expand Up @@ -511,7 +511,7 @@ func (imp *importer) importConfig(kvp ast.ConfigMapEntry) (model.BodyItem, synta
typeExpr = "string"
}

configVar, ok := imp.configuration[name]
configName, ok := imp.configuration[name]
contract.Assert(ok)

var defaultValue model.Expression
Expand All @@ -527,8 +527,15 @@ func (imp *importer) importConfig(kvp ast.ConfigMapEntry) (model.BodyItem, synta

configDef := &model.Block{
Type: "config",
Labels: []string{configVar.Name, typeExpr},
Body: &model.Body{},
Labels: []string{configName.Name, typeExpr},
Body: &model.Body{
Items: []model.BodyItem{
&model.Attribute{
Name: pcl.LogicalNamePropertyKey,
Value: quotedLit(kvp.Key.GetValue()),
},
},
},
}
if defaultValue != nil {
configDef.Body.Items = append(configDef.Body.Items, &model.Attribute{
Expand Down Expand Up @@ -922,6 +929,7 @@ func (imp *importer) assignNames() {

assign := func(name, suffix string) *model.Variable {
assignName := func(name, suffix string) string {

name = camel(makeLegalIdentifier(name))
if !assigned.Has(name) {
assigned.Add(name)
Expand Down
73 changes: 54 additions & 19 deletions pkg/pulumiyaml/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,12 @@ func HasDiagnostics(err error) (syntax.Diagnostics, bool) {
}
}

func (r *Runner) setDefaultProviders() error {
// Set default providers for resources and invokes.
//
// This function communicates errors by appending to the internal diags field of `r`.
// It is the responsibility of the caller to verify that no err diags were appended if
// that should prevent proceeding.
func (r *Runner) setDefaultProviders() {
defaultProviderInfoMap := make(map[string]*providerInfo)
for _, resource := range r.t.Resources.Entries {
v := resource.Value
Expand All @@ -196,7 +201,9 @@ func (r *Runner) setDefaultProviders() error {
}
}
} else if v.DefaultProvider != nil {
return errors.New("cannot set defaultProvider on non-provider resource")
r.sdiags.Extend(syntax.NodeError(
v.DefaultProvider.Syntax(),
"cannot set defaultProvider on non-provider resource", ""))
}
}

Expand Down Expand Up @@ -282,10 +289,10 @@ func (r *Runner) setDefaultProviders() error {
},
})

if diags.HasErrors() {
return diags
}
return nil
// This function communicates errors by appending to the internal diags field of `r`.
// It is the responsibility of the caller to verify that no err diags were appended if
// that should prevent proceeding.
contract.IgnoreError(diags)
}

// PrepareTemplate prepares a template for converting or running
Expand All @@ -295,25 +302,27 @@ func PrepareTemplate(t *ast.TemplateDecl, r *Runner, loader PackageLoader) (*Run
if r == nil {
r = newRunner(t, loader)
}

// We are preemptively calling r.setIntermediates. We are forcing tolerating missing
// nodes, ensuring the process can continue even for invalid templates. Diags will
// still be reported normally.
//
// r.setDefaultProviders uses r.setIntermediates, so this line need to precede calls
// to r.setDefaultProviders.
r.setIntermediates(nil, true /*force*/)

// runner hooks up default providers
err := r.setDefaultProviders()
if err != nil {
return nil, nil, err
}
r.setDefaultProviders()

// runner type checks nodes
_, diags := TypeCheck(r)
if diags.HasErrors() {
return nil, diags, nil
}

return r, diags, nil
}

// RunTemplate runs the programEvaluator against a template using the given request/settings.
func RunTemplate(ctx *pulumi.Context, t *ast.TemplateDecl, config map[string]string, loader PackageLoader) error {
r := newRunner(t, loader)
r.setIntermediates(config)
r.setIntermediates(config, false)
if r.sdiags.HasErrors() {
return &r.sdiags
}
Expand Down Expand Up @@ -722,21 +731,24 @@ func getPulumiConfNodes(config map[string]string) ([]configNode, error) {
}

// setIntermediates is called for convert and runtime evaluation
func (r *Runner) setIntermediates(config map[string]string) {
//
// If force is true, set intermediates even if errors were encountered
// Errors will always be reflected in r.sdiags.
func (r *Runner) setIntermediates(config map[string]string, force bool) {
if r.intermediates != nil {
return
}

r.intermediates = []graphNode{}
confNodes, err := getPulumiConfNodes(config)
if err != nil {
if err != nil && !force {
r.sdiags.Extend(syntax.Error(nil, err.Error(), ""))
return
}
// Topologically sort the intermediates based on implicit and explicit dependencies
intermediates, rdiags := topologicallySortedResources(r.t, confNodes)
r.sdiags.Extend(rdiags...)
if rdiags.HasErrors() {
if rdiags.HasErrors() && !force {
return
}
if intermediates != nil {
Expand All @@ -747,7 +759,7 @@ func (r *Runner) setIntermediates(config map[string]string) {
// ensureSetup is called at runtime evaluation
func (r *Runner) ensureSetup(ctx *pulumi.Context) {
// Our tests need to set intermediates, even though they don't have runtime config
r.setIntermediates(nil)
r.setIntermediates(nil, false)

cwd, err := os.Getwd()
if err != nil {
Expand Down Expand Up @@ -842,6 +854,8 @@ func (r *Runner) Run(e Evaluator) syntax.Diagnostics {
if !e.EvalResource(r, kvp) {
return returnDiags()
}
case missingNode:
// We ignore this intentionally
}
}

Expand Down Expand Up @@ -2166,3 +2180,24 @@ func listStrings(v *ast.StringListDecl) []string {
}
return a
}

// Edit the template with the goal of making it valid.
//
// This is a best effort function, and does not guarantee a valid template.
func InjectMissingNodes(r *Runner, template *ast.TemplateDecl) {
for _, node := range r.intermediates {
if node, ok := node.(missingNode); ok {
if isGlobalConfigName(node.name.GetValue()) {
template.Configuration.Entries = append(template.Configuration.Entries,
ast.ConfigMapEntry{
Key: node.key(),
Value: &ast.ConfigParamDecl{},
})
}
}
}
}

func isGlobalConfigName(s string) bool {
return strings.Count(s, ":") == 1
}
3 changes: 2 additions & 1 deletion pkg/pulumiyaml/run_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ variables:
}
err := pulumi.RunErr(func(ctx *pulumi.Context) error {
runner := newRunner(template, newMockPackageMap())
assert.Equal(t, runner.setDefaultProviders(), nil)
runner.setDefaultProviders()
requireNoErrors(t, template, runner.sdiags.diags)
diags := runner.Evaluate(ctx)
requireNoErrors(t, template, diags)
return nil
Expand Down
15 changes: 14 additions & 1 deletion pkg/pulumiyaml/sort.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ func (e configNodeEnv) value() interface{} {
return e.Value
}

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 @@ -167,7 +179,8 @@ func topologicallySortedResources(t *ast.TemplateDecl, externalConfig []configNo
e, ok := intermediates[name.Value]
if !ok {
diags.Extend(ast.ExprError(name, fmt.Sprintf("resource %q not found", name.Value), ""))
return false
e = missingNode{name}
addIntermediate(name.Value, e)
}
kind := e.valueKind()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
config sqlAdmin string {
__logicalName = "sqlAdmin"
default = "pulumi"
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
config sqlAdmin string {
__logicalName = "sqlAdmin"
default = "pulumi"
}

config retentionInDays int {
__logicalName = "retentionInDays"
default = 30
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
config hostname string {
__logicalName = "hostname"
default = "example.com"
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
config instanceType string {
__logicalName = "InstanceType"
default = "t3.micro"
}

Expand Down
1 change: 1 addition & 0 deletions pkg/tests/transpiled_examples/webserver-pp/webserver.pp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
config instanceType string {
__logicalName = "InstanceType"
default = "t3.micro"
}

Expand Down