pulumi/pkg/codegen/pcl/binder_component.go

242 lines
7.2 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 pcl
import (
"fmt"
"os"
"path/filepath"
"github.com/hashicorp/hcl/v2"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
hclsyntax "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
)
// componentVariableType returns the type of the variable of which the value is a component.
// The type is derived from the outputs of the sub-program of the component into an ObjectType
func componentVariableType(program *Program) model.Type {
properties := map[string]model.Type{}
for _, node := range program.Nodes {
switch node := node.(type) {
case *OutputVariable:
switch nodeType := node.Type().(type) {
case *model.OutputType:
// if the output variable is already an Output<T>, keep it as is
properties[node.LogicalName()] = nodeType
default:
// otherwise, wrap it as an output
properties[node.LogicalName()] = &model.OutputType{
ElementType: nodeType,
}
}
}
}
return &model.ObjectType{Properties: properties}
}
type componentInput struct {
key string
required bool
}
func componentInputs(program *Program) map[string]componentInput {
inputs := map[string]componentInput{}
for _, node := range program.Nodes {
switch node := node.(type) {
case *ConfigVariable:
inputs[node.LogicalName()] = componentInput{
required: node.DefaultValue == nil,
key: node.LogicalName(),
}
}
}
return inputs
}
func contains(slice []string, item string) bool {
set := make(map[string]struct{}, len(slice))
for _, s := range slice {
set[s] = struct{}{}
}
_, ok := set[item]
return ok
}
// ComponentProgramBinderFromFileSystem returns the default component program binder which uses the file system
// to parse and bind PCL files into a program.
func ComponentProgramBinderFromFileSystem() ComponentProgramBinder {
return func(args ComponentProgramBinderArgs) (*Program, hcl.Diagnostics, error) {
var diagnostics hcl.Diagnostics
binderDirPath := args.binderDirPath
componentSource := args.componentSource
nodeRange := args.componentNodeRange
loader := args.binderLoader
// bind the component here as if it was a new program
// this becomes the DirPath for the new binder
componentSourceDir := filepath.Join(binderDirPath, componentSource)
parser := hclsyntax.NewParser()
// Load all .pp files in the components' directory
files, err := os.ReadDir(componentSourceDir)
if err != nil {
diagnostics = diagnostics.Append(errorf(nodeRange, err.Error()))
return nil, diagnostics, nil
}
if len(files) == 0 {
diagnostics = diagnostics.Append(errorf(nodeRange, err.Error()))
return nil, diagnostics, nil
}
for _, file := range files {
if file.IsDir() {
continue
}
fileName := file.Name()
path := filepath.Join(componentSourceDir, fileName)
if filepath.Ext(fileName) == ".pp" {
file, err := os.Open(path)
if err != nil {
diagnostics = diagnostics.Append(errorf(nodeRange, err.Error()))
return nil, diagnostics, err
}
err = parser.ParseFile(file,
fmt.Sprintf("%s/%s", filepath.Base(componentSourceDir), filepath.Base(path)))
if err != nil {
diagnostics = diagnostics.Append(errorf(nodeRange, err.Error()))
return nil, diagnostics, err
}
diags := parser.Diagnostics
if diags.HasErrors() {
return nil, diagnostics, err
}
}
}
if err != nil {
diagnostics = diagnostics.Append(errorf(nodeRange, err.Error()))
return nil, diagnostics, err
}
componentProgram, programDiags, err := BindProgram(parser.Files,
Loader(loader),
DirPath(componentSourceDir),
ComponentBinder(ComponentProgramBinderFromFileSystem()))
return componentProgram, programDiags, err
}
}
func (b *binder) bindComponent(node *Component) hcl.Diagnostics {
block, diagnostics := model.BindBlock(node.syntax, model.StaticScope(b.root), b.tokens, b.options.modelOptions()...)
node.Definition = block
// check we can use components and load the program
if b.options.dirPath == "" {
diagnostics = diagnostics.Append(errorf(node.SyntaxNode().Range(),
"components require the binder to have set a directory path"))
return diagnostics
}
if b.options.componentProgramBinder == nil {
diagnostics = diagnostics.Append(errorf(node.SyntaxNode().Range(),
"components require the binder to have set the component program binder"))
return diagnostics
}
componentProgram, programDiags, err := b.options.componentProgramBinder(ComponentProgramBinderArgs{
binderLoader: b.options.loader,
binderDirPath: b.options.dirPath,
componentSource: node.source,
componentNodeRange: node.SyntaxNode().Range(),
})
if err != nil {
diagnostics = diagnostics.Append(errorf(node.SyntaxNode().Range(), err.Error()))
return diagnostics
}
if programDiags.HasErrors() || componentProgram == nil {
diagnostics = diagnostics.Append(errorf(node.SyntaxNode().Range(), programDiags.Error()))
return diagnostics
}
node.Program = componentProgram
node.VariableType = componentVariableType(componentProgram)
node.dirPath = filepath.Join(b.options.dirPath, node.source)
componentInputs := componentInputs(componentProgram)
providedInputs := []string{}
var options *model.Block
for _, item := range block.Body.Items {
switch item := item.(type) {
case *model.Attribute:
// logical name is a special attribute
if item.Name == LogicalNamePropertyKey {
logicalName, lDiags := getStringAttrValue(item)
if lDiags != nil {
diagnostics = diagnostics.Append(lDiags)
} else {
node.logicalName = logicalName
}
continue
}
// all other attributes are part of the inputs
_, knownInput := componentInputs[item.Name]
if !knownInput {
diagnostics = append(diagnostics, unsupportedAttribute(item.Name, item.Syntax.NameRange))
return diagnostics
}
node.Inputs = append(node.Inputs, item)
providedInputs = append(providedInputs, item.Name)
case *model.Block:
switch item.Type {
case "options":
if options != nil {
diagnostics = append(diagnostics, duplicateBlock(item.Type, item.Syntax.TypeRange))
} else {
options = item
}
default:
diagnostics = append(diagnostics, unsupportedBlock(item.Type, item.Syntax.TypeRange))
}
}
}
// check that all required inputs are actually set
for inputKey, componentInput := range componentInputs {
if componentInput.required && !contains(providedInputs, inputKey) {
diagnostics = append(diagnostics, missingRequiredAttribute(inputKey, node.SyntaxNode().Range()))
}
}
if options != nil {
resourceOptions, optionsDiags := bindResourceOptions(options)
diagnostics = append(diagnostics, optionsDiags...)
node.Options = resourceOptions
}
return diagnostics
}