2020-03-17 04:04:32 +01:00
|
|
|
// 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.
|
|
|
|
|
2020-04-07 04:43:16 +02:00
|
|
|
//nolint: goconst
|
2020-03-17 04:04:32 +01:00
|
|
|
package hcl2
|
|
|
|
|
|
|
|
import (
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
2020-04-07 04:43:16 +02:00
|
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
2020-04-14 10:30:25 +02:00
|
|
|
"github.com/pulumi/pulumi/pkg/v2/codegen"
|
|
|
|
"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"
|
2020-03-17 04:04:32 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
func getResourceToken(node *Resource) (string, hcl.Range) {
|
|
|
|
return node.Syntax.Labels[1], node.Syntax.LabelRanges[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *binder) bindResource(node *Resource) hcl.Diagnostics {
|
2020-04-17 01:44:34 +02:00
|
|
|
var diagnostics hcl.Diagnostics
|
|
|
|
|
|
|
|
typeDiags := b.bindResourceTypes(node)
|
|
|
|
diagnostics = append(diagnostics, typeDiags...)
|
|
|
|
|
|
|
|
bodyDiags := b.bindResourceBody(node)
|
|
|
|
diagnostics = append(diagnostics, bodyDiags...)
|
|
|
|
|
|
|
|
return diagnostics
|
2020-03-17 04:04:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// bindResourceTypes binds the input and output types for a resource.
|
|
|
|
func (b *binder) bindResourceTypes(node *Resource) hcl.Diagnostics {
|
|
|
|
// Set the input and output types to dynamic by default.
|
2020-04-17 01:44:34 +02:00
|
|
|
node.InputType, node.OutputType = model.DynamicType, model.DynamicType
|
2020-03-17 04:04:32 +01:00
|
|
|
|
|
|
|
// Find the resource's schema.
|
|
|
|
token, tokenRange := getResourceToken(node)
|
2020-04-07 04:43:16 +02:00
|
|
|
pkg, module, name, diagnostics := DecomposeToken(token, tokenRange)
|
2020-03-17 04:04:32 +01:00
|
|
|
if diagnostics.HasErrors() {
|
|
|
|
return diagnostics
|
|
|
|
}
|
|
|
|
|
2020-04-07 04:43:16 +02:00
|
|
|
isProvider := false
|
|
|
|
if pkg == "pulumi" && module == "providers" {
|
|
|
|
pkg, isProvider = name, true
|
|
|
|
}
|
|
|
|
|
2020-03-17 04:04:32 +01:00
|
|
|
pkgSchema, ok := b.packageSchemas[pkg]
|
|
|
|
if !ok {
|
|
|
|
return hcl.Diagnostics{unknownPackage(pkg, tokenRange)}
|
|
|
|
}
|
|
|
|
|
2020-04-07 04:43:16 +02:00
|
|
|
var inputProperties, properties []*schema.Property
|
|
|
|
if !isProvider {
|
|
|
|
res, ok := pkgSchema.resources[token]
|
|
|
|
if !ok {
|
|
|
|
canon := canonicalizeToken(token, pkgSchema.schema)
|
|
|
|
if res, ok = pkgSchema.resources[canon]; ok {
|
|
|
|
token = canon
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
return hcl.Diagnostics{unknownResourceType(token, tokenRange)}
|
|
|
|
}
|
|
|
|
inputProperties, properties = res.InputProperties, res.Properties
|
|
|
|
} else {
|
|
|
|
inputProperties, properties = pkgSchema.schema.Config, pkgSchema.schema.Config
|
2020-03-17 04:04:32 +01:00
|
|
|
}
|
|
|
|
node.Token = token
|
|
|
|
|
|
|
|
// Create input and output types for the schema.
|
2020-04-07 04:43:16 +02:00
|
|
|
inputType := model.InputType(schemaTypeToType(&schema.ObjectType{Properties: inputProperties}))
|
2020-03-17 04:04:32 +01:00
|
|
|
|
|
|
|
outputProperties := map[string]model.Type{
|
|
|
|
"id": model.NewOutputType(model.StringType),
|
|
|
|
"urn": model.NewOutputType(model.StringType),
|
|
|
|
}
|
2020-04-07 04:43:16 +02:00
|
|
|
for _, prop := range properties {
|
2020-03-17 04:04:32 +01:00
|
|
|
outputProperties[prop.Name] = model.NewOutputType(schemaTypeToType(prop.Type))
|
|
|
|
}
|
|
|
|
outputType := model.NewObjectType(outputProperties)
|
|
|
|
|
2020-04-17 01:44:34 +02:00
|
|
|
node.InputType, node.OutputType = inputType, outputType
|
2020-03-17 04:04:32 +01:00
|
|
|
return diagnostics
|
|
|
|
}
|
|
|
|
|
2020-04-07 04:43:16 +02:00
|
|
|
type resourceScopes struct {
|
|
|
|
root *model.Scope
|
|
|
|
withRange *model.Scope
|
|
|
|
resource *Resource
|
|
|
|
}
|
|
|
|
|
|
|
|
func newResourceScopes(root *model.Scope, resource *Resource, rangeKey, rangeValue model.Type) model.Scopes {
|
|
|
|
scopes := &resourceScopes{
|
|
|
|
root: root,
|
|
|
|
withRange: root,
|
|
|
|
resource: resource,
|
|
|
|
}
|
2020-04-17 01:44:34 +02:00
|
|
|
if rangeValue != nil {
|
|
|
|
properties := map[string]model.Type{
|
|
|
|
"value": rangeValue,
|
|
|
|
}
|
|
|
|
if rangeKey != nil {
|
|
|
|
properties["key"] = rangeKey
|
|
|
|
}
|
|
|
|
|
2020-04-07 04:43:16 +02:00
|
|
|
scopes.withRange = root.Push(syntax.None)
|
|
|
|
scopes.withRange.Define("range", &model.Variable{
|
2020-04-17 01:44:34 +02:00
|
|
|
Name: "range",
|
|
|
|
VariableType: model.NewObjectType(properties),
|
2020-04-07 04:43:16 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
return scopes
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *resourceScopes) GetScopesForBlock(block *hclsyntax.Block) (model.Scopes, hcl.Diagnostics) {
|
|
|
|
if block.Type == "options" {
|
|
|
|
return &optionsScopes{root: s.root, resource: s.resource}, nil
|
|
|
|
}
|
|
|
|
return model.StaticScope(s.withRange), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *resourceScopes) GetScopeForAttribute(attr *hclsyntax.Attribute) (*model.Scope, hcl.Diagnostics) {
|
|
|
|
return s.withRange, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type optionsScopes struct {
|
|
|
|
root *model.Scope
|
|
|
|
resource *Resource
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *optionsScopes) GetScopesForBlock(block *hclsyntax.Block) (model.Scopes, hcl.Diagnostics) {
|
|
|
|
return model.StaticScope(s.root), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *optionsScopes) GetScopeForAttribute(attr *hclsyntax.Attribute) (*model.Scope, hcl.Diagnostics) {
|
|
|
|
if attr.Name == "ignoreChanges" {
|
|
|
|
obj, ok := s.resource.InputType.(*model.ObjectType)
|
|
|
|
if !ok {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
scope := model.NewRootScope(syntax.None)
|
|
|
|
for k, t := range obj.Properties {
|
|
|
|
scope.Define(k, &ResourceProperty{
|
|
|
|
Path: hcl.Traversal{hcl.TraverseRoot{Name: k}},
|
|
|
|
PropertyType: t,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return scope, nil
|
|
|
|
}
|
|
|
|
return s.root, nil
|
|
|
|
}
|
|
|
|
|
2020-03-17 04:04:32 +01:00
|
|
|
// bindResourceBody binds the body of a resource.
|
|
|
|
func (b *binder) bindResourceBody(node *Resource) hcl.Diagnostics {
|
|
|
|
var diagnostics hcl.Diagnostics
|
|
|
|
|
2020-04-07 04:43:16 +02:00
|
|
|
// If the resource has a range option, we need to know the type of the collection being ranged over. Pre-bind the
|
|
|
|
// range expression now, but ignore the diagnostics.
|
2020-04-17 01:44:34 +02:00
|
|
|
node.VariableType = node.OutputType
|
2020-04-07 04:43:16 +02:00
|
|
|
var rangeKey, rangeValue model.Type
|
2020-04-17 01:44:34 +02:00
|
|
|
for _, block := range node.Syntax.Body.Blocks {
|
|
|
|
if block.Type == "options" {
|
|
|
|
if rng, hasRange := block.Body.Attributes["range"]; hasRange {
|
2020-04-17 17:24:44 +02:00
|
|
|
expr, _ := model.BindExpression(rng.Expr, b.root, b.tokens, b.options...)
|
2020-04-17 01:44:34 +02:00
|
|
|
switch {
|
|
|
|
case model.InputType(model.BoolType).ConversionFrom(expr.Type()) == model.SafeConversion:
|
|
|
|
node.VariableType = model.NewOptionalType(node.VariableType)
|
|
|
|
case model.InputType(model.NumberType).ConversionFrom(expr.Type()) != model.NoConversion:
|
|
|
|
rangeValue = model.NumberType
|
|
|
|
node.VariableType = model.NewListType(node.VariableType)
|
|
|
|
default:
|
|
|
|
rangeKey, rangeValue, diagnostics = model.GetCollectionTypes(expr.Type(), rng.Range())
|
|
|
|
node.VariableType = model.NewListType(node.VariableType)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-07 04:43:16 +02:00
|
|
|
}
|
|
|
|
|
2020-03-17 04:04:32 +01:00
|
|
|
// Bind the resource's body.
|
2020-04-17 17:24:44 +02:00
|
|
|
scopes := newResourceScopes(b.root, node, rangeKey, rangeValue)
|
|
|
|
block, blockDiags := model.BindBlock(node.Syntax, scopes, b.tokens, b.options...)
|
2020-03-17 04:04:32 +01:00
|
|
|
diagnostics = append(diagnostics, blockDiags...)
|
|
|
|
|
2020-04-07 04:43:16 +02:00
|
|
|
var options *model.Block
|
2020-03-17 04:04:32 +01:00
|
|
|
for _, item := range block.Body.Items {
|
|
|
|
switch item := item.(type) {
|
|
|
|
case *model.Attribute:
|
|
|
|
node.Inputs = append(node.Inputs, item)
|
|
|
|
case *model.Block:
|
|
|
|
switch item.Type {
|
|
|
|
case "options":
|
2020-04-07 04:43:16 +02:00
|
|
|
if options != nil {
|
|
|
|
diagnostics = append(diagnostics, duplicateBlock(item.Type, item.Syntax.TypeRange))
|
|
|
|
} else {
|
|
|
|
options = item
|
|
|
|
}
|
2020-03-17 04:04:32 +01:00
|
|
|
default:
|
|
|
|
diagnostics = append(diagnostics, unsupportedBlock(item.Type, item.Syntax.TypeRange))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Typecheck the attributes.
|
|
|
|
if objectType, ok := node.InputType.(*model.ObjectType); ok {
|
|
|
|
attrNames := codegen.StringSet{}
|
|
|
|
for _, attr := range node.Inputs {
|
|
|
|
attrNames.Add(attr.Name)
|
|
|
|
|
|
|
|
if typ, ok := objectType.Properties[attr.Name]; ok {
|
|
|
|
if !typ.ConversionFrom(attr.Value.Type()).Exists() {
|
|
|
|
diagnostics = append(diagnostics, model.ExprNotConvertible(typ, attr.Value))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
diagnostics = append(diagnostics, unsupportedAttribute(attr.Name, attr.Syntax.NameRange))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, k := range codegen.SortedKeys(objectType.Properties) {
|
|
|
|
if !model.IsOptionalType(objectType.Properties[k]) && !attrNames.Has(k) {
|
|
|
|
diagnostics = append(diagnostics, missingRequiredAttribute(k, node.Body.Syntax.MissingItemRange()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-07 04:43:16 +02:00
|
|
|
// Typecheck the options block.
|
|
|
|
if options != nil {
|
|
|
|
resourceOptions := &ResourceOptions{}
|
|
|
|
for _, item := range options.Body.Items {
|
|
|
|
switch item := item.(type) {
|
|
|
|
case *model.Attribute:
|
|
|
|
var t model.Type
|
|
|
|
switch item.Name {
|
|
|
|
case "range":
|
2020-04-17 01:44:34 +02:00
|
|
|
t = model.NewUnionType(model.BoolType, model.NumberType, model.NewListType(model.DynamicType),
|
|
|
|
model.NewMapType(model.DynamicType))
|
2020-04-07 04:43:16 +02:00
|
|
|
resourceOptions.Range = item.Value
|
|
|
|
case "provider":
|
2020-04-17 01:44:34 +02:00
|
|
|
t = model.DynamicType
|
2020-04-07 04:43:16 +02:00
|
|
|
resourceOptions.Provider = item.Value
|
|
|
|
case "dependsOn":
|
|
|
|
t = model.NewListType(model.DynamicType)
|
|
|
|
resourceOptions.DependsOn = item.Value
|
|
|
|
case "protect":
|
|
|
|
t = model.BoolType
|
|
|
|
resourceOptions.Protect = item.Value
|
|
|
|
case "ignoreChanges":
|
|
|
|
t = model.NewListType(ResourcePropertyType)
|
|
|
|
resourceOptions.IgnoreChanges = item.Value
|
|
|
|
default:
|
|
|
|
diagnostics = append(diagnostics, unsupportedAttribute(item.Name, item.Syntax.NameRange))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if model.InputType(t).ConversionFrom(item.Value.Type()) == model.NoConversion {
|
|
|
|
diagnostics = append(diagnostics, model.ExprNotConvertible(model.InputType(t), item.Value))
|
|
|
|
}
|
|
|
|
case *model.Block:
|
|
|
|
diagnostics = append(diagnostics, unsupportedBlock(item.Type, item.Syntax.TypeRange))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
node.Options = resourceOptions
|
|
|
|
}
|
2020-03-17 04:04:32 +01:00
|
|
|
|
|
|
|
return diagnostics
|
|
|
|
}
|