Skip to content

Commit 3232af1

Browse files
authored
Add terraform_module_shallow_clone rule (#267)
1 parent ac1e78e commit 3232af1

File tree

5 files changed

+458
-0
lines changed

5 files changed

+458
-0
lines changed

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ All rules are enabled by default, but by setting `preset = "recommended"`, you c
1515
|[terraform_empty_list_equality](terraform_empty_list_equality.md)|Disallow comparisons with `[]` when checking if a collection is empty||
1616
|[terraform_map_duplicate_keys](terraform_map_duplicate_keys.md)|Disallow duplicate keys in a map object||
1717
|[terraform_module_pinned_source](terraform_module_pinned_source.md)|Disallow specifying a git or mercurial repository as a module source without pinning to a version||
18+
|[terraform_module_shallow_clone](terraform_module_shallow_clone.md)|Require pinned Git-hosted Terraform modules to use shallow cloning||
1819
|[terraform_module_version](terraform_module_version.md)|Checks that Terraform modules sourced from a registry specify a version||
1920
|[terraform_naming_convention](terraform_naming_convention.md)|Enforces naming conventions for resources, data sources, etc||
2021
|[terraform_required_providers](terraform_required_providers.md)|Require that all providers have version constraints through required_providers||
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# terraform_module_shallow_clone
2+
3+
Require pinned Git-hosted Terraform modules to use shallow cloning.
4+
5+
## Example
6+
7+
```hcl
8+
module "consul" {
9+
source = "git::ssh://git@github.com/hashicorp/consul.git?ref=v1.0.0"
10+
}
11+
```
12+
13+
```
14+
$ tflint
15+
1 issue(s) found:
16+
17+
Warning: Module source "git::ssh://git@github.com/hashicorp/consul.git?ref=v1.0.0" should enable shallow cloning by adding "depth=1" parameter (terraform_module_shallow_clone)
18+
19+
on main.tf line 2:
20+
3: source = "git::ssh://git@github.com/hashicorp/consul.git?ref=v1.0.0"
21+
22+
Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.13.0/docs/rules/terraform_module_shallow_clone.md
23+
```
24+
25+
## Why
26+
27+
https://developer.hashicorp.com/terraform/language/modules/sources#shallow-clone
28+
29+
When sourcing a Terraform module from a Git repository by tag or branch, enabling shallow cloning can significantly improve performance by reducing the amount of data that needs to be downloaded. This is especially beneficial in CI/CD pipelines where modules are downloaded frequently.
30+
31+
Shallow cloning only includes the most recent commit for a reference. Because it uses the `--branch` argument to `git clone`, it can only be used for named branches and tags, not raw commit IDs.
32+
33+
## How To Fix
34+
35+
Add the `depth=1` query parameter to enable shallow cloning.

rules/preset.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var PresetRules = map[string][]tflint.Rule{
1313
NewTerraformEmptyListEqualityRule(),
1414
NewTerraformMapDuplicateKeysRule(),
1515
NewTerraformModulePinnedSourceRule(),
16+
NewTerraformModuleShallowCloneRule(),
1617
NewTerraformModuleVersionRule(),
1718
NewTerraformNamingConventionRule(),
1819
NewTerraformRequiredProvidersRule(),
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package rules
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"path/filepath"
7+
"regexp"
8+
"strings"
9+
10+
"github.com/hashicorp/go-getter"
11+
"github.com/hashicorp/hcl/v2"
12+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
13+
"github.com/terraform-linters/tflint-ruleset-terraform/project"
14+
"github.com/terraform-linters/tflint-ruleset-terraform/terraform"
15+
)
16+
17+
var gitCommitRegex = regexp.MustCompile("^[a-f0-9]{4,64}$")
18+
19+
// TerraformModuleShallowCloneRule checks that Git-hosted Terraform modules use shallow cloning
20+
type TerraformModuleShallowCloneRule struct {
21+
tflint.DefaultRule
22+
23+
attributeName string
24+
}
25+
26+
// NewTerraformModuleShallowCloneRule returns new rule with default attributes
27+
func NewTerraformModuleShallowCloneRule() *TerraformModuleShallowCloneRule {
28+
return &TerraformModuleShallowCloneRule{
29+
attributeName: "source",
30+
}
31+
}
32+
33+
// Name returns the rule name
34+
func (r *TerraformModuleShallowCloneRule) Name() string {
35+
return "terraform_module_shallow_clone"
36+
}
37+
38+
// Enabled returns whether the rule is enabled by default
39+
func (r *TerraformModuleShallowCloneRule) Enabled() bool {
40+
return false
41+
}
42+
43+
// Severity returns the rule severity
44+
func (r *TerraformModuleShallowCloneRule) Severity() tflint.Severity {
45+
return tflint.WARNING
46+
}
47+
48+
// Link returns the rule reference link
49+
func (r *TerraformModuleShallowCloneRule) Link() string {
50+
return project.ReferenceLink(r.Name())
51+
}
52+
53+
// Check checks if Git-hosted Terraform modules use shallow cloning
54+
func (r *TerraformModuleShallowCloneRule) Check(rr tflint.Runner) error {
55+
runner := rr.(*terraform.Runner)
56+
57+
path, err := runner.GetModulePath()
58+
if err != nil {
59+
return err
60+
}
61+
if !path.IsRoot() {
62+
// This rule does not evaluate child modules.
63+
return nil
64+
}
65+
66+
calls, diags := runner.GetModuleCalls()
67+
if diags.HasErrors() {
68+
return diags
69+
}
70+
71+
for _, call := range calls {
72+
if err := r.checkModule(runner, call); err != nil {
73+
return err
74+
}
75+
}
76+
77+
return nil
78+
}
79+
80+
func (r *TerraformModuleShallowCloneRule) checkModule(runner tflint.Runner, module *terraform.ModuleCall) error {
81+
filename := module.DefRange.Filename
82+
source, err := getter.Detect(module.Source, filepath.Dir(filename), []getter.Detector{
83+
// https://github.com/hashicorp/terraform/blob/51b0aee36cc2145f45f5b04051a01eb6eb7be8bf/internal/getmodules/getter.go#L30-L52
84+
new(getter.GitHubDetector),
85+
new(getter.GitDetector),
86+
new(getter.BitBucketDetector),
87+
new(getter.GCSDetector),
88+
new(getter.S3Detector),
89+
new(getter.FileDetector),
90+
})
91+
if err != nil {
92+
return err
93+
}
94+
95+
u, err := url.Parse(source)
96+
if err != nil {
97+
return err
98+
}
99+
100+
// Only check Git-based sources
101+
if u.Scheme != "git" {
102+
return nil
103+
}
104+
105+
if u.Opaque != "" {
106+
// for git:: pseudo-URLs, Opaque is :https, but query will still be parsed
107+
query := u.RawQuery
108+
u, err = url.Parse(strings.TrimPrefix(u.Opaque, ":"))
109+
if err != nil {
110+
return err
111+
}
112+
113+
u.RawQuery = query
114+
}
115+
116+
if u.Hostname() == "" {
117+
return nil
118+
}
119+
120+
query := u.Query()
121+
122+
// Check if module is pinned to a specific version
123+
ref := query.Get("ref")
124+
125+
// Skip if not pinned at all
126+
if ref == "" {
127+
return nil
128+
}
129+
130+
// Skip if it's a raw git commit ID (40 character hex string)
131+
if gitCommitRegex.MatchString(ref) {
132+
return nil
133+
}
134+
135+
// Check if depth parameter is already set
136+
if query.Get("depth") == "1" {
137+
return nil
138+
}
139+
140+
exprRange := module.SourceAttr.Expr.Range()
141+
142+
if err := runner.EmitIssueWithFix(
143+
r,
144+
fmt.Sprintf(`Module source %q should enable shallow cloning by adding "depth=1" parameter`, module.Source),
145+
exprRange,
146+
func(f tflint.Fixer) error {
147+
// Find the position of "ref=" in the source string
148+
refPos := strings.Index(module.Source, "ref=")
149+
if refPos == -1 {
150+
return fmt.Errorf(`could not find "ref=" string in module source`)
151+
}
152+
153+
// Create a range that includes the opening quote + source up to "ref="
154+
endPos := exprRange.Start
155+
// +1 for opening quote, +refPos for chars before "ref="
156+
endPos.Byte += 1 + refPos
157+
endPos.Column += 1 + refPos
158+
159+
insertRange := hcl.Range{
160+
Filename: filename,
161+
Start: exprRange.Start,
162+
End: endPos,
163+
}
164+
165+
return f.InsertTextAfter(insertRange, "depth=1&")
166+
},
167+
); err != nil {
168+
return hcl.Diagnostics{
169+
{
170+
Severity: hcl.DiagError,
171+
Summary: "failed to call EmitIssueWithFix()",
172+
Detail: err.Error(),
173+
},
174+
}
175+
}
176+
177+
return nil
178+
}

0 commit comments

Comments
 (0)