Skip to content

Commit 085d7df

Browse files
committed
expose client collection struct, add linter
1 parent 0785337 commit 085d7df

10 files changed

Lines changed: 174 additions & 16 deletions

File tree

.custom-gcl.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@ plugins:
1515
- module: 'github.com/stackitcloud/terraform-provider-stackit/tools'
1616
import: 'github.com/stackitcloud/terraform-provider-stackit/tools/linters/tfmodifyplan'
1717
path: ./tools
18+
- module: 'github.com/stackitcloud/terraform-provider-stackit/tools'
19+
import: 'github.com/stackitcloud/terraform-provider-stackit/tools/linters/tfclientcollection'
20+
path: ./tools
1821
# WARNING: when working with custom linting rules make sure to clear your cache when linting:
1922
# golangci-lint cache clean && make lint

golang-ci.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ linters:
1414
- tflogresponse # custom local linter
1515
- tfwriteid # custom local linter
1616
- tfmodifyplan # custom local linter
17+
- tfclientcollection # custom local linter
1718
- bodyclose
1819
- depguard
1920
- errorlint
@@ -46,6 +47,9 @@ linters:
4647
tfmodifyplan:
4748
type: module
4849
description: "A custom local linter"
50+
tfclientcollection:
51+
type: module
52+
description: "A custom local linter"
4953
depguard:
5054
rules:
5155
main:
@@ -112,6 +116,16 @@ linters:
112116
- third_party$
113117
- builtin$
114118
- examples$
119+
rules:
120+
- path: stackit/internal/core/
121+
linters:
122+
- tfclientcollection
123+
- path: stackit/internal/services/iam/rolebindings/v1/generic/
124+
linters:
125+
- tfclientcollection
126+
- path: stackit/internal/services/iam/rolebindings/v1/services/
127+
linters:
128+
- tfclientcollection
115129
formatters:
116130
settings:
117131
goimports:

stackit/internal/core/clientutils.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ type ClientFactory interface {
9393
newVpnV1Client() (vpn.DefaultAPI, error)
9494
}
9595

96-
func initClientCollection(clientFactory ClientFactory) (*clientCollection, error) {
96+
func initClientCollection(clientFactory ClientFactory) (*ClientCollection, error) {
9797
var g errgroup.Group
98-
cc := &clientCollection{}
98+
cc := &ClientCollection{}
9999

100100
// initialize clients in parallel
101101
g.Go(func() (err error) { cc.IaaSv2Client, err = clientFactory.newIaaSV2Client(); return err })

stackit/internal/core/clientutils_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func Test_initClientCollection(t *testing.T) {
1212
tests := []struct {
1313
name string
1414
args args
15-
want *clientCollection
15+
want *ClientCollection
1616
wantErr bool
1717
}{
1818
// TODO: Add test cases.

stackit/internal/core/core.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,10 @@ func NewProviderDataInternal(providerData ProviderData, clientFactory ClientFact
131131
type providerDataInternal struct {
132132
// providerData is the public provider data
133133
providerData ProviderData
134-
clients clientCollection
134+
clients ClientCollection
135135
}
136136

137-
type RoleBindingClientCollection = clientCollection
138-
139-
type clientCollection struct {
137+
type ClientCollection struct {
140138
IaaSv2Client iaasv2.DefaultAPI
141139
IaaSv2AlphaClient iaasv2alpha.DefaultAPI
142140
ResourceManagerClient resourcemanager.DefaultAPI
@@ -179,16 +177,16 @@ type clientCollection struct {
179177
ObservabilityV1Client observability.DefaultAPI
180178
}
181179

182-
func ParseProviderData(ctx context.Context, providerData any, diags *diag.Diagnostics) (ProviderData, clientCollection, bool) {
180+
func ParseProviderData(ctx context.Context, providerData any, diags *diag.Diagnostics) (ProviderData, ClientCollection, bool) {
183181
// Prevent panic if the provider has not been configured.
184182
if providerData == nil {
185-
return ProviderData{}, clientCollection{}, false
183+
return ProviderData{}, ClientCollection{}, false
186184
}
187185

188186
stackitProviderDataInternal, ok := providerData.(providerDataInternal)
189187
if !ok {
190188
LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Expected configure type core.providerDataInternal, got %T", providerData))
191-
return ProviderData{}, clientCollection{}, false
189+
return ProviderData{}, ClientCollection{}, false
192190
}
193191
return stackitProviderDataInternal.providerData, stackitProviderDataInternal.clients, true
194192
}

stackit/internal/services/iam/rolebindings/v1/generic/datasource.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ type RoleBindingDatasource[C any] struct {
4141
ResourceType string // e.g. "instance", ...
4242

4343
// callbacks for lifecyle handling
44-
ApiClientExtractor func(clientCollection core.RoleBindingClientCollection) C
44+
ApiClientExtractor func(clientCollection core.ClientCollection) C
4545
ExecReadRequest func(ctx context.Context, client C, region, resourceId string) ([]GenericRoleBindingResponse, error)
4646
}
4747

stackit/internal/services/iam/rolebindings/v1/generic/resource.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type RoleBindingResource[C any] struct {
4848
ResourceType string // e.g. "instance", ...
4949

5050
// callbacks for lifecyle handling
51-
ApiClientExtractor func(clientCollection core.RoleBindingClientCollection) C
51+
ApiClientExtractor func(clientCollection core.ClientCollection) C
5252
ExecReadRequest func(ctx context.Context, client C, region, resourceId, role, subject string) (GenericRoleBindingResponse, error)
5353
ExecCreateRequest func(ctx context.Context, client C, region, resourceId, role, subject string) (GenericRoleBindingResponse, error)
5454
ExecUpdateRequest func(ctx context.Context, client C, region, resourceId, role, subject string) (GenericRoleBindingResponse, error)

stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/instance.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func NewSecretsmanagerInstanceRoleBindingResource() resource.Resource {
1818
return &generic.RoleBindingResource[secretsmanagerV1Alpha.DefaultAPI]{
1919
ApiName: "secretsmanager",
2020
ResourceType: "instance",
21-
ApiClientExtractor: func(clientCollection core.RoleBindingClientCollection) secretsmanagerV1Alpha.DefaultAPI {
21+
ApiClientExtractor: func(clientCollection core.ClientCollection) secretsmanagerV1Alpha.DefaultAPI {
2222
return clientCollection.SecretsmanagerV1AlphaClient
2323
},
2424
ExecCreateRequest: func(ctx context.Context, client secretsmanagerV1Alpha.DefaultAPI, region, resourceId, role, subject string) (generic.GenericRoleBindingResponse, error) {
@@ -60,7 +60,7 @@ func NewSecretsmanagerInstanceRoleBindingsDatasource() datasource.DataSource {
6060
return &generic.RoleBindingDatasource[secretsmanagerV1Alpha.DefaultAPI]{
6161
ApiName: "secretsmanager",
6262
ResourceType: "instance",
63-
ApiClientExtractor: func(clientCollection core.RoleBindingClientCollection) secretsmanagerV1Alpha.DefaultAPI {
63+
ApiClientExtractor: func(clientCollection core.ClientCollection) secretsmanagerV1Alpha.DefaultAPI {
6464
return clientCollection.SecretsmanagerV1AlphaClient
6565
},
6666
ExecReadRequest: func(ctx context.Context, client secretsmanagerV1Alpha.DefaultAPI, region, resourceId string) ([]generic.GenericRoleBindingResponse, error) {

stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/secret_group.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func NewSecretsmanagerSecretGroupRoleBindingResource() resource.Resource {
1818
return &generic.RoleBindingResource[secretsmanagerV1Alpha.DefaultAPI]{
1919
ApiName: "secretsmanager",
2020
ResourceType: "secret_group",
21-
ApiClientExtractor: func(clientCollection core.RoleBindingClientCollection) secretsmanagerV1Alpha.DefaultAPI {
21+
ApiClientExtractor: func(clientCollection core.ClientCollection) secretsmanagerV1Alpha.DefaultAPI {
2222
return clientCollection.SecretsmanagerV1AlphaClient
2323
},
2424
ExecCreateRequest: func(ctx context.Context, client secretsmanagerV1Alpha.DefaultAPI, region, resourceId, role, subject string) (generic.GenericRoleBindingResponse, error) {
@@ -60,7 +60,7 @@ func NewSecretsmanagerSecretGroupRoleBindingsDatasource() datasource.DataSource
6060
return &generic.RoleBindingDatasource[secretsmanagerV1Alpha.DefaultAPI]{
6161
ApiName: "secretsmanager",
6262
ResourceType: "secret_group",
63-
ApiClientExtractor: func(clientCollection core.RoleBindingClientCollection) secretsmanagerV1Alpha.DefaultAPI {
63+
ApiClientExtractor: func(clientCollection core.ClientCollection) secretsmanagerV1Alpha.DefaultAPI {
6464
return clientCollection.SecretsmanagerV1AlphaClient
6565
},
6666
ExecReadRequest: func(ctx context.Context, client secretsmanagerV1Alpha.DefaultAPI, region, resourceId string) ([]generic.GenericRoleBindingResponse, error) {
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package tfclientcollection
2+
3+
import (
4+
"go/ast"
5+
"go/token"
6+
"go/types"
7+
8+
"github.com/golangci/plugin-module-register/register"
9+
"golang.org/x/tools/go/analysis"
10+
"golang.org/x/tools/go/analysis/passes/inspect"
11+
"golang.org/x/tools/go/ast/inspector"
12+
)
13+
14+
var Analyzer = &analysis.Analyzer{
15+
Name: "tfclientcollection",
16+
Doc: "Prevents the keeping the core.ClientCollection struct from being kept in a resource. Only clients should be extracted from it and kept.",
17+
Run: run,
18+
Requires: []*analysis.Analyzer{inspect.Analyzer},
19+
}
20+
21+
const (
22+
targetPkg = "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
23+
targetStruct = "ClientCollection"
24+
)
25+
26+
func run(pass *analysis.Pass) (any, error) {
27+
inspector, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
28+
if !ok || inspector == nil {
29+
return nil, nil
30+
}
31+
32+
nodeFilter := []ast.Node{
33+
(*ast.GenDecl)(nil),
34+
(*ast.StructType)(nil),
35+
(*ast.FuncType)(nil),
36+
}
37+
38+
inspector.Preorder(nodeFilter, func(n ast.Node) {
39+
switch node := n.(type) {
40+
// Checks 'var' declarations
41+
case *ast.GenDecl:
42+
if node.Tok != token.VAR {
43+
return
44+
}
45+
for _, spec := range node.Specs {
46+
vspec, ok := spec.(*ast.ValueSpec)
47+
if !ok {
48+
continue
49+
}
50+
for _, name := range vspec.Names {
51+
obj := pass.TypesInfo.ObjectOf(name)
52+
if obj != nil && isForbiddenType(obj.Type()) {
53+
pass.Reportf(
54+
name.Pos(),
55+
"variable %q declared via 'var' uses forbidden type %s.%s",
56+
name.Name,
57+
targetPkg,
58+
targetStruct,
59+
)
60+
}
61+
}
62+
}
63+
64+
// Checks struct fields
65+
case *ast.StructType:
66+
if node.Fields == nil {
67+
return
68+
}
69+
for _, field := range node.Fields.List {
70+
fieldType := pass.TypesInfo.TypeOf(field.Type)
71+
if isForbiddenType(fieldType) {
72+
pass.Reportf(
73+
field.Pos(),
74+
"struct field uses forbidden type %s.%s",
75+
targetPkg,
76+
targetStruct,
77+
)
78+
}
79+
}
80+
81+
// Checks function, method, and function literal parameters
82+
case *ast.FuncType:
83+
if node.Params == nil {
84+
return
85+
}
86+
for _, param := range node.Params.List {
87+
paramType := pass.TypesInfo.TypeOf(param.Type)
88+
if isForbiddenType(paramType) {
89+
pass.Reportf(
90+
param.Pos(),
91+
"function parameter uses forbidden type %s.%s",
92+
targetPkg,
93+
targetStruct,
94+
)
95+
}
96+
}
97+
}
98+
})
99+
100+
return nil, nil
101+
}
102+
103+
func isForbiddenType(t types.Type) bool {
104+
if t == nil {
105+
return false
106+
}
107+
// Unwrap variadic parameters (...pkg.ForbiddenStruct -> pkg.ForbiddenStruct)
108+
if slice, ok := t.(*types.Slice); ok {
109+
t = slice.Elem()
110+
}
111+
// Unwrap pointers (*pkg.Struct -> pkg.Struct)
112+
if ptr, ok := t.(*types.Pointer); ok {
113+
t = ptr.Elem()
114+
}
115+
named, ok := t.(*types.Named)
116+
if !ok {
117+
return false
118+
}
119+
obj := named.Obj()
120+
if obj == nil || obj.Pkg() == nil {
121+
return false
122+
}
123+
return obj.Pkg().Path() == targetPkg && obj.Name() == targetStruct
124+
}
125+
126+
func init() {
127+
register.Plugin("tfclientcollection", New)
128+
}
129+
130+
func New(settings any) (register.LinterPlugin, error) {
131+
return &plugin{}, nil
132+
}
133+
134+
type plugin struct{}
135+
136+
func (p *plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) {
137+
return []*analysis.Analyzer{Analyzer}, nil
138+
}
139+
140+
func (p *plugin) GetLoadMode() string {
141+
// LoadModeSyntax is required because we need to inspect the AST (Syntax trees)
142+
return register.LoadModeSyntax
143+
}

0 commit comments

Comments
 (0)