Skip to content

Commit ffb9a50

Browse files
committed
(feature): add statussubresource linter
Signed-off-by: everettraven <everettraven@gmail.com>
1 parent 52306d5 commit ffb9a50

File tree

12 files changed

+505
-0
lines changed

12 files changed

+505
-0
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,31 @@ It will suggest to remove the pointer from the field, and update the `json` tag
201201
If you prefer not to suggest fixes for pointers in required fields, you can change the `pointerPolicy` to `Warn`.
202202
The linter will then only suggest to remove the `omitempty` value from the `json` tag.
203203

204+
## StatusSubresource
205+
206+
The `statussubresource` linter checks that the status subresource is configured correctly for
207+
structs marked with the `kubebuilder:object:root:=true` marker. Correct configuration is that
208+
when there is a status field the `kubebuilder:subresource:status` marker is present on the struct
209+
OR when the `kubebuilder:subresource:status` marker is present on the struct there is a status field.
210+
211+
The status field name used to check for presence of a status field can be configured with
212+
the `statusFieldName` option. By default `statusFieldName` is set to `"Status"`.
213+
214+
This linter is not enabled by default as it is only applicable to CustomResourceDefinitions.
215+
216+
### Configuration
217+
218+
```yaml
219+
lintersConfig:
220+
statussubresource:
221+
statusFieldName: "Status"
222+
```
223+
224+
### Fixes (via standalone binary only)
225+
226+
In the case where there is a status field present but no `kubebuilder:subresource:status` marker, the
227+
linter will suggest adding the comment `// +kubebuilder:subresource:status` above the struct.
228+
204229
# Contributing
205230

206231
New linters can be added by following the [New Linter][new-linter] guide.

pkg/analysis/registry.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/JoelSpeed/kal/pkg/analysis/jsontags"
88
"github.com/JoelSpeed/kal/pkg/analysis/optionalorrequired"
99
"github.com/JoelSpeed/kal/pkg/analysis/requiredfields"
10+
"github.com/JoelSpeed/kal/pkg/analysis/statussubresource"
1011
"github.com/JoelSpeed/kal/pkg/config"
1112
"golang.org/x/tools/go/analysis"
1213

@@ -53,6 +54,7 @@ func NewRegistry() Registry {
5354
jsontags.Initializer(),
5455
optionalorrequired.Initializer(),
5556
requiredfields.Initializer(),
57+
statussubresource.Initializer(),
5658
},
5759
}
5860
}

pkg/analysis/registry_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ var _ = Describe("Registry", func() {
3232
"jsontags",
3333
"optionalorrequired",
3434
"requiredfields",
35+
"statussubresource",
3536
))
3637
})
3738
})
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package statussubresource
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"go/ast"
7+
"go/token"
8+
9+
"github.com/JoelSpeed/kal/pkg/analysis/helpers/markers"
10+
"github.com/JoelSpeed/kal/pkg/config"
11+
"golang.org/x/tools/go/analysis"
12+
"golang.org/x/tools/go/analysis/passes/inspect"
13+
"golang.org/x/tools/go/ast/inspector"
14+
)
15+
16+
const (
17+
name = "statussubresource"
18+
19+
DefaultStatusFieldName = "Status"
20+
21+
// KubebuilderRootMarker is the marker that indicates that a struct is the object root for code and CRD generation.
22+
KubebuilderRootMarker = "kubebuilder:object:root:=true"
23+
24+
// KubebuilderStatusSubresourceMarker is the marker that indicates that the CRD generated for a struct should include the /status subresource.
25+
KubebuilderStatusSubresourceMarker = "kubebuilder:subresource:status"
26+
)
27+
28+
var (
29+
errCouldNotGetInspector = errors.New("could not get inspector")
30+
errCouldNotGetMarkers = errors.New("could not get markers")
31+
)
32+
33+
type analyzer struct {
34+
statusFieldName string
35+
}
36+
37+
// newAnalyzer creates a new analyzer with the given configuration.
38+
func newAnalyzer(cfg config.StatusSubresourceConfig) *analysis.Analyzer {
39+
defaultConfig(&cfg)
40+
41+
a := &analyzer{
42+
statusFieldName: cfg.StatusFieldName,
43+
}
44+
45+
return &analysis.Analyzer{
46+
Name: name,
47+
Doc: "Checks that a type marked with kubebuilder:object:root:=true and containing a status field is marked with kubebuilder:subresource:status",
48+
Run: a.run,
49+
Requires: []*analysis.Analyzer{inspect.Analyzer, markers.Analyzer},
50+
}
51+
}
52+
53+
func (a *analyzer) run(pass *analysis.Pass) (interface{}, error) {
54+
inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
55+
if !ok {
56+
return nil, errCouldNotGetInspector
57+
}
58+
59+
markersAccess, ok := pass.ResultOf[markers.Analyzer].(markers.Markers)
60+
if !ok {
61+
return nil, errCouldNotGetMarkers
62+
}
63+
64+
// Filter to structs so that we can iterate over fields in a struct.
65+
nodeFilter := []ast.Node{
66+
(*ast.TypeSpec)(nil),
67+
}
68+
69+
inspect.Preorder(nodeFilter, func(n ast.Node) {
70+
typeSpec, ok := n.(*ast.TypeSpec)
71+
if !ok {
72+
return
73+
}
74+
75+
sTyp, ok := typeSpec.Type.(*ast.StructType)
76+
if !ok {
77+
return
78+
}
79+
80+
// no identifier on the type
81+
if typeSpec.Name == nil {
82+
return
83+
}
84+
85+
structMarkers := markersAccess.StructMarkers(sTyp)
86+
a.checkStruct(pass, sTyp, typeSpec.Name.Name, structMarkers)
87+
})
88+
89+
return nil, nil
90+
}
91+
92+
func (a *analyzer) checkStruct(pass *analysis.Pass, sTyp *ast.StructType, name string, structMarkers markers.MarkerSet) {
93+
if sTyp == nil {
94+
return
95+
}
96+
97+
if !structMarkers.Has(KubebuilderRootMarker) {
98+
return
99+
}
100+
101+
hasStatusSubresourceMarker := structMarkers.Has(KubebuilderStatusSubresourceMarker)
102+
hasStatusField := hasStatusField(sTyp.Fields, a.statusFieldName)
103+
104+
switch {
105+
case (hasStatusSubresourceMarker && hasStatusField), (!hasStatusSubresourceMarker && !hasStatusField):
106+
// acceptable state
107+
case hasStatusSubresourceMarker && !hasStatusField:
108+
// Might be able to have some suggested fixes here, but it is likely much more complex
109+
// so for now leave it with a descriptive failure message.
110+
pass.Reportf(sTyp.Pos(), "root object type %q is marked to enable the status subresource with marker %q but has no status field", name, KubebuilderStatusSubresourceMarker)
111+
case !hasStatusSubresourceMarker && hasStatusField:
112+
// In this case we can suggest the autofix to add the status subresource marker
113+
pass.Report(analysis.Diagnostic{
114+
Pos: sTyp.Pos(),
115+
Message: fmt.Sprintf("root object type %q has a status field but does not have the marker %q to enable the status subresource", name, KubebuilderStatusSubresourceMarker),
116+
SuggestedFixes: []analysis.SuggestedFix{
117+
{
118+
Message: "should add the kubebuilder:subresource:status marker",
119+
TextEdits: []analysis.TextEdit{
120+
// go one line above the struct and add the marker
121+
{
122+
Pos: sTyp.Pos() - token.Pos(len(name)) - token.Pos(2) - token.Pos(len("type")) - token.Pos(1),
123+
// prefix with a newline to ensure we aren't appending to a previous comment
124+
NewText: []byte("\n// +kubebuilder:subresource:status"),
125+
},
126+
},
127+
},
128+
},
129+
})
130+
}
131+
}
132+
133+
func hasStatusField(fieldList *ast.FieldList, statusFieldName string) bool {
134+
if fieldList == nil {
135+
return false
136+
}
137+
138+
for _, field := range fieldList.List {
139+
if field == nil || len(field.Names) == 0 {
140+
continue
141+
}
142+
143+
if field.Names[0] == nil {
144+
continue
145+
}
146+
147+
if field.Names[0].Name == statusFieldName {
148+
return true
149+
}
150+
}
151+
152+
return false
153+
}
154+
155+
func defaultConfig(cfg *config.StatusSubresourceConfig) {
156+
if cfg.StatusFieldName == "" {
157+
cfg.StatusFieldName = DefaultStatusFieldName
158+
}
159+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package statussubresource_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/JoelSpeed/kal/pkg/analysis/statussubresource"
7+
"github.com/JoelSpeed/kal/pkg/config"
8+
"golang.org/x/tools/go/analysis/analysistest"
9+
)
10+
11+
func TestStatusSubresourceAnalyzerWithDefaultConfig(t *testing.T) {
12+
testdata := analysistest.TestData()
13+
initializer := statussubresource.Initializer()
14+
analyzer, err := initializer.Init(config.LintersConfig{})
15+
if err != nil {
16+
t.Fatal(err)
17+
}
18+
19+
analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "a")
20+
}
21+
22+
func TestStatusSubresourceAnalyzerWithCustomStatusFieldName(t *testing.T) {
23+
testdata := analysistest.TestData()
24+
initializer := statussubresource.Initializer()
25+
analyzer, err := initializer.Init(config.LintersConfig{
26+
StatusSubresource: config.StatusSubresourceConfig{
27+
StatusFieldName: "MyCustomStatus",
28+
},
29+
})
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
34+
analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "b")
35+
}

pkg/analysis/statussubresource/doc.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// statussubresource is a linter to check that the status subresource is configured correctly for
2+
// structs marked with the 'kubebuilder:object:root:=true' marker. Correct configuration is that
3+
// when there is a status field the 'kubebuilder:subresource:status' marker is present on the struct
4+
// OR when the 'kubebuilder:subresource:status' marker is present on the struct there is a status field.
5+
//
6+
// In the case where there is a status field present but no 'kubebuilder:subresource:status' marker, the
7+
// linter will suggest adding the comment '// +kubebuilder:subresource:status' above the struct.
8+
//
9+
// The status field name used to check for presence of a status field can be configured with
10+
// the 'statusFieldName' option. By default 'statusFieldName' is set to 'Status'.
11+
//
12+
// This linter is not enabled by default as it is only applicable to CustomResourceDefinitions.
13+
package statussubresource
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package statussubresource
2+
3+
import (
4+
"github.com/JoelSpeed/kal/pkg/config"
5+
"golang.org/x/tools/go/analysis"
6+
)
7+
8+
// Initializer returns the AnalyzerInitializer for this
9+
// Analyzer so that it can be added to the registry.
10+
func Initializer() initializer {
11+
return initializer{}
12+
}
13+
14+
// intializer implements the AnalyzerInitializer interface.
15+
type initializer struct{}
16+
17+
// Name returns the name of the Analyzer.
18+
func (initializer) Name() string {
19+
return name
20+
}
21+
22+
// Init returns the intialized Analyzer.
23+
func (initializer) Init(cfg config.LintersConfig) (*analysis.Analyzer, error) {
24+
return newAnalyzer(cfg.StatusSubresource), nil
25+
}
26+
27+
// Default determines whether this Analyzer is on by default, or not.
28+
func (initializer) Default() bool {
29+
// This check only applies to CRDs so should not be on by default.
30+
return false
31+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package a
2+
3+
// +kubebuilder:object:root:=true
4+
type Foo struct {
5+
Spec FooSpec
6+
}
7+
8+
type FooSpec struct {
9+
Name string
10+
}
11+
12+
// +kubebuilder:object:root:=true
13+
// +kubebuilder:subresource:status
14+
type Bar struct { // want "root object type \"Bar\" is marked to enable the status subresource with marker \"kubebuilder:subresource:status\" but has no status field"
15+
Spec BarSpec
16+
}
17+
18+
type BarSpec struct {
19+
Name string
20+
}
21+
22+
// +kubebuilder:object:root:=true
23+
type Baz struct { // want "root object type \"Baz\" has a status field but does not have the marker \"kubebuilder:subresource:status\" to enable the status subresource"
24+
Spec BazSpec
25+
Status BazStatus
26+
}
27+
28+
type BazSpec struct {
29+
Name string
30+
}
31+
32+
type BazStatus struct {
33+
Name string
34+
}
35+
36+
// +kubebuilder:object:root:=true
37+
// +kubebuilder:subresource:status
38+
type FooBar struct {
39+
Spec FooBarSpec
40+
Status FooBarStatus
41+
}
42+
43+
type FooBarSpec struct {
44+
Name string
45+
}
46+
47+
type FooBarStatus struct {
48+
Name string
49+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package a
2+
3+
// +kubebuilder:object:root:=true
4+
type Foo struct {
5+
Spec FooSpec
6+
}
7+
8+
type FooSpec struct {
9+
Name string
10+
}
11+
12+
// +kubebuilder:object:root:=true
13+
// +kubebuilder:subresource:status
14+
type Bar struct { // want "root object type \"Bar\" is marked to enable the status subresource with marker \"kubebuilder:subresource:status\" but has no status field"
15+
Spec BarSpec
16+
}
17+
18+
type BarSpec struct {
19+
Name string
20+
}
21+
22+
// +kubebuilder:object:root:=true
23+
// +kubebuilder:subresource:status
24+
type Baz struct { // want "root object type \"Baz\" has a status field but does not have the marker \"kubebuilder:subresource:status\" to enable the status subresource"
25+
Spec BazSpec
26+
Status BazStatus
27+
}
28+
29+
type BazSpec struct {
30+
Name string
31+
}
32+
33+
type BazStatus struct {
34+
Name string
35+
}
36+
37+
// +kubebuilder:object:root:=true
38+
// +kubebuilder:subresource:status
39+
type FooBar struct {
40+
Spec FooBarSpec
41+
Status FooBarStatus
42+
}
43+
44+
type FooBarSpec struct {
45+
Name string
46+
}
47+
48+
type FooBarStatus struct {
49+
Name string
50+
}

0 commit comments

Comments
 (0)