Skip to content

Commit 374049e

Browse files
authored
Implement schema mappings for modules with dashed fields (#427)
* Implement schema mappings for modules with dashed fields * remove terraform artifacts used for debugging
1 parent 8279c90 commit 374049e

File tree

19 files changed

+698
-8
lines changed

19 files changed

+698
-8
lines changed

pkg/modprovider/module.go

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,60 @@ func (h *moduleHandler) prepSandbox(
172172
// which will get further reused for Pulumi URNs.
173173
tfName := getModuleName(urn)
174174

175+
hasOutputFieldMapping := inferredModule != nil &&
176+
inferredModule.SchemaFieldMappings != nil &&
177+
inferredModule.SchemaFieldMappings.OutputFieldMappings != nil
178+
175179
outputSpecs := []tfsandbox.TFOutputSpec{}
176180
for outputName := range inferredModule.Outputs {
181+
if hasOutputFieldMapping {
182+
mappings := inferredModule.SchemaFieldMappings.OutputFieldMappings
183+
if tfName, ok := mappings[outputName]; ok {
184+
outputName = tfName
185+
}
186+
}
187+
177188
outputSpecs = append(outputSpecs, tfsandbox.TFOutputSpec{
178189
Name: tfsandbox.DecodePulumiTopLevelKey(outputName),
179190
})
180191
}
181192

193+
// remap input fields to terraform module inputs
194+
// for example if terraform module input was "input-value" but pulumi input was "input_value",
195+
// then we need to remap it to "input-value" in the tf file.
196+
hasInputFieldMappings := inferredModule != nil &&
197+
inferredModule.SchemaFieldMappings != nil &&
198+
inferredModule.SchemaFieldMappings.InputFieldMappings != nil
199+
200+
if hasInputFieldMappings {
201+
mappings := inferredModule.SchemaFieldMappings.InputFieldMappings
202+
for pulumiInputName, input := range moduleInputs {
203+
if tfName, ok := mappings[pulumiInputName]; ok {
204+
// if the input is mapped, use the mapped name
205+
moduleInputs[tfName] = input
206+
delete(moduleInputs, pulumiInputName)
207+
}
208+
}
209+
}
210+
211+
// remap some required providers in the TF module. For example,
212+
// if the module requires "google-beta", the Pulumi name of the field would be "google_beta"
213+
// so we need to remap it to "google-beta" in the tf file.
214+
hasProviderFieldMappings := inferredModule != nil &&
215+
inferredModule.SchemaFieldMappings != nil &&
216+
inferredModule.SchemaFieldMappings.ProviderFieldMappings != nil
217+
218+
if hasProviderFieldMappings {
219+
mappings := inferredModule.SchemaFieldMappings.ProviderFieldMappings
220+
for providerName, config := range providersConfig {
221+
if tfName, ok := mappings[providerName]; ok {
222+
// if the provider is mapped, use the mapped name
223+
providersConfig[tfName] = config
224+
delete(providersConfig, providerName)
225+
}
226+
}
227+
}
228+
182229
err = tfsandbox.CreateTFFile(tfName, moduleSource,
183230
moduleVersion, tf.WorkingDir(),
184231
moduleInputs, outputSpecs, providersConfig)
@@ -202,7 +249,7 @@ func (h *moduleHandler) prepSandbox(
202249
return tf, nil
203250
}
204251

205-
// This method handles Create and Update in a uniform way; both map to tofu apply operation.
252+
// This method handles Create and Update in a uniform way; both map to tofu/terraform apply operation.
206253
func (h *moduleHandler) applyModuleOperation(
207254
ctx context.Context,
208255
urn urn.URN,
@@ -289,6 +336,22 @@ func (h *moduleHandler) applyModuleOperation(
289336
logger.Log(ctx, tfsandbox.Error, fmt.Sprintf("partial failure in apply: %v", applyErr))
290337
}
291338

339+
hasOutputFieldMappings := inferredModule != nil &&
340+
inferredModule.SchemaFieldMappings != nil &&
341+
inferredModule.SchemaFieldMappings.OutputFieldMappings != nil
342+
343+
if hasOutputFieldMappings {
344+
mappings := inferredModule.SchemaFieldMappings.OutputFieldMappings
345+
for tfName, output := range moduleOutputs {
346+
for pulumiOutputName, mappedTerraformName := range mappings {
347+
if tfName == mappedTerraformName {
348+
moduleOutputs[pulumiOutputName] = output
349+
delete(moduleOutputs, tfName)
350+
}
351+
}
352+
}
353+
}
354+
292355
return moduleOutputs, views, applyErr
293356
}
294357

pkg/modprovider/tfmodules.go

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,20 @@ func parseModuleSchemaOverrides(packageName string) []*ModuleSchemaOverride {
9292
return overrides
9393
}
9494

95+
type SchemaFieldMappings struct {
96+
ProviderFieldMappings map[string]string
97+
InputFieldMappings map[resource.PropertyKey]resource.PropertyKey
98+
OutputFieldMappings map[resource.PropertyKey]resource.PropertyKey
99+
}
100+
95101
type InferredModuleSchema struct {
96-
Inputs map[resource.PropertyKey]*schema.PropertySpec `json:"inputs"`
97-
Outputs map[resource.PropertyKey]*schema.PropertySpec `json:"outputs"`
98-
SupportingTypes map[string]*schema.ComplexTypeSpec `json:"supportingTypes"`
99-
RequiredInputs []resource.PropertyKey `json:"requiredInputs"`
100-
NonNilOutputs []resource.PropertyKey `json:"nonNilOutputs"`
101-
ProvidersConfig schema.ConfigSpec `json:"providersConfig"`
102+
Inputs map[resource.PropertyKey]*schema.PropertySpec `json:"inputs"`
103+
Outputs map[resource.PropertyKey]*schema.PropertySpec `json:"outputs"`
104+
SupportingTypes map[string]*schema.ComplexTypeSpec `json:"supportingTypes"`
105+
RequiredInputs []resource.PropertyKey `json:"requiredInputs"`
106+
NonNilOutputs []resource.PropertyKey `json:"nonNilOutputs"`
107+
ProvidersConfig schema.ConfigSpec `json:"providersConfig"`
108+
SchemaFieldMappings *SchemaFieldMappings `json:"schemaFieldMappings,omitempty"`
102109
}
103110

104111
var stringType = schema.TypeSpec{Type: "string"}
@@ -343,6 +350,10 @@ func InferModuleSchema(
343350
return inferModuleSchema(ctx, tf, packageName, mod, ver, newComponentLogger(nil, nil))
344351
}
345352

353+
func containsDash(s string) bool {
354+
return strings.Contains(s, "-")
355+
}
356+
346357
func inferModuleSchema(
347358
ctx context.Context,
348359
tf *tfsandbox.ModuleRuntime,
@@ -365,10 +376,26 @@ func inferModuleSchema(
365376
ProvidersConfig: schema.ConfigSpec{
366377
Variables: map[string]schema.PropertySpec{},
367378
},
379+
SchemaFieldMappings: &SchemaFieldMappings{
380+
InputFieldMappings: make(map[resource.PropertyKey]resource.PropertyKey),
381+
OutputFieldMappings: make(map[resource.PropertyKey]resource.PropertyKey),
382+
ProviderFieldMappings: make(map[string]string),
383+
},
368384
}
369385

386+
providerFieldMappings := inferredModuleSchema.SchemaFieldMappings.ProviderFieldMappings
387+
inputFieldMappings := inferredModuleSchema.SchemaFieldMappings.InputFieldMappings
388+
outputFieldMappings := inferredModuleSchema.SchemaFieldMappings.OutputFieldMappings
389+
370390
if module.ProviderRequirements != nil {
371391
for providerName := range module.ProviderRequirements.RequiredProviders {
392+
if containsDash(providerName) {
393+
// fields with dashes are not valid in Pulumi
394+
// so we replace dashes with underscores
395+
pulumiName := strings.ReplaceAll(providerName, "-", "_")
396+
providerFieldMappings[pulumiName] = providerName
397+
providerName = pulumiName
398+
}
372399
inferredModuleSchema.ProvidersConfig.Variables[providerName] = schema.PropertySpec{
373400
Description: "provider configuration for " + providerName,
374401
TypeSpec: mapType(anyType),
@@ -377,6 +404,14 @@ func inferModuleSchema(
377404
}
378405

379406
for variableName, variable := range module.Variables {
407+
if containsDash(variableName) {
408+
// fields with dashes are not valid in Pulumi
409+
// so we replace dashes with underscores
410+
pulumiName := strings.ReplaceAll(variableName, "-", "_")
411+
inputFieldMappings[resource.PropertyKey(pulumiName)] = resource.PropertyKey(variableName)
412+
variableName = pulumiName
413+
}
414+
380415
variableType := convertType(variable.Type, variableName, packageName, inferredModuleSchema.SupportingTypes)
381416

382417
key := tfsandbox.PulumiTopLevelKey(variableName)
@@ -395,11 +430,21 @@ func inferModuleSchema(
395430
}
396431

397432
for outputName, output := range module.Outputs {
433+
if containsDash(outputName) {
434+
// fields with dashes are not valid in Pulumi
435+
// so we replace dashes with underscores
436+
pulumiName := strings.ReplaceAll(outputName, "-", "_")
437+
outputFieldMappings[resource.PropertyKey(pulumiName)] = resource.PropertyKey(outputName)
438+
outputName = pulumiName
439+
}
440+
398441
// TODO[pulumi/pulumi-terraform-module#70] reconsider output type inference vs config
399442
var inferredType schema.TypeSpec
400443
if referencedVariableName, ok := isVariableReference(output.Expr); ok {
401444
k := tfsandbox.PulumiTopLevelKey(referencedVariableName)
402-
inferredType = inferredModuleSchema.Inputs[k].TypeSpec
445+
tfName := string(k)
446+
pulumiInputName := resource.PropertyKey(strings.ReplaceAll(tfName, "-", "_"))
447+
inferredType = inferredModuleSchema.Inputs[pulumiInputName].TypeSpec
403448
} else {
404449
inferredType = inferExpressionType(output.Expr)
405450
}

pkg/modprovider/tfmodules_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,72 @@ func TestInferringInputsFromLocalPath(t *testing.T) {
405405
}
406406
}
407407

408+
func TestInferringSchemaWithDashedFielsFromLocalPath(t *testing.T) {
409+
ctx := context.Background()
410+
src := filepath.Join("..", "..", "tests", "testdata", "modules", "dashed-module-fields")
411+
p, err := filepath.Abs(src)
412+
require.NoError(t, err)
413+
for _, executor := range []string{"terraforn", "opentofu"} {
414+
logger := newTestLogger(t)
415+
t.Run("executor="+executor, func(t *testing.T) {
416+
tf := newTestRuntime(t, executor)
417+
assert.NoError(t, err, "failed to pick module runtime")
418+
inferredSchema, err := inferModuleSchema(ctx, tf,
419+
packageName("dashed"),
420+
TFModuleSource(p),
421+
TFModuleVersion(""),
422+
logger)
423+
require.NoError(t, err)
424+
require.NotNil(t, inferredSchema, "module schema should not be nil")
425+
426+
expectedInputs := map[resource.PropertyKey]*schema.PropertySpec{
427+
"dashed_input": {
428+
TypeSpec: stringType,
429+
},
430+
}
431+
432+
// Pulumi -> Terraform field name conversion
433+
expectedInputFieldMappings := map[resource.PropertyKey]resource.PropertyKey{
434+
"dashed_input": "dashed-input",
435+
}
436+
437+
assert.Equal(t, expectedInputs, inferredSchema.Inputs, "inferred inputs do not match")
438+
assert.Equal(t, expectedInputFieldMappings, inferredSchema.SchemaFieldMappings.InputFieldMappings)
439+
440+
expectedOutputs := map[resource.PropertyKey]*schema.PropertySpec{
441+
"dashed_output": {
442+
TypeSpec: stringType,
443+
},
444+
}
445+
446+
// Pulumi -> Terraform field name conversion
447+
expectedOutputFieldMappings := map[resource.PropertyKey]resource.PropertyKey{
448+
"dashed_output": "dashed-output",
449+
}
450+
451+
assert.Equal(t, expectedOutputs, inferredSchema.Outputs, "inferred outputs do not match")
452+
assert.Equal(t, expectedOutputFieldMappings, inferredSchema.SchemaFieldMappings.OutputFieldMappings)
453+
454+
expectedProviderConfig := schema.ConfigSpec{
455+
Variables: map[string]schema.PropertySpec{
456+
"google_beta": {
457+
Description: "provider configuration for google_beta",
458+
TypeSpec: mapType(anyType),
459+
},
460+
},
461+
}
462+
463+
expectedProviderFieldMappings := map[string]string{
464+
"google_beta": "google-beta",
465+
}
466+
467+
assert.Equal(t, expectedProviderConfig, inferredSchema.ProvidersConfig, "inferred provider config does not match")
468+
assert.Equal(t, expectedProviderFieldMappings, inferredSchema.SchemaFieldMappings.ProviderFieldMappings,
469+
"inferred provider field mappings do not match")
470+
})
471+
}
472+
}
473+
408474
func TestInferModuleSchemaFromGitHubSource(t *testing.T) {
409475
ctx := context.Background()
410476
packageName := packageName("demoWebsite")

tests/acc_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,38 @@ func TestE2eYAML(t *testing.T) {
11821182
}
11831183
}
11841184

1185+
func TestEndToEndUsingModuleWithDashes(t *testing.T) {
1186+
localProviderBinPath := ensureCompiledProvider(t)
1187+
localPath := opttest.LocalProviderPath("terraform-module", filepath.Dir(localProviderBinPath))
1188+
modulePath, err := filepath.Abs(filepath.Join("testdata", "modules", "dashed-module-fields"))
1189+
assert.NoError(t, err, "failed to get absolute path for dashed module")
1190+
// TODO[pulumi/pulumi-terraform-module#426] - test local modules with Go
1191+
for _, runtime := range []string{"yaml", "ts", "python", "dotnet"} {
1192+
t.Run(runtime, func(t *testing.T) {
1193+
testProgram, err := filepath.Abs(filepath.Join("testdata", "programs", runtime, "dashed_module"))
1194+
require.NoError(t, err, "failed to get absolute path for dashed_module program")
1195+
1196+
// debug generated Terraform files
1197+
// tfFilesOutputDir := filepath.Join(testProgram, "tf_files")
1198+
// t.Cleanup(func() {
1199+
// t.Log("Cleaning up Terraform artifacts")
1200+
// cleanRandomDataFromTerraformArtifacts(t, tfFilesOutputDir, map[string]string{
1201+
// modulePath: "./path/to/module",
1202+
// })
1203+
// })
1204+
// add this option to the test program
1205+
// opttest.Env("PULUMI_TERRAFORM_MODULE_WRITE_TF_FILE", tfFilesOutputDir)
1206+
1207+
it := newPulumiTest(t, testProgram, localPath, opttest.SkipInstall())
1208+
pulumiPackageAdd(t, it, localProviderBinPath, modulePath, "", "dashed")
1209+
upResult := it.Up(t)
1210+
result, ok := upResult.Outputs["result"]
1211+
assert.True(t, ok, "expected output 'result' to be present")
1212+
assert.Equal(t, "example", result.Value)
1213+
})
1214+
}
1215+
}
1216+
11851217
func TestDiffDetailTerraform(t *testing.T) {
11861218
w := newTestWriter(t)
11871219

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
terraform {
2+
required_providers {
3+
google-beta = {
4+
source = "hashicorp/google-beta"
5+
version = ">= 6.19, < 7"
6+
}
7+
}
8+
}
9+
10+
variable "dashed-input" {
11+
type = string
12+
default = "default-value"
13+
}
14+
15+
output "dashed-output" {
16+
value = var.dashed-input
17+
}

0 commit comments

Comments
 (0)