Skip to content

Commit 30dc2b6

Browse files
committed
chore: lock file parsing
1 parent 5e8d005 commit 30dc2b6

File tree

6 files changed

+325
-31
lines changed

6 files changed

+325
-31
lines changed

internal/remotestate/backend/gcs/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ func (cfg Config) ParseExtendedGCSConfig() (*ExtendedRemoteStateConfigGCS, error
5656
extendedConfig ExtendedRemoteStateConfigGCS
5757
)
5858

59-
if err := mapstructure.Decode(cfg, &gcsConfig); err != nil {
59+
if err := mapstructure.WeakDecode(cfg, &gcsConfig); err != nil {
6060
return nil, errors.New(err)
6161
}
6262

63-
if err := mapstructure.Decode(cfg, &extendedConfig); err != nil {
63+
if err := mapstructure.WeakDecode(cfg, &extendedConfig); err != nil {
6464
return nil, errors.New(err)
6565
}
6666

internal/remotestate/backend/gcs/config_test.go

Lines changed: 106 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ package gcs_test
33
import (
44
"testing"
55

6-
"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/gcs"
6+
gcsbackend "github.com/gruntwork-io/terragrunt/internal/remotestate/backend/gcs"
77
"github.com/gruntwork-io/terragrunt/test/helpers/logger"
88
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
910
)
1011

1112
func TestConfig_IsEqual(t *testing.T) {
@@ -15,80 +16,80 @@ func TestConfig_IsEqual(t *testing.T) {
1516

1617
testCases := []struct { //nolint: govet
1718
name string
18-
cfg gcs.Config
19-
comparableCfg gcs.Config
19+
cfg gcsbackend.Config
20+
comparableCfg gcsbackend.Config
2021
shouldBeEqual bool
2122
}{
2223
{
2324
"equal-both-empty",
24-
gcs.Config{},
25-
gcs.Config{},
25+
gcsbackend.Config{},
26+
gcsbackend.Config{},
2627
true,
2728
},
2829
{
2930
"equal-empty-and-nil",
30-
gcs.Config{},
31+
gcsbackend.Config{},
3132
nil,
3233
true,
3334
},
3435
{
3536
"equal-one-key",
36-
gcs.Config{"foo": "bar"},
37-
gcs.Config{"foo": "bar"},
37+
gcsbackend.Config{"foo": "bar"},
38+
gcsbackend.Config{"foo": "bar"},
3839
true,
3940
},
4041
{
4142
"equal-multiple-keys",
42-
gcs.Config{"foo": "bar", "baz": []string{"a", "b", "c"}, "blah": 123, "bool": true},
43-
gcs.Config{"foo": "bar", "baz": []string{"a", "b", "c"}, "blah": 123, "bool": true},
43+
gcsbackend.Config{"foo": "bar", "baz": []string{"a", "b", "c"}, "blah": 123, "bool": true},
44+
gcsbackend.Config{"foo": "bar", "baz": []string{"a", "b", "c"}, "blah": 123, "bool": true},
4445
true,
4546
},
4647
{
4748
"equal-encrypt-bool-handling",
48-
gcs.Config{"encrypt": true},
49-
gcs.Config{"encrypt": "true"},
49+
gcsbackend.Config{"encrypt": true},
50+
gcsbackend.Config{"encrypt": "true"},
5051
true,
5152
},
5253
{
5354
"equal-general-bool-handling",
54-
gcs.Config{"something": true, "encrypt": true},
55-
gcs.Config{"something": "true", "encrypt": "true"},
55+
gcsbackend.Config{"something": true, "encrypt": true},
56+
gcsbackend.Config{"something": "true", "encrypt": "true"},
5657
true,
5758
},
5859
{
5960
"equal-ignore-gcs-labels",
60-
gcs.Config{"foo": "bar", "gcs_bucket_labels": []map[string]string{{"foo": "bar"}}},
61-
gcs.Config{"foo": "bar"},
61+
gcsbackend.Config{"foo": "bar", "gcs_bucket_labels": []map[string]string{{"foo": "bar"}}},
62+
gcsbackend.Config{"foo": "bar"},
6263
true,
6364
},
6465
{
6566
"unequal-values",
66-
gcs.Config{"foo": "bar"},
67-
gcs.Config{"foo": "different"},
67+
gcsbackend.Config{"foo": "bar"},
68+
gcsbackend.Config{"foo": "different"},
6869
false,
6970
},
7071
{
7172
"unequal-non-empty-cfg-nil",
72-
gcs.Config{"foo": "bar"},
73+
gcsbackend.Config{"foo": "bar"},
7374
nil,
7475
false,
7576
},
7677
{
7778
"unequal-general-bool-handling",
78-
gcs.Config{"something": true},
79-
gcs.Config{"something": "false"},
79+
gcsbackend.Config{"something": true},
80+
gcsbackend.Config{"something": "false"},
8081
false,
8182
},
8283
{
8384
"equal-null-ignored",
84-
gcs.Config{"something": "foo"},
85-
gcs.Config{"something": "foo", "ignored-because-null": nil},
85+
gcsbackend.Config{"something": "foo"},
86+
gcsbackend.Config{"something": "foo", "ignored-because-null": nil},
8687
true,
8788
},
8889
{
8990
"terragrunt-only-configs-remain-intact",
90-
gcs.Config{"something": "foo", "skip_bucket_creation": true},
91-
gcs.Config{"something": "foo"},
91+
gcsbackend.Config{"something": "foo", "skip_bucket_creation": true},
92+
gcsbackend.Config{"something": "foo"},
9293
true,
9394
},
9495
}
@@ -102,3 +103,83 @@ func TestConfig_IsEqual(t *testing.T) {
102103
})
103104
}
104105
}
106+
107+
// TestParseExtendedGCSConfig_StringBoolCoercion verifies that boolean config values
108+
// passed as strings (e.g. from HCL ternary type unification) are correctly parsed.
109+
// See https://github.com/gruntwork-io/terragrunt/issues/5475
110+
func TestParseExtendedGCSConfig_StringBoolCoercion(t *testing.T) {
111+
t.Parallel()
112+
113+
testCases := []struct { //nolint: govet
114+
name string
115+
config gcsbackend.Config
116+
check func(t *testing.T, cfg *gcsbackend.ExtendedRemoteStateConfigGCS)
117+
}{
118+
{
119+
"skip-bucket-versioning-string-true",
120+
gcsbackend.Config{
121+
"bucket": "my-bucket",
122+
"skip_bucket_versioning": "true",
123+
},
124+
func(t *testing.T, cfg *gcsbackend.ExtendedRemoteStateConfigGCS) {
125+
t.Helper()
126+
assert.True(t, cfg.SkipBucketVersioning)
127+
},
128+
},
129+
{
130+
"skip-bucket-versioning-string-false",
131+
gcsbackend.Config{
132+
"bucket": "my-bucket",
133+
"skip_bucket_versioning": "false",
134+
},
135+
func(t *testing.T, cfg *gcsbackend.ExtendedRemoteStateConfigGCS) {
136+
t.Helper()
137+
assert.False(t, cfg.SkipBucketVersioning)
138+
},
139+
},
140+
{
141+
"skip-bucket-creation-string-true",
142+
gcsbackend.Config{
143+
"bucket": "my-bucket",
144+
"skip_bucket_creation": "true",
145+
},
146+
func(t *testing.T, cfg *gcsbackend.ExtendedRemoteStateConfigGCS) {
147+
t.Helper()
148+
assert.True(t, cfg.SkipBucketCreation)
149+
},
150+
},
151+
{
152+
"enable-bucket-policy-only-string-true",
153+
gcsbackend.Config{
154+
"bucket": "my-bucket",
155+
"enable_bucket_policy_only": "true",
156+
},
157+
func(t *testing.T, cfg *gcsbackend.ExtendedRemoteStateConfigGCS) {
158+
t.Helper()
159+
assert.True(t, cfg.EnableBucketPolicyOnly)
160+
},
161+
},
162+
{
163+
"native-bool-still-works",
164+
gcsbackend.Config{
165+
"bucket": "my-bucket",
166+
"skip_bucket_versioning": true,
167+
},
168+
func(t *testing.T, cfg *gcsbackend.ExtendedRemoteStateConfigGCS) {
169+
t.Helper()
170+
assert.True(t, cfg.SkipBucketVersioning)
171+
},
172+
},
173+
}
174+
175+
for _, tc := range testCases {
176+
t.Run(tc.name, func(t *testing.T) {
177+
t.Parallel()
178+
179+
extGCSCfg, err := tc.config.ParseExtendedGCSConfig()
180+
require.NoError(t, err)
181+
182+
tc.check(t, extGCSCfg)
183+
})
184+
}
185+
}

internal/remotestate/backend/s3/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,11 @@ func (cfg Config) ParseExtendedS3Config() (*ExtendedRemoteStateConfigS3, error)
103103
extendedConfig ExtendedRemoteStateConfigS3
104104
)
105105

106-
if err := mapstructure.Decode(cfg, &s3Config); err != nil {
106+
if err := mapstructure.WeakDecode(cfg, &s3Config); err != nil {
107107
return nil, errors.New(err)
108108
}
109109

110-
if err := mapstructure.Decode(cfg, &extendedConfig); err != nil {
110+
if err := mapstructure.WeakDecode(cfg, &extendedConfig); err != nil {
111111
return nil, errors.New(err)
112112
}
113113

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package s3_test
2+
3+
import (
4+
"testing"
5+
6+
s3backend "github.com/gruntwork-io/terragrunt/internal/remotestate/backend/s3"
7+
"github.com/gruntwork-io/terragrunt/pkg/log"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// TestParseExtendedS3Config_StringBoolCoercion verifies that boolean config values
13+
// passed as strings (e.g. from HCL ternary type unification) are correctly parsed.
14+
// See https://github.com/gruntwork-io/terragrunt/issues/5475
15+
func TestParseExtendedS3Config_StringBoolCoercion(t *testing.T) {
16+
t.Parallel()
17+
18+
testCases := []struct { //nolint: govet
19+
name string
20+
config s3backend.Config
21+
check func(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3)
22+
}{
23+
{
24+
"use-lockfile-string-true",
25+
s3backend.Config{
26+
"bucket": "my-bucket",
27+
"key": "my-key",
28+
"region": "us-east-1",
29+
"use_lockfile": "true",
30+
},
31+
func(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {
32+
t.Helper()
33+
assert.True(t, cfg.RemoteStateConfigS3.UseLockfile)
34+
},
35+
},
36+
{
37+
"use-lockfile-string-false",
38+
s3backend.Config{
39+
"bucket": "my-bucket",
40+
"key": "my-key",
41+
"region": "us-east-1",
42+
"use_lockfile": "false",
43+
},
44+
func(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {
45+
t.Helper()
46+
assert.False(t, cfg.RemoteStateConfigS3.UseLockfile)
47+
},
48+
},
49+
{
50+
"encrypt-string-true",
51+
s3backend.Config{
52+
"bucket": "my-bucket",
53+
"key": "my-key",
54+
"region": "us-east-1",
55+
"encrypt": "true",
56+
},
57+
func(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {
58+
t.Helper()
59+
assert.True(t, cfg.RemoteStateConfigS3.Encrypt)
60+
},
61+
},
62+
{
63+
"force-path-style-string-true",
64+
s3backend.Config{
65+
"bucket": "my-bucket",
66+
"key": "my-key",
67+
"region": "us-east-1",
68+
"force_path_style": "true",
69+
},
70+
func(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {
71+
t.Helper()
72+
assert.True(t, cfg.RemoteStateConfigS3.S3ForcePathStyle)
73+
},
74+
},
75+
{
76+
"skip-bucket-versioning-string-true",
77+
s3backend.Config{
78+
"bucket": "my-bucket",
79+
"key": "my-key",
80+
"region": "us-east-1",
81+
"skip_bucket_versioning": "true",
82+
},
83+
func(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {
84+
t.Helper()
85+
assert.True(t, cfg.SkipBucketVersioning)
86+
},
87+
},
88+
{
89+
"native-bool-still-works",
90+
s3backend.Config{
91+
"bucket": "my-bucket",
92+
"key": "my-key",
93+
"region": "us-east-1",
94+
"use_lockfile": true,
95+
},
96+
func(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {
97+
t.Helper()
98+
assert.True(t, cfg.RemoteStateConfigS3.UseLockfile)
99+
},
100+
},
101+
}
102+
103+
for _, tc := range testCases {
104+
t.Run(tc.name, func(t *testing.T) {
105+
t.Parallel()
106+
107+
extS3Cfg, err := tc.config.Normalize(log.Default()).ParseExtendedS3Config()
108+
require.NoError(t, err)
109+
110+
tc.check(t, extS3Cfg)
111+
})
112+
}
113+
}
114+
115+
// TestParseExtendedS3Config_InvalidStringBool verifies that WeakDecode rejects
116+
// invalid string values for bool fields (e.g. "maybe" is not a valid bool).
117+
func TestParseExtendedS3Config_InvalidStringBool(t *testing.T) {
118+
t.Parallel()
119+
120+
cfg := s3backend.Config{
121+
"bucket": "my-bucket",
122+
"key": "my-key",
123+
"region": "us-east-1",
124+
"use_lockfile": "maybe",
125+
}
126+
127+
_, err := cfg.Normalize(log.Default()).ParseExtendedS3Config()
128+
require.Error(t, err)
129+
}

0 commit comments

Comments
 (0)