Skip to content

Commit ea0eb3f

Browse files
Add rule to ensure unique TLS app names
Duplicate names can cause all sorts of issues, see tickets for details Ticket: DENA-991
1 parent 108a928 commit ea0eb3f

File tree

5 files changed

+309
-0
lines changed

5 files changed

+309
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ plugin "uw-kafka-config" {
2323
| [`msk_app_topics`](rules/msk_app_topics.md) | Requires apps consume from and produce to only topics define in their module. |
2424
| [`msk_topic_name`](rules/msk_topic_name.md) | Requires defined topics in a module to belong to that team. |
2525
| [`msk_topic_config`](rules/msk_topic_config.md) | Checks the configuration for MSK topics |
26+
| [`msk_unique_app_names`](rules/msk_unique_app_names.md) | Checks that TLS app names are unique |
2627

2728

2829
## Building the plugin

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func main() {
2020
&rules.MSKAppTopicsRule{},
2121
&rules.MSKTopicNameRule{},
2222
&rules.MSKTopicConfigRule{},
23+
&rules.MSKUniqueAppNamesRule{},
2324
},
2425
},
2526
})

rules/msk_unique_app_names.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package rules
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/hcl/v2/gohcl"
7+
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
8+
"github.com/terraform-linters/tflint-plugin-sdk/logger"
9+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
10+
)
11+
12+
const commonNameAttribute = "cert_common_name"
13+
14+
type MSKUniqueAppNamesRule struct {
15+
tflint.DefaultRule
16+
}
17+
18+
func (r *MSKUniqueAppNamesRule) Name() string {
19+
return "msk_unique_app_names"
20+
}
21+
22+
func (r *MSKUniqueAppNamesRule) Enabled() bool {
23+
return true
24+
}
25+
26+
func (r *MSKUniqueAppNamesRule) Link() string {
27+
return ReferenceLink(r.Name())
28+
}
29+
30+
func (r *MSKUniqueAppNamesRule) Severity() tflint.Severity {
31+
return tflint.ERROR
32+
}
33+
34+
func (r *MSKUniqueAppNamesRule) Check(runner tflint.Runner) error {
35+
isRoot, err := isRootModule(runner)
36+
if err != nil {
37+
return err
38+
}
39+
if !isRoot {
40+
logger.Debug("skipping child module")
41+
return nil
42+
}
43+
44+
TLSAppModules, err := getTLSAppModules(runner)
45+
if err != nil {
46+
return err
47+
}
48+
49+
return r.reportDuplicateTLSAppNames(runner, TLSAppModules)
50+
}
51+
52+
func getTLSAppModules(runner tflint.Runner) (hclext.Blocks, error) {
53+
modules, err := runner.GetModuleContent(
54+
&hclext.BodySchema{
55+
Blocks: []hclext.BlockSchema{
56+
{
57+
Type: "module",
58+
LabelNames: []string{"name"},
59+
Body: &hclext.BodySchema{
60+
Attributes: []hclext.AttributeSchema{
61+
{Name: commonNameAttribute},
62+
},
63+
},
64+
},
65+
},
66+
},
67+
nil,
68+
)
69+
if err != nil {
70+
return nil, fmt.Errorf("getting modules: %w", err)
71+
}
72+
73+
var TLSAppModules hclext.Blocks
74+
for _, moduleBlock := range modules.Blocks {
75+
if _, ok := moduleBlock.Body.Attributes[commonNameAttribute]; ok {
76+
TLSAppModules = append(TLSAppModules, moduleBlock)
77+
}
78+
}
79+
80+
return TLSAppModules, nil
81+
}
82+
83+
type tlsAppName struct {
84+
attr *hclext.Attribute
85+
name string
86+
}
87+
88+
func (r *MSKUniqueAppNamesRule) reportDuplicateTLSAppNames(runner tflint.Runner, tlsAppModules hclext.Blocks) error {
89+
seenNames := map[string]struct{}{}
90+
duplicateNames := []tlsAppName{}
91+
for _, appModule := range tlsAppModules {
92+
appNameAttr := appModule.Body.Attributes[commonNameAttribute]
93+
94+
var appName string
95+
diags := gohcl.DecodeExpression(appNameAttr.Expr, nil, &appName)
96+
if diags.HasErrors() {
97+
return fmt.Errorf("decoding expression for attribute %s: %w", commonNameAttribute, diags)
98+
}
99+
100+
if _, ok := seenNames[appName]; ok {
101+
duplicateNames = append(duplicateNames, tlsAppName{attr: appNameAttr, name: appName})
102+
continue
103+
}
104+
105+
seenNames[appName] = struct{}{}
106+
}
107+
108+
for _, appName := range duplicateNames {
109+
if err := runner.EmitIssue(
110+
r,
111+
fmt.Sprintf(
112+
"'%s' must be unique across a module, but '%s' has already been seen",
113+
commonNameAttribute,
114+
appName.name,
115+
),
116+
appName.attr.Range,
117+
); err != nil {
118+
return fmt.Errorf("emitting issue: %w", err)
119+
}
120+
}
121+
122+
return nil
123+
}

rules/msk_unique_app_names.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# `msk_unique_app_names`
2+
3+
## Requirements
4+
5+
Requires all modules using the `tls-app` in our Kafka cluster config provide a
6+
unique name for the `cert_common_name`. This is because this name is used to
7+
identify the ACLs for the modules.
8+
9+
## Example
10+
11+
### Bad example
12+
13+
``` hcl
14+
module "my_team_example_producer" {
15+
source = "../../../modules/tls-app"
16+
produce_topics = [kafka_topic.some_topic.name]
17+
cert_common_name = "pubsub/example-app"
18+
}
19+
20+
module "my_team_example_consumer" {
21+
source = "../../../modules/tls-app"
22+
consume_topics = [kafka_topic.some_topic.name]
23+
# BAD: cert_common_name is same as module above
24+
cert_common_name = "pubsub/example-app"
25+
}
26+
```
27+
28+
### Good example
29+
30+
``` hcl
31+
module "my_team_example_producer" {
32+
source = "../../../modules/tls-app"
33+
produce_topics = [kafka_topic.some_topic.name]
34+
cert_common_name = "pubsub/example-producer"
35+
}
36+
37+
module "my_team_example_consumer" {
38+
source = "../../../modules/tls-app"
39+
consume_topics = [kafka_topic.some_topic.name]
40+
# GOOD: cert_common_name is unique
41+
cert_common_name = "pubsub/example-consumer"
42+
}
43+
```

rules/msk_unique_app_names_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package rules
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/hcl/v2"
7+
"github.com/stretchr/testify/require"
8+
"github.com/terraform-linters/tflint-plugin-sdk/helper"
9+
)
10+
11+
func Test_MSKUniqueAppNamesRule(t *testing.T) {
12+
rule := &MSKUniqueAppNamesRule{}
13+
14+
for _, tc := range []struct {
15+
name string
16+
files map[string]string
17+
expected helper.Issues
18+
}{
19+
{
20+
name: "reports duplicate app names in same file",
21+
files: map[string]string{
22+
"file.tf": `
23+
module "first_app" {
24+
source = "../../../modules/tls-app"
25+
cert_common_name = "my-namespace/my-app"
26+
}
27+
28+
module "second_app" {
29+
source = "../../../modules/tls-app"
30+
cert_common_name = "my-namespace/my-app"
31+
}
32+
`,
33+
},
34+
expected: []*helper.Issue{
35+
{
36+
Rule: rule,
37+
Message: "'cert_common_name' must be unique across a module, but 'my-namespace/my-app' has already been seen",
38+
Range: hcl.Range{
39+
Filename: "file.tf",
40+
Start: hcl.Pos{Line: 9, Column: 3},
41+
End: hcl.Pos{Line: 9, Column: 43},
42+
},
43+
},
44+
},
45+
},
46+
{
47+
name: "reports duplicate app names across files",
48+
files: map[string]string{
49+
"first.tf": `
50+
module "first_app" {
51+
source = "../../../modules/tls-app"
52+
cert_common_name = "my-namespace/my-app"
53+
}
54+
`,
55+
"second.tf": `
56+
module "second_app" {
57+
source = "../../../modules/tls-app"
58+
cert_common_name = "my-namespace/my-app"
59+
}
60+
`,
61+
},
62+
expected: []*helper.Issue{
63+
{
64+
Rule: rule,
65+
Message: "'cert_common_name' must be unique across a module, but 'my-namespace/my-app' has already been seen",
66+
Range: hcl.Range{
67+
Filename: "second.tf",
68+
Start: hcl.Pos{Line: 4, Column: 3},
69+
End: hcl.Pos{Line: 4, Column: 43},
70+
},
71+
},
72+
},
73+
},
74+
{
75+
name: "reports repeated duplicate app names",
76+
files: map[string]string{
77+
"file.tf": `
78+
module "first_app" {
79+
source = "../../../modules/tls-app"
80+
cert_common_name = "my-namespace/my-app"
81+
}
82+
83+
module "second_app" {
84+
source = "../../../modules/tls-app"
85+
cert_common_name = "my-namespace/my-app"
86+
}
87+
88+
module "third_app" {
89+
source = "../../../modules/tls-app"
90+
cert_common_name = "my-namespace/my-app"
91+
}
92+
`,
93+
},
94+
expected: []*helper.Issue{
95+
{
96+
Rule: rule,
97+
Message: "'cert_common_name' must be unique across a module, but 'my-namespace/my-app' has already been seen",
98+
Range: hcl.Range{
99+
Filename: "file.tf",
100+
Start: hcl.Pos{Line: 9, Column: 3},
101+
End: hcl.Pos{Line: 9, Column: 43},
102+
},
103+
},
104+
{
105+
Rule: rule,
106+
Message: "'cert_common_name' must be unique across a module, but 'my-namespace/my-app' has already been seen",
107+
Range: hcl.Range{
108+
Filename: "file.tf",
109+
Start: hcl.Pos{Line: 14, Column: 3},
110+
End: hcl.Pos{Line: 14, Column: 43},
111+
},
112+
},
113+
},
114+
},
115+
{
116+
name: "Reports nothing with all unique names",
117+
files: map[string]string{
118+
"file.tf": `
119+
module "first_app" {
120+
source = "../../../modules/tls-app"
121+
cert_common_name = "my-namespace/first-app"
122+
}
123+
124+
module "second_app" {
125+
source = "../../../modules/tls-app"
126+
cert_common_name = "my-namespace/second-app"
127+
}
128+
`,
129+
},
130+
expected: []*helper.Issue{},
131+
},
132+
} {
133+
t.Run(tc.name, func(t *testing.T) {
134+
runner := helper.TestRunner(t, tc.files)
135+
136+
require.NoError(t, rule.Check(runner))
137+
138+
helper.AssertIssues(t, tc.expected, runner.Issues)
139+
})
140+
}
141+
}

0 commit comments

Comments
 (0)