Skip to content

Commit c6cd22d

Browse files
authored
fix: handle query params in module sources with subdirectories (#296)
The `terraform_module_pinned_source` rule was incorrectly reporting false positives when module sources included subdirectories with query parameters (e.g., `github.com/org/repo.git/directory?ref=v1.2.3`). The issue occurred because `go-getter` URL-encodes query parameters when they appear after a subdirectory path with a single slash. The fix extracts query parameters from the original source before calling `getter.Detect()`, then merges them with any parameters found in the detected URL. Adds test cases for GitHub and git:// protocol sources with subdirectories to prevent regressions.
1 parent f75a9a5 commit c6cd22d

File tree

2 files changed

+111
-0
lines changed

2 files changed

+111
-0
lines changed

rules/terraform_module_pinned_source.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ func (r *TerraformModulePinnedSourceRule) Check(rr tflint.Runner) error {
8787
}
8888

8989
func (r *TerraformModulePinnedSourceRule) checkModule(runner tflint.Runner, module *terraform.ModuleCall, config terraformModulePinnedSourceRuleConfig) error {
90+
// Extract query parameters from the original source before calling getter.Detect()
91+
// because go-getter may URL-encode them when there's a subdirectory path
92+
originalQuery := url.Values{}
93+
if idx := strings.Index(module.Source, "?"); idx != -1 {
94+
if parsedQuery, err := url.ParseQuery(module.Source[idx+1:]); err == nil {
95+
originalQuery = parsedQuery
96+
}
97+
}
98+
9099
source, err := getter.Detect(module.Source, filepath.Dir(module.DefRange.Filename), []getter.Detector{
91100
// https://github.com/hashicorp/terraform/blob/51b0aee36cc2145f45f5b04051a01eb6eb7be8bf/internal/getmodules/getter.go#L30-L52
92101
new(getter.GitHubDetector),
@@ -130,7 +139,13 @@ func (r *TerraformModulePinnedSourceRule) checkModule(runner tflint.Runner, modu
130139
)
131140
}
132141

142+
// Merge query parameters from both the detected URL and the original source
133143
query := u.Query()
144+
for key, values := range originalQuery {
145+
if len(query[key]) == 0 {
146+
query[key] = values
147+
}
148+
}
134149

135150
if ref := query.Get("ref"); ref != "" {
136151
return r.checkRevision(runner, module, config, "ref", ref)

rules/terraform_module_pinned_source_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,32 @@ module "default_git" {
6363
Content: `
6464
module "pinned_git" {
6565
source = "git://hashicorp.com/consul.git?ref=pinned"
66+
}`,
67+
Expected: helper.Issues{},
68+
},
69+
{
70+
Name: "git module with subdirectory is not pinned",
71+
Content: `
72+
module "unpinned" {
73+
source = "git://hashicorp.com/consul.git/subdirectory"
74+
}`,
75+
Expected: helper.Issues{
76+
{
77+
Rule: NewTerraformModulePinnedSourceRule(),
78+
Message: "Module source \"git://hashicorp.com/consul.git/subdirectory\" is not pinned",
79+
Range: hcl.Range{
80+
Filename: "module.tf",
81+
Start: hcl.Pos{Line: 3, Column: 12},
82+
End: hcl.Pos{Line: 3, Column: 57},
83+
},
84+
},
85+
},
86+
},
87+
{
88+
Name: "git module with subdirectory reference is pinned",
89+
Content: `
90+
module "pinned_git" {
91+
source = "git://hashicorp.com/consul.git/subdirectory?ref=v1.2.3"
6692
}`,
6793
Expected: helper.Issues{},
6894
},
@@ -192,6 +218,76 @@ module "default_git" {
192218
Content: `
193219
module "pinned_git" {
194220
source = "github.com/hashicorp/consul.git?ref=pinned"
221+
}`,
222+
Expected: helper.Issues{},
223+
},
224+
{
225+
Name: "github module with subdirectory is not pinned",
226+
Content: `
227+
module "unpinned" {
228+
source = "github.com/hashicorp/consul.git/subdirectory"
229+
}`,
230+
Expected: helper.Issues{
231+
{
232+
Rule: NewTerraformModulePinnedSourceRule(),
233+
Message: "Module source \"github.com/hashicorp/consul.git/subdirectory\" is not pinned",
234+
Range: hcl.Range{
235+
Filename: "module.tf",
236+
Start: hcl.Pos{Line: 3, Column: 12},
237+
End: hcl.Pos{Line: 3, Column: 58},
238+
},
239+
},
240+
},
241+
},
242+
{
243+
Name: "github module with subdirectory reference is pinned",
244+
Content: `
245+
module "pinned_git" {
246+
source = "github.com/hashicorp/consul.git/subdirectory?ref=v1.2.3"
247+
}`,
248+
Expected: helper.Issues{},
249+
},
250+
{
251+
Name: "github module with subdirectory reference is default",
252+
Content: `
253+
module "default_git" {
254+
source = "github.com/hashicorp/consul.git/subdirectory?ref=master"
255+
}`,
256+
Expected: helper.Issues{
257+
{
258+
Rule: NewTerraformModulePinnedSourceRule(),
259+
Message: "Module source \"github.com/hashicorp/consul.git/subdirectory?ref=master\" uses a default branch as ref (master)",
260+
Range: hcl.Range{
261+
Filename: "module.tf",
262+
Start: hcl.Pos{Line: 3, Column: 12},
263+
End: hcl.Pos{Line: 3, Column: 69},
264+
},
265+
},
266+
},
267+
},
268+
{
269+
Name: "github module with multiple subdirectories is not pinned",
270+
Content: `
271+
module "unpinned" {
272+
source = "github.com/hashicorp/consul.git/directory/subdirectory"
273+
}`,
274+
Expected: helper.Issues{
275+
{
276+
Rule: NewTerraformModulePinnedSourceRule(),
277+
Message: "Module source \"github.com/hashicorp/consul.git/directory/subdirectory\" is not pinned",
278+
Range: hcl.Range{
279+
Filename: "module.tf",
280+
Start: hcl.Pos{Line: 3, Column: 12},
281+
End: hcl.Pos{Line: 3, Column: 68},
282+
},
283+
},
284+
},
285+
},
286+
{
287+
Name: "github module with multiple subdirectories reference is pinned",
288+
Content: `
289+
module "pinned_git" {
290+
source = "github.com/hashicorp/consul.git/directory/subdirectory?ref=v1.2.3"
195291
}`,
196292
Expected: helper.Issues{},
197293
},

0 commit comments

Comments
 (0)