Skip to content

Commit 8bc584b

Browse files
authored
non-nullable inputs should be marked as required (#385)
* non-nullable inputs should be marked as required * add opentofu to the executors of the test
1 parent 7fe959e commit 8bc584b

File tree

4 files changed

+196
-11
lines changed

4 files changed

+196
-11
lines changed

pkg/modprovider/helpers_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,26 @@ import (
1313
)
1414

1515
type testLogger struct {
16-
logs []string
16+
t *testing.T
1717
}
1818

1919
func (l *testLogger) Log(_ context.Context, level tfsandbox.LogLevel, msg string) {
20-
l.logs = append(l.logs, string(level)+": "+msg)
20+
l.t.Log(string(level) + ": " + msg)
2121
}
2222

2323
func (l *testLogger) LogStatus(_ context.Context, level tfsandbox.LogLevel, msg string) {
24-
l.logs = append(l.logs, string(level)+": "+msg)
24+
l.t.Log(string(level) + ": " + msg)
25+
}
26+
27+
func newTestLogger(t *testing.T) tfsandbox.Logger {
28+
return &testLogger{t: t}
2529
}
2630

2731
//nolint:unused
2832
func newTestTofu(t *testing.T) *tfsandbox.ModuleRuntime {
2933
srv := newTestAuxProviderServer(t)
30-
31-
tofu, err := tfsandbox.NewTofu(context.Background(), tfsandbox.DiscardLogger, nil, srv, tofuresolver.ResolveOpts{})
34+
logger := newTestLogger(t)
35+
tofu, err := tfsandbox.NewTofu(context.Background(), logger, nil, srv, tofuresolver.ResolveOpts{})
3236
require.NoError(t, err)
3337

3438
t.Cleanup(func() {

pkg/modprovider/tfmodules.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,10 @@ func inferModuleSchema(
365365
TypeSpec: variableType,
366366
}
367367

368-
if variable.Default.IsNull() && !variable.Nullable {
368+
nullable := variable.NullableSet && variable.Nullable
369+
hasDefault := variable.Default.Type() != cty.NilType
370+
optional := hasDefault || nullable
371+
if !optional {
369372
inferredModuleSchema.RequiredInputs = append(inferredModuleSchema.RequiredInputs, key)
370373
}
371374
}

pkg/modprovider/tfmodules_test.go

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
"os"
2020
"path/filepath"
21+
"slices"
2122
"testing"
2223

2324
"github.com/stretchr/testify/assert"
@@ -43,16 +44,13 @@ func TestExtractModuleContentWorks(t *testing.T) {
4344
for _, executor := range executors {
4445
t.Run("executor="+executor, func(t *testing.T) {
4546
ctx := context.Background()
46-
logger := &testLogger{}
47+
logger := newTestLogger(t)
4748
source := TFModuleSource("terraform-aws-modules/vpc/aws")
4849
version := TFModuleVersion("5.18.1")
4950
tf := newTestRuntime(t, executor)
5051
awsVpc, err := extractModuleContent(ctx, tf, source, version, logger)
5152
assert.NoError(t, err, "failed to infer module schema for aws vpc module")
5253
assert.NotNil(t, awsVpc, "inferred module schema for aws vpc module is nil")
53-
for _, log := range logger.logs {
54-
t.Logf("Log: %s", log)
55-
}
5654
})
5755
}
5856
}
@@ -303,7 +301,7 @@ func TestExtractModuleContentWorksFromLocalPath(t *testing.T) {
303301
executors := getExecutorsFromEnv()
304302
for _, executor := range executors {
305303
t.Run("executor="+executor, func(t *testing.T) {
306-
logger := &testLogger{}
304+
logger := newTestLogger(t)
307305
tf := newTestRuntime(t, executor)
308306
assert.NoError(t, err, "failed to pick module runtime")
309307
mod, err := extractModuleContent(ctx, tf, TFModuleSource(p), "", logger)
@@ -313,6 +311,100 @@ func TestExtractModuleContentWorksFromLocalPath(t *testing.T) {
313311
}
314312
}
315313

314+
func TestInferringInputsFromLocalPath(t *testing.T) {
315+
ctx := context.Background()
316+
src := filepath.Join("..", "..", "tests", "testdata", "modules", "schema-inference-example")
317+
p, err := filepath.Abs(src)
318+
require.NoError(t, err)
319+
for _, executor := range []string{"terraforn", "opentofu"} {
320+
logger := newTestLogger(t)
321+
t.Run("executor="+executor, func(t *testing.T) {
322+
tf := newTestRuntime(t, executor)
323+
assert.NoError(t, err, "failed to pick module runtime")
324+
inferredSchema, err := inferModuleSchema(ctx, tf,
325+
packageName("schema-inference-example"),
326+
TFModuleSource(p),
327+
TFModuleVersion(""),
328+
logger)
329+
require.NoError(t, err)
330+
require.NotNil(t, inferredSchema, "module schema should not be nil")
331+
332+
expectedInputs := map[resource.PropertyKey]*schema.PropertySpec{
333+
"required_string": {
334+
Description: "required string",
335+
TypeSpec: stringType,
336+
},
337+
"optional_string_with_default": {
338+
Description: "optional string with default",
339+
TypeSpec: stringType,
340+
},
341+
"optional_string_without_default": {
342+
Description: "optional string without default",
343+
TypeSpec: stringType,
344+
},
345+
"required_string_using_nullable_false": {
346+
TypeSpec: stringType,
347+
},
348+
"optional_string_using_nullable_true": {
349+
TypeSpec: stringType,
350+
},
351+
"required_boolean": {
352+
TypeSpec: boolType,
353+
},
354+
"optional_boolean_with_default": {
355+
TypeSpec: boolType,
356+
},
357+
"required_number": {
358+
TypeSpec: numberType,
359+
},
360+
"optional_number_with_default": {
361+
TypeSpec: numberType,
362+
},
363+
"required_list_of_strings": {
364+
TypeSpec: arrayType(stringType),
365+
},
366+
"optional_list_of_strings_with_default": {
367+
TypeSpec: arrayType(stringType),
368+
Description: "optional list of strings with default",
369+
},
370+
"optional_list_of_strings_without_default": {
371+
TypeSpec: arrayType(stringType),
372+
Description: "optional list of strings without default",
373+
},
374+
"required_map_of_strings": {
375+
TypeSpec: mapType(stringType),
376+
},
377+
"optional_map_of_strings_with_default": {
378+
TypeSpec: mapType(stringType),
379+
Description: "optional map of strings with default",
380+
},
381+
}
382+
383+
for name, expected := range expectedInputs {
384+
actual, ok := inferredSchema.Inputs[name]
385+
assert.True(t, ok, "input %s is missing from the schema", name)
386+
assert.Equal(t, expected.Description, actual.Description, "input %s description is incorrect", name)
387+
assert.Equal(t, expected.TypeSpec, actual.TypeSpec, "input %s type is incorrect", name)
388+
}
389+
390+
expectedRequiredInputs := []resource.PropertyKey{
391+
"required_string",
392+
"required_string_using_nullable_false",
393+
"required_number",
394+
"required_boolean",
395+
"required_list_of_strings",
396+
"required_map_of_strings",
397+
}
398+
399+
actualRequiredInputs := inferredSchema.RequiredInputs
400+
401+
slices.Sort(expectedRequiredInputs)
402+
slices.Sort(actualRequiredInputs)
403+
assert.Equal(t, expectedRequiredInputs, actualRequiredInputs)
404+
})
405+
}
406+
}
407+
316408
func TestInferModuleSchemaFromGitHubSource(t *testing.T) {
317409
ctx := context.Background()
318410
packageName := packageName("demoWebsite")
@@ -443,6 +535,22 @@ func TestInferModuleSchemaFromGitHubSourceWithSubModuleAndVersion(t *testing.T)
443535
}
444536
}
445537

538+
func TestInferRequiredInputsWorks(t *testing.T) {
539+
ctx := context.Background()
540+
packageName := packageName("http")
541+
for _, executor := range []string{"terraform", "opentofu"} {
542+
t.Run("executor="+executor, func(t *testing.T) {
543+
source := TFModuleSource("terraform-aws-modules/security-group/aws//modules/http-80")
544+
version := TFModuleVersion("5.3.0")
545+
tf := newTestRuntime(t, executor)
546+
httpSchema, err := InferModuleSchema(ctx, tf, packageName, source, version)
547+
assert.NoError(t, err, "failed to infer module schema for aws vpc module")
548+
assert.NotNil(t, httpSchema, "inferred module schema for aws vpc module is nil")
549+
assert.Contains(t, httpSchema.RequiredInputs, resource.PropertyKey("vpc_id"))
550+
})
551+
}
552+
}
553+
446554
func TestResolveModuleSources(t *testing.T) {
447555
executors := getExecutorsFromEnv()
448556
for _, executor := range executors {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
variable "required_string" {
2+
description = "required string"
3+
type = string
4+
}
5+
6+
variable "optional_string_with_default" {
7+
description = "optional string with default"
8+
type = string
9+
default = "default_value"
10+
}
11+
12+
variable "optional_string_without_default" {
13+
description = "optional string without default"
14+
type = string
15+
default = null
16+
}
17+
18+
variable "required_string_using_nullable_false" {
19+
type = string
20+
nullable = false
21+
}
22+
23+
variable "optional_string_using_nullable_true" {
24+
type = string
25+
nullable = true
26+
}
27+
28+
variable "required_boolean" {
29+
type = bool
30+
}
31+
32+
variable "optional_boolean_with_default" {
33+
type = bool
34+
default = true
35+
}
36+
37+
variable "required_number" {
38+
type = number
39+
}
40+
41+
variable "optional_number_with_default" {
42+
type = number
43+
default = 42
44+
}
45+
46+
variable "required_list_of_strings" {
47+
type = list(string)
48+
}
49+
50+
variable "optional_list_of_strings_with_default" {
51+
description = "optional list of strings with default"
52+
type = list(string)
53+
default = []
54+
}
55+
56+
variable "optional_list_of_strings_without_default" {
57+
description = "optional list of strings without default"
58+
type = list(string)
59+
default = null
60+
}
61+
62+
variable "required_map_of_strings" {
63+
type = map(string)
64+
}
65+
66+
variable "optional_map_of_strings_with_default" {
67+
description = "optional map of strings with default"
68+
type = map(string)
69+
default = {}
70+
}

0 commit comments

Comments
 (0)