Skip to content

Commit 03bc735

Browse files
committed
(feature): add statussubresource linter
Signed-off-by: Bryce Palmer <bpalmer@redhat.com>
1 parent 495588b commit 03bc735

File tree

9 files changed

+355
-0
lines changed

9 files changed

+355
-0
lines changed

README.md

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

276+
## StatusSubresource
277+
278+
The `statussubresource` linter checks that the status subresource is configured correctly for
279+
structs marked with the `kubebuilder:object:root:=true` marker. Correct configuration is that
280+
when there is a status field the `kubebuilder:subresource:status` marker is present on the struct
281+
OR when the `kubebuilder:subresource:status` marker is present on the struct there is a status field.
282+
283+
This linter is not enabled by default as it is only applicable to CustomResourceDefinitions.
284+
285+
### Fixes (via standalone binary only)
286+
287+
In the case where there is a status field present but no `kubebuilder:subresource:status` marker, the
288+
linter will suggest adding the comment `// +kubebuilder:subresource:status` above the struct.
289+
276290
# Contributing
277291

278292
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
@@ -12,6 +12,7 @@ import (
1212
"github.com/JoelSpeed/kal/pkg/analysis/nophase"
1313
"github.com/JoelSpeed/kal/pkg/analysis/optionalorrequired"
1414
"github.com/JoelSpeed/kal/pkg/analysis/requiredfields"
15+
"github.com/JoelSpeed/kal/pkg/analysis/statussubresource"
1516
"github.com/JoelSpeed/kal/pkg/config"
1617
"golang.org/x/tools/go/analysis"
1718

@@ -63,6 +64,7 @@ func NewRegistry() Registry {
6364
nophase.Initializer(),
6465
optionalorrequired.Initializer(),
6566
requiredfields.Initializer(),
67+
statussubresource.Initializer(),
6668
},
6769
}
6870
}

pkg/analysis/registry_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ var _ = Describe("Registry", func() {
4040
"nophase",
4141
"optionalorrequired",
4242
"requiredfields",
43+
"statussubresource",
4344
))
4445
})
4546
})
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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/extractjsontags"
10+
"github.com/JoelSpeed/kal/pkg/analysis/helpers/markers"
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+
statusJSONTag = "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+
errCouldNotGetJSONTags = errors.New("could not get json tags")
32+
)
33+
34+
type analyzer struct{}
35+
36+
// newAnalyzer creates a new analyzer with the given configuration.
37+
func newAnalyzer() *analysis.Analyzer {
38+
a := &analyzer{}
39+
40+
return &analysis.Analyzer{
41+
Name: name,
42+
Doc: "Checks that a type marked with kubebuilder:object:root:=true and containing a status field is marked with kubebuilder:subresource:status",
43+
Run: a.run,
44+
Requires: []*analysis.Analyzer{inspect.Analyzer, markers.Analyzer, extractjsontags.Analyzer},
45+
}
46+
}
47+
48+
func (a *analyzer) run(pass *analysis.Pass) (interface{}, error) {
49+
inspect, ok := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
50+
if !ok {
51+
return nil, errCouldNotGetInspector
52+
}
53+
54+
markersAccess, ok := pass.ResultOf[markers.Analyzer].(markers.Markers)
55+
if !ok {
56+
return nil, errCouldNotGetMarkers
57+
}
58+
59+
jsonTags, ok := pass.ResultOf[extractjsontags.Analyzer].(extractjsontags.StructFieldTags)
60+
if !ok {
61+
return nil, errCouldNotGetJSONTags
62+
}
63+
64+
// Filter to type specs so we can get the names of types
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+
// we only care about struct types
76+
sTyp, ok := typeSpec.Type.(*ast.StructType)
77+
if !ok {
78+
return
79+
}
80+
81+
// no identifier on the type
82+
if typeSpec.Name == nil {
83+
return
84+
}
85+
86+
structMarkers := markersAccess.StructMarkers(sTyp)
87+
a.checkStruct(pass, sTyp, typeSpec.Name.Name, structMarkers, jsonTags)
88+
})
89+
90+
return nil, nil //nolint:nilnil
91+
}
92+
93+
func (a *analyzer) checkStruct(pass *analysis.Pass, sTyp *ast.StructType, name string, structMarkers markers.MarkerSet, jsonTags extractjsontags.StructFieldTags) {
94+
if sTyp == nil {
95+
return
96+
}
97+
98+
if !structMarkers.HasWithValue(kubebuilderRootMarker) {
99+
return
100+
}
101+
102+
hasStatusSubresourceMarker := structMarkers.Has(kubebuilderStatusSubresourceMarker)
103+
hasStatusField := hasStatusField(sTyp, jsonTags)
104+
105+
switch {
106+
case (hasStatusSubresourceMarker && hasStatusField), (!hasStatusSubresourceMarker && !hasStatusField):
107+
// acceptable state
108+
case hasStatusSubresourceMarker && !hasStatusField:
109+
// Might be able to have some suggested fixes here, but it is likely much more complex
110+
// so for now leave it with a descriptive failure message.
111+
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)
112+
case !hasStatusSubresourceMarker && hasStatusField:
113+
// In this case we can suggest the autofix to add the status subresource marker
114+
pass.Report(analysis.Diagnostic{
115+
Pos: sTyp.Pos(),
116+
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),
117+
SuggestedFixes: []analysis.SuggestedFix{
118+
{
119+
Message: "should add the kubebuilder:subresource:status marker",
120+
TextEdits: []analysis.TextEdit{
121+
// go one line above the struct and add the marker
122+
{
123+
// sTyp.Pos() is the beginning of the 'struct' keyword. Subtract
124+
// the length of the struct name + 7 (2 for spaces surrounding type name, 4 for the 'type' keyword,
125+
// and 1 for the newline) to position at the end of the line above the struct
126+
// definition.
127+
Pos: sTyp.Pos() - token.Pos(len(name)+7),
128+
// prefix with a newline to ensure we aren't appending to a previous comment
129+
NewText: []byte("\n// +kubebuilder:subresource:status"),
130+
},
131+
},
132+
},
133+
},
134+
})
135+
}
136+
}
137+
138+
func hasStatusField(sTyp *ast.StructType, jsonTags extractjsontags.StructFieldTags) bool {
139+
if sTyp == nil {
140+
return false
141+
}
142+
143+
if sTyp.Fields == nil || sTyp.Fields.List == nil {
144+
return false
145+
}
146+
147+
for _, field := range sTyp.Fields.List {
148+
info := jsonTags.FieldTags(field)
149+
if info.Name == statusJSONTag {
150+
return true
151+
}
152+
}
153+
154+
return false
155+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 TestStatusSubresourceAnalyzer(t *testing.T) {
12+
testdata := analysistest.TestData()
13+
initializer := statussubresource.Initializer()
14+
15+
analyzer, err := initializer.Init(config.LintersConfig{})
16+
if err != nil {
17+
t.Fatal(err)
18+
}
19+
20+
analysistest.RunWithSuggestedFixes(t, testdata, analyzer, "a")
21+
}

pkg/analysis/statussubresource/doc.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
// This linter is not enabled by default as it is only applicable to CustomResourceDefinitions.
10+
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(), 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: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package a
2+
3+
// +kubebuilder:object:root:=true
4+
type Foo struct {
5+
Spec FooSpec `json:"spec"`
6+
}
7+
8+
type FooSpec struct {
9+
Name string `json:"name"`
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 `json:"spec"`
16+
}
17+
18+
type BarSpec struct {
19+
Name string `json:"name"`
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 `json:"spec"`
25+
Status BazStatus `json:"status,omitempty"`
26+
}
27+
28+
type BazSpec struct {
29+
Name string `json:"name"`
30+
}
31+
32+
type BazStatus struct {
33+
Name string `json:"name"`
34+
}
35+
36+
// +kubebuilder:object:root:=true
37+
// +kubebuilder:subresource:status
38+
type FooBar struct {
39+
Spec FooBarSpec `json:"spec"`
40+
Status FooBarStatus `json:"status"`
41+
}
42+
43+
type FooBarSpec struct {
44+
Name string `json:"name"`
45+
}
46+
47+
type FooBarStatus struct {
48+
Name string `json:"name"`
49+
}
50+
51+
// Test that it works with 'root=true' as well
52+
// +kubebuilder:object:root=true
53+
// +kubebuilder:subresource:status
54+
type FooBarBaz struct { // want "root object type \"FooBarBaz\" is marked to enable the status subresource with marker \"kubebuilder:subresource:status\" but has no status field"
55+
Spec FooBarBazSpec `json:"spec"`
56+
}
57+
58+
type FooBarBazSpec struct {
59+
Name string `json:"name"`
60+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package a
2+
3+
// +kubebuilder:object:root:=true
4+
type Foo struct {
5+
Spec FooSpec `json:"spec"`
6+
}
7+
8+
type FooSpec struct {
9+
Name string `json:"name"`
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 `json:"spec"`
16+
}
17+
18+
type BarSpec struct {
19+
Name string `json:"name"`
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 `json:"spec"`
26+
Status BazStatus `json:"status,omitempty"`
27+
}
28+
29+
type BazSpec struct {
30+
Name string `json:"name"`
31+
}
32+
33+
type BazStatus struct {
34+
Name string `json:"name"`
35+
}
36+
37+
// +kubebuilder:object:root:=true
38+
// +kubebuilder:subresource:status
39+
type FooBar struct {
40+
Spec FooBarSpec `json:"spec"`
41+
Status FooBarStatus `json:"status"`
42+
}
43+
44+
type FooBarSpec struct {
45+
Name string `json:"name"`
46+
}
47+
48+
type FooBarStatus struct {
49+
Name string `json:"name"`
50+
}
51+
52+
// Test that it works with 'root=true' as well
53+
// +kubebuilder:object:root=true
54+
// +kubebuilder:subresource:status
55+
type FooBarBaz struct { // want "root object type \"FooBarBaz\" is marked to enable the status subresource with marker \"kubebuilder:subresource:status\" but has no status field"
56+
Spec FooBarBazSpec `json:"spec"`
57+
}
58+
59+
type FooBarBazSpec struct {
60+
Name string `json:"name"`
61+
}

0 commit comments

Comments
 (0)