These changes add support for provider-side previews of create and update operations, which allows resource providers to supply output property values for resources that are being created or updated during a preview. If a plugin supports provider-side preview, its create/update methods will be invoked during previews with the `preview` property set to true. It is the responsibility of the provider to fill in any output properties that are known before returning. It is a best practice for providers to only fill in property values that are guaranteed to be identical if the preview were instead an update (i.e. only those output properties whose values can be conclusively determined without actually performing the create/update operation should be populated). Providers that support previews must accept unknown values in their create and update methods. If a plugin does not support provider-side preview, the inputs to a create or update operation will be propagated to the outputs as they are today. Fixes #4992.
255 lines
7.1 KiB
Go
255 lines
7.1 KiB
Go
// Copyright 2016-2020, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package hcl2
|
|
|
|
import (
|
|
"os"
|
|
"sort"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/pulumi/pulumi/pkg/v2/codegen/hcl2/model"
|
|
"github.com/pulumi/pulumi/pkg/v2/codegen/hcl2/syntax"
|
|
"github.com/pulumi/pulumi/pkg/v2/codegen/schema"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/resource/plugin"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
type bindOptions struct {
|
|
allowMissingVariables bool
|
|
loader schema.Loader
|
|
packageCache *PackageCache
|
|
}
|
|
|
|
func (opts bindOptions) modelOptions() []model.BindOption {
|
|
if opts.allowMissingVariables {
|
|
return []model.BindOption{model.AllowMissingVariables}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type binder struct {
|
|
options bindOptions
|
|
|
|
referencedPackages map[string]*schema.Package
|
|
typeSchemas map[model.Type]schema.Type
|
|
|
|
tokens syntax.TokenMap
|
|
nodes []Node
|
|
root *model.Scope
|
|
}
|
|
|
|
type BindOption func(*bindOptions)
|
|
|
|
func AllowMissingVariables(options *bindOptions) {
|
|
options.allowMissingVariables = true
|
|
}
|
|
|
|
func PluginHost(host plugin.Host) BindOption {
|
|
return Loader(schema.NewPluginLoader(host))
|
|
}
|
|
|
|
func Loader(loader schema.Loader) BindOption {
|
|
return func(options *bindOptions) {
|
|
options.loader = loader
|
|
}
|
|
}
|
|
|
|
func Cache(cache *PackageCache) BindOption {
|
|
return func(options *bindOptions) {
|
|
options.packageCache = cache
|
|
}
|
|
}
|
|
|
|
// BindProgram performs semantic analysis on the given set of HCL2 files that represent a single program. The given
|
|
// host, if any, is used for loading any resource plugins necessary to extract schema information.
|
|
func BindProgram(files []*syntax.File, opts ...BindOption) (*Program, hcl.Diagnostics, error) {
|
|
var options bindOptions
|
|
for _, o := range opts {
|
|
o(&options)
|
|
}
|
|
|
|
if options.loader == nil {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
ctx, err := plugin.NewContext(nil, nil, nil, nil, cwd, nil, false, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
options.loader = schema.NewPluginLoader(ctx.Host)
|
|
|
|
defer contract.IgnoreClose(ctx)
|
|
}
|
|
|
|
if options.packageCache == nil {
|
|
options.packageCache = NewPackageCache()
|
|
}
|
|
|
|
b := &binder{
|
|
options: options,
|
|
tokens: syntax.NewTokenMapForFiles(files),
|
|
referencedPackages: map[string]*schema.Package{},
|
|
typeSchemas: map[model.Type]schema.Type{},
|
|
root: model.NewRootScope(syntax.None),
|
|
}
|
|
|
|
// Define null.
|
|
b.root.Define("null", &model.Constant{
|
|
Name: "null",
|
|
ConstantValue: cty.NullVal(cty.DynamicPseudoType),
|
|
})
|
|
// Define builtin functions.
|
|
for name, fn := range pulumiBuiltins {
|
|
b.root.DefineFunction(name, fn)
|
|
}
|
|
// Define the invoke function.
|
|
b.root.DefineFunction(Invoke, model.NewFunction(model.GenericFunctionSignature(b.bindInvokeSignature)))
|
|
|
|
var diagnostics hcl.Diagnostics
|
|
|
|
// Sort files in source order, then declare all top-level nodes in each.
|
|
sort.Slice(files, func(i, j int) bool {
|
|
return files[i].Name < files[j].Name
|
|
})
|
|
for _, f := range files {
|
|
fileDiags, err := b.declareNodes(f)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
diagnostics = append(diagnostics, fileDiags...)
|
|
}
|
|
|
|
// Now bind the nodes.
|
|
for _, n := range b.nodes {
|
|
diagnostics = append(diagnostics, b.bindNode(n)...)
|
|
}
|
|
|
|
return &Program{
|
|
Nodes: b.nodes,
|
|
files: files,
|
|
binder: b,
|
|
}, diagnostics, nil
|
|
}
|
|
|
|
// declareNodes declares all of the top-level nodes in the given file. This includes config, resources, outputs, and
|
|
// locals.
|
|
func (b *binder) declareNodes(file *syntax.File) (hcl.Diagnostics, error) {
|
|
var diagnostics hcl.Diagnostics
|
|
|
|
// Declare body items in source order.
|
|
for _, item := range model.SourceOrderBody(file.Body) {
|
|
switch item := item.(type) {
|
|
case *hclsyntax.Attribute:
|
|
v := &LocalVariable{syntax: item}
|
|
attrDiags := b.declareNode(item.Name, v)
|
|
diagnostics = append(diagnostics, attrDiags...)
|
|
|
|
if err := b.loadReferencedPackageSchemas(v); err != nil {
|
|
return nil, err
|
|
}
|
|
case *hclsyntax.Block:
|
|
switch item.Type {
|
|
case "config":
|
|
name, typ := "<unnamed>", model.Type(model.DynamicType)
|
|
switch len(item.Labels) {
|
|
case 1:
|
|
name = item.Labels[0]
|
|
case 2:
|
|
name = item.Labels[0]
|
|
|
|
typeExpr, diags := model.BindExpressionText(item.Labels[1], model.TypeScope, item.LabelRanges[1].Start)
|
|
diagnostics = append(diagnostics, diags...)
|
|
typ = typeExpr.Type()
|
|
default:
|
|
diagnostics = append(diagnostics, labelsErrorf(item, "config variables must have exactly one or two labels"))
|
|
}
|
|
|
|
// TODO(pdg): check body for valid contents
|
|
|
|
v := &ConfigVariable{
|
|
typ: typ,
|
|
syntax: item,
|
|
}
|
|
diags := b.declareNode(name, v)
|
|
diagnostics = append(diagnostics, diags...)
|
|
|
|
if err := b.loadReferencedPackageSchemas(v); err != nil {
|
|
return nil, err
|
|
}
|
|
case "resource":
|
|
if len(item.Labels) != 2 {
|
|
diagnostics = append(diagnostics, labelsErrorf(item, "resource variables must have exactly two labels"))
|
|
}
|
|
|
|
resource := &Resource{
|
|
syntax: item,
|
|
}
|
|
declareDiags := b.declareNode(item.Labels[0], resource)
|
|
diagnostics = append(diagnostics, declareDiags...)
|
|
|
|
if err := b.loadReferencedPackageSchemas(resource); err != nil {
|
|
return nil, err
|
|
}
|
|
case "output":
|
|
name, typ := "<unnamed>", model.Type(model.DynamicType)
|
|
switch len(item.Labels) {
|
|
case 1:
|
|
name = item.Labels[0]
|
|
case 2:
|
|
name = item.Labels[0]
|
|
|
|
typeExpr, diags := model.BindExpressionText(item.Labels[1], model.TypeScope, item.LabelRanges[1].Start)
|
|
diagnostics = append(diagnostics, diags...)
|
|
typ = typeExpr.Type()
|
|
default:
|
|
diagnostics = append(diagnostics, labelsErrorf(item, "config variables must have exactly one or two labels"))
|
|
}
|
|
|
|
// TODO(pdg): check body for valid contents
|
|
|
|
v := &OutputVariable{
|
|
typ: typ,
|
|
syntax: item,
|
|
}
|
|
diags := b.declareNode(name, v)
|
|
diagnostics = append(diagnostics, diags...)
|
|
|
|
if err := b.loadReferencedPackageSchemas(v); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return diagnostics, nil
|
|
}
|
|
|
|
// declareNode declares a single top-level node. If a node with the same name has already been declared, it returns an
|
|
// appropriate diagnostic.
|
|
func (b *binder) declareNode(name string, n Node) hcl.Diagnostics {
|
|
if !b.root.Define(name, n) {
|
|
existing, _ := b.root.BindReference(name)
|
|
return hcl.Diagnostics{errorf(existing.SyntaxNode().Range(), "%q already declared", name)}
|
|
}
|
|
b.nodes = append(b.nodes, n)
|
|
return nil
|
|
}
|
|
|
|
func (b *binder) bindExpression(node hclsyntax.Node) (model.Expression, hcl.Diagnostics) {
|
|
return model.BindExpression(node, b.root, b.tokens, b.options.modelOptions()...)
|
|
}
|