// 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 importer import ( "context" "encoding/json" "io" "testing" "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" "github.com/pulumi/pulumi/pkg/v3/codegen/pcl" "github.com/pulumi/pulumi/pkg/v3/codegen/schema" "github.com/pulumi/pulumi/pkg/v3/codegen/testing/utils" "github.com/pulumi/pulumi/pkg/v3/resource/stack" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/config" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "github.com/stretchr/testify/assert" ) func TestGenerateLanguageDefinition(t *testing.T) { t.Parallel() loader := schema.NewPluginLoader(utils.NewHost(testdataPath)) cases, err := readTestCases("testdata/cases.json") if !assert.NoError(t, err) { t.Fatal() } //nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg for _, s := range cases.Resources { s := s t.Run(string(s.URN), func(t *testing.T) { t.Parallel() state, err := stack.DeserializeResource(s, config.NopDecrypter, config.NopEncrypter) if !assert.NoError(t, err) { t.Fatal() } var actualState *resource.State err = GenerateLanguageDefinitions(io.Discard, loader, func(_ io.Writer, p *pcl.Program) error { if !assert.Len(t, p.Nodes, 1) { t.Fatal() } res, isResource := p.Nodes[0].(*pcl.Resource) if !assert.True(t, isResource) { t.Fatal() } actualState = renderResource(t, res) return nil }, []*resource.State{state}, names) if !assert.NoError(t, err) { t.Fatal() } assert.Equal(t, state.Type, actualState.Type) assert.Equal(t, state.URN, actualState.URN) assert.Equal(t, state.Parent, actualState.Parent) assert.Equal(t, state.Provider, actualState.Provider) assert.Equal(t, state.Protect, actualState.Protect) if !assert.True(t, actualState.Inputs.DeepEquals(state.Inputs)) { actual, err := stack.SerializeResource(context.Background(), actualState, config.NopEncrypter, false) contract.IgnoreError(err) sb, err := json.MarshalIndent(s, "", " ") contract.IgnoreError(err) ab, err := json.MarshalIndent(actual, "", " ") contract.IgnoreError(err) t.Logf("%v\n\n%v\n", string(sb), string(ab)) } }) } } func TestGenerateLanguageDefinitionsRetriesCodegenWhenEncounteringCircularReferences(t *testing.T) { t.Parallel() loader := schema.NewPluginLoader(utils.NewHost(testdataPath)) generatedProgram := "" generator := func(_ io.Writer, p *pcl.Program) error { for _, content := range p.Source() { generatedProgram += content } return nil } // Create a circular reference between two resources. // In this case, generating the PCL would fail with a circular reference error but we retry the codegen // without guessing the dependencies between the resources. resources := []apitype.ResourceV3{ { URN: "urn:pulumi:stack::project::aws:s3/bucketObject:BucketObject::first", ID: "bucket-object-1", Custom: true, Type: "aws:s3/bucketObject:BucketObject", Inputs: map[string]interface{}{ "bucket": "bucket-object-2", }, }, { URN: "urn:pulumi:stack::project::aws:s3/bucketObject:BucketObject::second", ID: "bucket-object-2", Custom: true, Type: "aws:s3/bucketObject:BucketObject", Inputs: map[string]interface{}{ "bucket": "bucket-object-1", }, }, } states := make([]*resource.State, 0) for _, r := range resources { state, err := stack.DeserializeResource(r, config.NopDecrypter, config.NopEncrypter) if !assert.NoError(t, err) { t.Fatal() } states = append(states, state) } var names NameTable err := GenerateLanguageDefinitions(io.Discard, loader, generator, states, names) assert.NoError(t, err) // notice here the generated program doesn't have any references because // we retried the codegen without guessing the dependencies between the resources. expectedCode := `resource first "aws:s3/bucketObject:BucketObject" { bucket = "bucket-object-2" } resource second "aws:s3/bucketObject:BucketObject" { bucket = "bucket-object-1" } ` assert.Equal(t, expectedCode, generatedProgram) } func TestGenerateLanguageDefinitionsAllowsGeneratingParentVariables(t *testing.T) { t.Parallel() loader := schema.NewPluginLoader(utils.NewHost(testdataPath)) generatedProgram := "" generator := func(_ io.Writer, p *pcl.Program) error { for _, content := range p.Source() { generatedProgram += content } return nil } componentURN := resource.NewURN("dev", "project", "", "example:index:MyComponent", "example") childURN := resource.NewURN( "dev", "project", "example:index:MyComponent", "random:index/randomPet:RandomPet", "randomPet") nameTable := NameTable{ componentURN: "parentComponent", } resources := []apitype.ResourceV3{ { URN: childURN, Custom: true, Type: "random:index/randomPet:RandomPet", Parent: componentURN, }, } states := make([]*resource.State, 0) for _, r := range resources { state, err := stack.DeserializeResource(r, config.NopDecrypter, config.NopEncrypter) if !assert.NoError(t, err) { t.Fatal() } states = append(states, state) } err := GenerateLanguageDefinitions(io.Discard, loader, generator, states, nameTable) assert.NoError(t, err) expectedCode := `resource randomPet "random:index/randomPet:RandomPet" { options { parent = parentComponent } } ` assert.Equal(t, expectedCode, generatedProgram) }