package pcl_test import ( "bytes" "os" "path/filepath" "testing" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "github.com/spf13/afero" "github.com/hashicorp/hcl/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" "github.com/pulumi/pulumi/pkg/v3/codegen/pcl" "github.com/pulumi/pulumi/pkg/v3/codegen/schema" "github.com/pulumi/pulumi/pkg/v3/codegen/testing/test" "github.com/pulumi/pulumi/pkg/v3/codegen/testing/utils" ) var testdataPath = filepath.Join("..", "testing", "test", "testdata") func TestBindProgram(t *testing.T) { t.Parallel() testdata, err := os.ReadDir(testdataPath) if err != nil { t.Fatalf("could not read test data: %v", err) } bindOptions := map[string][]pcl.BindOption{} for _, r := range test.PulumiPulumiProgramTests { bindOptions[r.Directory+"-pp"] = r.BindOptions } //nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg for _, v := range testdata { v := v if !v.IsDir() { continue } folderPath := filepath.Join(testdataPath, v.Name()) files, err := os.ReadDir(folderPath) if err != nil { t.Fatalf("could not read test data: %v", err) } for _, fileName := range files { fileName := fileName.Name() if filepath.Ext(fileName) != ".pp" { continue } t.Run(fileName, func(t *testing.T) { t.Parallel() path := filepath.Join(folderPath, fileName) contents, err := os.ReadFile(path) require.NoErrorf(t, err, "could not read %v", path) parser := syntax.NewParser() err = parser.ParseFile(bytes.NewReader(contents), fileName) require.NoErrorf(t, err, "could not read %v", path) require.False(t, parser.Diagnostics.HasErrors(), "failed to parse files") var bindError error var diags hcl.Diagnostics loader := pcl.Loader(schema.NewPluginLoader(utils.NewHost(testdataPath))) absoluteFolderPath, err := filepath.Abs(folderPath) if err != nil { t.Fatalf("failed to bind program: unable to find the absolute path of %v", folderPath) } options := append( bindOptions[v.Name()], loader, pcl.DirPath(absoluteFolderPath), pcl.ComponentBinder(pcl.ComponentProgramBinderFromFileSystem())) // PCL binder options are taken from program_driver.go program, diags, bindError := pcl.BindProgram(parser.Files, options...) assert.NoError(t, bindError) if diags.HasErrors() || program == nil { t.Fatalf("failed to bind program: %v", diags) } }) } } } func TestWritingProgramSource(t *testing.T) { t.Parallel() // STEP 1: Bind the program from {test-data}/components componentsDir := "components-pp" folderPath := filepath.Join(testdataPath, componentsDir) files, err := os.ReadDir(folderPath) if err != nil { t.Fatalf("could not read test data: %v", err) } parser := syntax.NewParser() for _, fileName := range files { fileName := fileName.Name() if filepath.Ext(fileName) != ".pp" { continue } path := filepath.Join(folderPath, fileName) contents, err := os.ReadFile(path) require.NoErrorf(t, err, "could not read %v", path) err = parser.ParseFile(bytes.NewReader(contents), fileName) require.NoErrorf(t, err, "could not read %v", path) require.False(t, parser.Diagnostics.HasErrors(), "failed to parse files") } var bindError error var diags hcl.Diagnostics absoluteProgramPath, err := filepath.Abs(folderPath) if err != nil { t.Fatalf("failed to bind program: unable to find the absolute path of %v", folderPath) } program, diags, bindError := pcl.BindProgram(parser.Files, pcl.Loader(schema.NewPluginLoader(utils.NewHost(testdataPath))), pcl.DirPath(absoluteProgramPath), pcl.ComponentBinder(pcl.ComponentProgramBinderFromFileSystem())) assert.NoError(t, bindError) if diags.HasErrors() || program == nil { t.Fatalf("failed to bind program: %v", diags) } // STEP 2: assert the resulting files fs := afero.NewMemMapFs() writingFilesError := program.WriteSource(fs) assert.NoError(t, writingFilesError, "failed to write source files") // Assert main file exists mainFileExists, err := afero.Exists(fs, "/components.pp") assert.NoError(t, err, "failed to get the main file") assert.True(t, mainFileExists, "main program file should exist at the root") // Assert directories "simpleComponent" and "exampleComponent" are present simpleComponentDirExists, err := afero.DirExists(fs, "/simpleComponent") assert.NoError(t, err, "failed to get the simple component dir") assert.True(t, simpleComponentDirExists, "simple component dir exists") exampleComponentDirExists, err := afero.DirExists(fs, "/exampleComponent") assert.NoError(t, err, "failed to get the example component dir") assert.True(t, exampleComponentDirExists, "example component dir exists") // Assert simpleComponent/main.pp and exampleComponent/main.pp exist simpleMainExists, err := afero.Exists(fs, "/simpleComponent/main.pp") assert.NoError(t, err, "failed to get the main file of simple component") assert.True(t, simpleMainExists, "main program file of simple component should exist") exampleMainExists, err := afero.Exists(fs, "/exampleComponent/main.pp") assert.NoError(t, err, "failed to get the main file of example component") assert.True(t, exampleMainExists, "main program file of example component should exist") } func TestConfigNodeTypedString(t *testing.T) { t.Parallel() source := "config cidrBlock string { }" program, diags, err := ParseAndBindProgram(t, source, "config.pp") if err != nil { t.Fatalf("could not bind program: %v", err) } contract.Ignore(diags) assert.NotNil(t, program, "failed to parse and bind program") assert.Equal(t, len(program.Nodes), 1, "there is one node") config, ok := program.Nodes[0].(*pcl.ConfigVariable) assert.True(t, ok, "first node is a config variable") assert.Equal(t, config.Name(), "cidrBlock") assert.Equal(t, config.Type(), model.StringType, "the type is a string") } func TestConfigNodeTypedOptionalString(t *testing.T) { t.Parallel() source := "config cidrBlock string { default = null }" program, diags, err := ParseAndBindProgram(t, source, "config.pp") require.NoError(t, err) contract.Ignore(diags) assert.NotNil(t, program, "failed to parse and bind program") assert.Equal(t, len(program.Nodes), 1, "there is one node") config, ok := program.Nodes[0].(*pcl.ConfigVariable) assert.True(t, ok, "first node is a config variable") assert.Equal(t, config.Name(), "cidrBlock") assert.True(t, model.IsOptionalType(config.Type()), "the type is optional") elementType := pcl.UnwrapOption(config.Type()) assert.Equal(t, elementType, model.StringType, "element type is a string") assert.True(t, config.Nullable, "The config variable is nullable") } func TestConfigNodeTypedInt(t *testing.T) { t.Parallel() source := "config count int { }" program, diags, err := ParseAndBindProgram(t, source, "config.pp") require.NoError(t, err) contract.Ignore(diags) assert.NotNil(t, program, "failed to parse and bind program") assert.Equal(t, len(program.Nodes), 1, "there is one node") config, ok := program.Nodes[0].(*pcl.ConfigVariable) assert.True(t, ok, "first node is a config variable") assert.Equal(t, config.Name(), "count") assert.Equal(t, config.Type(), model.IntType, "the type is a string") } func TestConfigNodeTypedStringList(t *testing.T) { t.Parallel() source := "config names \"list(string)\" { }" program, diags, err := ParseAndBindProgram(t, source, "config.pp") require.NoError(t, err) contract.Ignore(diags) assert.NotNil(t, program, "failed to parse and bind program") assert.Equal(t, len(program.Nodes), 1, "there is one node") config, ok := program.Nodes[0].(*pcl.ConfigVariable) assert.True(t, ok, "first node is a config variable") assert.Equal(t, config.Name(), "names") listType, ok := config.Type().(*model.ListType) assert.True(t, ok, "the type of config is a list type") assert.Equal(t, listType.ElementType, model.StringType, "the element type is a string") } func TestConfigNodeTypedIntList(t *testing.T) { t.Parallel() source := "config names \"list(int)\" { }" program, diags, err := ParseAndBindProgram(t, source, "config.pp") require.NoError(t, err) contract.Ignore(diags) assert.NotNil(t, program, "failed to parse and bind program") assert.Equal(t, len(program.Nodes), 1, "there is one node") config, ok := program.Nodes[0].(*pcl.ConfigVariable) assert.True(t, ok, "first node is a config variable") assert.Equal(t, config.Name(), "names") listType, ok := config.Type().(*model.ListType) assert.True(t, ok, "the type of config is a list type") assert.Equal(t, listType.ElementType, model.IntType, "the element type is an int") } func TestConfigNodeTypedStringMap(t *testing.T) { t.Parallel() source := "config names \"map(string)\" { }" program, diags, err := ParseAndBindProgram(t, source, "config.pp") require.NoError(t, err) contract.Ignore(diags) assert.NotNil(t, program, "failed to parse and bind program") assert.Equal(t, len(program.Nodes), 1, "there is one node") config, ok := program.Nodes[0].(*pcl.ConfigVariable) assert.True(t, ok, "first node is a config variable") assert.Equal(t, config.Name(), "names") mapType, ok := config.Type().(*model.MapType) assert.True(t, ok, "the type of config is a map type") assert.Equal(t, mapType.ElementType, model.StringType, "the element type is a string") } func TestConfigNodeTypedIntMap(t *testing.T) { t.Parallel() source := "config names \"map(int)\" { }" program, diags, err := ParseAndBindProgram(t, source, "config.pp") require.NoError(t, err) contract.Ignore(diags) assert.NotNil(t, program, "failed to parse and bind program") assert.Equal(t, len(program.Nodes), 1, "there is one node") config, ok := program.Nodes[0].(*pcl.ConfigVariable) assert.True(t, ok, "first node is a config variable") assert.Equal(t, config.Name(), "names") mapType, ok := config.Type().(*model.MapType) assert.True(t, ok, "the type of config is a map type") assert.Equal(t, mapType.ElementType, model.IntType, "the element type is an int") } func TestConfigNodeTypedAnyMap(t *testing.T) { t.Parallel() source := "config names \"map(any)\" { }" program, diags, err := ParseAndBindProgram(t, source, "config.pp") require.NoError(t, err) contract.Ignore(diags) assert.NotNil(t, program, "failed to parse and bind program") assert.Equal(t, len(program.Nodes), 1, "there is one node") config, ok := program.Nodes[0].(*pcl.ConfigVariable) assert.True(t, ok, "first node is a config variable") assert.Equal(t, config.Name(), "names") mapType, ok := config.Type().(*model.MapType) assert.True(t, ok, "the type of config is a map type") assert.Equal(t, mapType.ElementType, model.DynamicType, "the element type is a dynamic") } func TestOutputsCanHaveSameNameAsOtherNodes(t *testing.T) { t.Parallel() // here we have an output with the same name as a config variable // this should bind and type-check just fine source := ` config cidrBlock string { } output cidrBlock { value = cidrBlock } ` program, diags, err := ParseAndBindProgram(t, source, "config.pp") require.NoError(t, err) assert.Equal(t, 0, len(diags), "There are no diagnostics") assert.NotNil(t, program) } func TestUsingDynamicConfigAsRange(t *testing.T) { t.Parallel() source := ` config "endpointsServiceNames" { description = "Information about the VPC endpoints to create." } config "vpcId" "int" { description = "The ID of the VPC" } resource "endpoint" "aws:ec2/vpcEndpoint:VpcEndpoint" { options { range = endpointsServiceNames } vpcId = vpcId serviceName = range.value.name vpcEndpointType = range.value.type privateDnsEnabled = range.value.privateDns } ` program, diags, err := ParseAndBindProgram(t, source, "config.pp") require.NoError(t, err) assert.Equal(t, 0, len(diags), "There are no diagnostics") assert.NotNil(t, program) } func TestLengthFunctionCanBeUsedWithDynamic(t *testing.T) { t.Parallel() source := ` config "data" "object({ lambda=object({ subnetIds=list(string) }) })" { } output "numberOfEndpoints" { value = length(data.lambda.subnetIds) } ` program, diags, err := ParseAndBindProgram(t, source, "config.pp") require.NoError(t, err) assert.Equal(t, 0, len(diags), "There are no diagnostics") assert.NotNil(t, program) } func TestBindingUnknownResourceWhenSkippingResourceTypeChecking(t *testing.T) { t.Parallel() source := ` resource provider "pulumi:providers:unknown" { } resource main "unknown:index:main" { first = "hello" second = { foo = "bar" } } resource fromModule "unknown:eks:example" { options { range = 10 } associatedMain = main.id anotherValue = main.unknown } output "mainId" { value = main.id } output "values" { value = fromModule.values.first }` lenientProgram, lenientDiags, lenientError := ParseAndBindProgram(t, source, "prog.pp", pcl.SkipResourceTypechecking) require.NoError(t, lenientError) assert.False(t, lenientDiags.HasErrors(), "There are no errors") assert.NotNil(t, lenientProgram) strictProgram, _, strictError := ParseAndBindProgram(t, source, "program.pp") assert.NotNil(t, strictError, "Binding fails in strict mode") assert.Nil(t, strictProgram) } func TestBindingUnknownResourceFromKnownSchemaWhenSkippingResourceTypeChecking(t *testing.T) { t.Parallel() // here the random package is available, but it doesn't have a resource called "Unknown" source := ` resource main "random:index:unknown" { first = "hello" second = { foo = "bar" } } output "mainId" { value = main.id }` lenientProgram, lenientDiags, lenientError := ParseAndBindProgram(t, source, "prog.pp", pcl.SkipResourceTypechecking) require.NoError(t, lenientError) assert.False(t, lenientDiags.HasErrors(), "There are no errors") assert.NotNil(t, lenientProgram) strictProgram, _, strictError := ParseAndBindProgram(t, source, "program.pp") assert.NotNil(t, strictError, "Binding fails in strict mode") assert.Nil(t, strictProgram) } func TestBindingUnknownPropertyFromKnownResourceWhenSkippingResourceTypeChecking(t *testing.T) { t.Parallel() // here the resource declaration is correctly typed but the output `unknownId` references an unknown property // this program binds without errors source := ` resource randomPet "random:index/randomPet:RandomPet" { prefix = "doggo" } output "unknownId" { value = randomPet.unknownProperty } output "knownId" { value = randomPet.id } ` lenientProgram, lenientDiags, lenientError := ParseAndBindProgram(t, source, "prog.pp", pcl.SkipResourceTypechecking) require.NoError(t, lenientError) assert.False(t, lenientDiags.HasErrors(), "There are no errors") assert.NotNil(t, lenientProgram) for _, output := range lenientProgram.OutputVariables() { outputType := model.ResolveOutputs(output.Value.Type()) if output.Name() == "unknownId" { assert.Equal(t, model.DynamicType, outputType) } if output.Name() == "knownId" { assert.Equal(t, model.StringType, outputType) } } strictProgram, _, strictError := ParseAndBindProgram(t, source, "program.pp") assert.NotNil(t, strictError, "Binding fails in strict mode") assert.Nil(t, strictProgram) } func TestAssigningWrongTypeToResourcePropertyWhenSkippingResourceTypeChecking(t *testing.T) { t.Parallel() // here the RandomPet resource expects the prefix property to be of type string // but we assigned to a boolean. It should still bind when using pcl.SkipResourceTypechecking source := ` config data "list(string)" {} resource randomPet "random:index/randomPet:RandomPet" { prefix = data }` lenientProgram, lenientDiags, lenientError := ParseAndBindProgram(t, source, "prog.pp", pcl.SkipResourceTypechecking) require.NoError(t, lenientError) assert.False(t, lenientDiags.HasErrors(), "There are no errors") assert.NotNil(t, lenientProgram) strictProgram, _, strictError := ParseAndBindProgram(t, source, "program.pp") assert.NotNil(t, strictError, "Binding fails in strict mode") assert.Nil(t, strictProgram) } func TestAssigningUnknownPropertyFromKnownResourceWhenSkippingResourceTypeChecking(t *testing.T) { t.Parallel() // here the resource declaration is assigning an unknown property "unknown" which is not part // of the RandomPet inputs. source := ` resource randomPet "random:index/randomPet:RandomPet" { unknown = "doggo" } output "mainId" { value = randomPet.unknownProperty }` lenientProgram, lenientDiags, lenientError := ParseAndBindProgram(t, source, "prog.pp", pcl.SkipResourceTypechecking) require.NoError(t, lenientError) assert.False(t, lenientDiags.HasErrors(), "There are no errors") assert.NotNil(t, lenientProgram) strictProgram, _, strictError := ParseAndBindProgram(t, source, "program.pp") assert.NotNil(t, strictError, "Binding fails in strict mode") assert.Nil(t, strictProgram) } func TestTraversalOfOptionalObject(t *testing.T) { t.Parallel() // foo : Option<{ bar: string }> // assert that foo.bar : Option<string> source := ` config "foo" "object({ bar=string })" { default = null description = "Foo is an optional object because the default is null" } output "fooBar" { value = foo.bar } ` // first assert that binding the program works program, diags, err := ParseAndBindProgram(t, source, "program.pp") require.NoError(t, err) assert.Equal(t, 0, len(diags), "There are no diagnostics") assert.NotNil(t, program) // get the output variable outputVars := program.OutputVariables() assert.Equal(t, 1, len(outputVars), "There is only one output variable") fooBar := outputVars[0] fooBarType := fooBar.Value.Type() assert.True(t, model.IsOptionalType(fooBarType)) unwrappedType := pcl.UnwrapOption(fooBarType) assert.Equal(t, model.StringType, unwrappedType) } func TestBindingInvalidRangeTypeWhenSkippingRangeTypechecking(t *testing.T) { t.Parallel() // here the range function expects a number but we pass a boolean source := ` config "inputRange" "string" { } data = [for x in inputRange : x] resource randomPet "random:index/randomPet:RandomPet" { options { range = inputRange } } ` // usually a string is not a valid range type // but when skipping range typechecking it should still bind lenientProgram, lenientDiags, lenientError := ParseAndBindProgram(t, source, "prog.pp", pcl.SkipRangeTypechecking) require.NoError(t, lenientError) assert.False(t, lenientDiags.HasErrors(), "There are no errors") assert.NotNil(t, lenientProgram) strictProgram, diags, strictError := ParseAndBindProgram(t, source, "program.pp") assert.NotNil(t, strictError, "Binding fails in strict mode") assert.Equal(t, 2, len(diags), "There are two diagnostics") assert.Nil(t, strictProgram) } func TestTransitivePackageReferencesAreLoadedFromTopLevelResourceDefinition(t *testing.T) { t.Parallel() // when binding a resource from a package that has a transitive dependency // then that transitive dependency is part of the program package references. // for example when binding a resource from AWSX package and that resources uses types from the AWS package // then both AWSX and AWS packages are part of the program package references source := `resource "example" "awsx:ecs:EC2Service" { }` program, diags, err := ParseAndBindProgram(t, source, "program.pp", pcl.NonStrictBindOptions()...) require.NoError(t, err) assert.False(t, diags.HasErrors(), "There are no error diagnostics") assert.NotNil(t, program) assert.Equal(t, 2, len(program.PackageReferences()), "There are two package references") packageRefExists := func(pkg string) bool { for _, ref := range program.PackageReferences() { if ref.Name() == pkg { return true } } return false } assert.True(t, packageRefExists("awsx"), "The program has a reference to the awsx package") assert.True(t, packageRefExists("aws"), "The program has a reference to the aws package") } func TestAllowMissingVariablesShouldNotErrorOnUnboundVariableReferences(t *testing.T) { t.Parallel() source := ` resource randomPet "random:index/randomPet:RandomPet" { options { parent = parentComponentVariable } }` program, diags, err := ParseAndBindProgram(t, source, "program.pp", pcl.AllowMissingVariables) require.NoError(t, err) assert.False(t, diags.HasErrors(), "There are no error diagnostics") assert.NotNil(t, program) }