Skip to content

Commit ea97177

Browse files
author
Phillip Wittrock
authored
Merge pull request #182 from droot/verify-rbac-annotations
Verify rbac annotations
2 parents e3dfa25 + 565b835 commit ea97177

File tree

15 files changed

+325
-37
lines changed

15 files changed

+325
-37
lines changed

cmd/internal/codegen/parse/parser.go

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@ limitations under the License.
1717
package parse
1818

1919
import (
20+
"bufio"
21+
"fmt"
2022
"go/build"
23+
"log"
24+
"os"
2125
"path/filepath"
2226
"strings"
23-
"os"
24-
"bufio"
2527

2628
"github.com/golang/glog"
27-
2829
"github.com/kubernetes-sigs/kubebuilder/cmd/internal/codegen"
30+
"github.com/markbates/inflect"
2931
"github.com/pkg/errors"
3032
rbacv1 "k8s.io/api/rbac/v1"
3133
"k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -68,11 +70,90 @@ func NewAPIs(context *generator.Context, arguments *args.GeneratorArgs) *APIs {
6870
b.parseControllers()
6971
b.parseRBAC()
7072
b.parseInformers()
73+
b.verifyRBACAnnotations()
7174
b.parseAPIs()
7275
b.parseCRDs()
7376
return b
7477
}
7578

79+
// verifyRBACAnnotations verifies that there are corresponding RBAC annotations for
80+
// each informer annotation.
81+
// e.g. if there is an // +kubebuilder:informer annotation for Pods, then there
82+
// should also be a // +kubebuilder:rbac annotation for Pods
83+
func (b *APIs) verifyRBACAnnotations() {
84+
parseOption := b.arguments.CustomArgs.(*ParseOptions)
85+
if parseOption.SkipRBACValidation {
86+
log.Println("skipping RBAC validations")
87+
return
88+
}
89+
err := rbacMatchesInformers(b.Informers, b.Rules)
90+
if err != nil {
91+
log.Fatal(err)
92+
}
93+
}
94+
95+
func rbacMatchesInformers(informers map[v1.GroupVersionKind]bool, rbacRules []rbacv1.PolicyRule) error {
96+
rs := inflect.NewDefaultRuleset()
97+
98+
// For each informer, look for the RBAC annotation
99+
for gvk := range informers {
100+
found := false
101+
102+
// Search all RBAC rules for one that matches the informer group and resource
103+
for _, rule := range rbacRules {
104+
105+
// Check if the group matches the informer group
106+
groupFound := false
107+
for _, g := range rule.APIGroups {
108+
// RBAC has the full group with domain, whereas informers do not. Strip the domain
109+
// from the group before comparing.
110+
parts := strings.Split(g, ".")
111+
group := parts[len(parts)-1]
112+
113+
// If the RBAC group is wildcard or matches, it is a match
114+
if g == "*" || group == gvk.Group {
115+
groupFound = true
116+
break
117+
}
118+
// Edge case where "core" and "" are equivalent
119+
if (group == "core" || group == "") && (gvk.Group == "core" || gvk.Group == "") {
120+
groupFound = true
121+
break
122+
}
123+
}
124+
if !groupFound {
125+
continue
126+
}
127+
128+
// The resource name is the lower-plural of the Kind
129+
resource := rs.Pluralize(strings.ToLower(gvk.Kind))
130+
// Check if the resource matches the informer resource
131+
resourceFound := false
132+
for _, k := range rule.Resources {
133+
// If the RBAC resource is a wildcard or matches the informer resource, it is a match
134+
if k == "*" || k == resource {
135+
resourceFound = true
136+
break
137+
}
138+
}
139+
if !resourceFound {
140+
continue
141+
}
142+
143+
// Found a matching RBAC rule
144+
found = true
145+
break
146+
}
147+
if !found {
148+
return fmt.Errorf("Missing rbac rule for %s.%s. Add with // +kubebuilder:rbac:groups=%s,"+
149+
"resources=%s,verbs=get;list;watch comment on controller struct "+
150+
"or run the command with '--skip-rbac-validation' arg", gvk.Group, gvk.Kind, gvk.Group,
151+
inflect.NewDefaultRuleset().Pluralize(strings.ToLower(gvk.Kind)))
152+
}
153+
}
154+
return nil
155+
}
156+
76157
// parseGroupNames initializes b.GroupNames with the set of all groups
77158
func (b *APIs) parseGroupNames() {
78159
b.GroupNames = sets.String{}
@@ -129,10 +210,10 @@ func (b *APIs) parseDomain() {
129210

130211
func parseDomainFromFiles(paths []string) string {
131212
var domain string
132-
for _, path := range paths {
133-
if strings.HasSuffix(path, "pkg/apis") {
134-
filePath := strings.Join([]string{build.Default.GOPATH, "src", path, "doc.go"}, "/")
135-
lines := []string{}
213+
for _, path := range paths {
214+
if strings.HasSuffix(path, "pkg/apis") {
215+
filePath := strings.Join([]string{build.Default.GOPATH, "src", path, "doc.go"}, "/")
216+
lines := []string{}
136217

137218
file, err := os.Open(filePath)
138219
if err != nil {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
Copyright 2018 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package parse
17+
18+
import (
19+
"testing"
20+
21+
rbacv1 "k8s.io/api/rbac/v1"
22+
"k8s.io/apimachinery/pkg/apis/meta/v1"
23+
)
24+
25+
func TestrbacMatchesInformers(t *testing.T) {
26+
tests := []struct {
27+
informers map[v1.GroupVersionKind]bool
28+
rbacRules []rbacv1.PolicyRule
29+
expErr bool
30+
}{
31+
{
32+
// informer resource matches the RBAC rule
33+
informers: map[v1.GroupVersionKind]bool{
34+
v1.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}: true,
35+
},
36+
rbacRules: []rbacv1.PolicyRule{
37+
{APIGroups: []string{"apps"}, Resources: []string{"deployments"}},
38+
},
39+
expErr: false,
40+
},
41+
{
42+
// RBAC rule does not match the informer resource because of missing pluralization in RBAC rules
43+
informers: map[v1.GroupVersionKind]bool{
44+
v1.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}: true,
45+
},
46+
rbacRules: []rbacv1.PolicyRule{
47+
{APIGroups: []string{"apps"}, Resources: []string{"Deployment"}},
48+
},
49+
expErr: true,
50+
},
51+
{
52+
// wild-card RBAC rule should match any resource in the group
53+
informers: map[v1.GroupVersionKind]bool{
54+
v1.GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"}: true,
55+
v1.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}: true,
56+
},
57+
rbacRules: []rbacv1.PolicyRule{
58+
{APIGroups: []string{"apps"}, Resources: []string{"*"}},
59+
},
60+
expErr: false,
61+
},
62+
{
63+
// empty group name is normalized to "core"
64+
informers: map[v1.GroupVersionKind]bool{
65+
v1.GroupVersionKind{Group: "core", Version: "v1", Kind: "Pod"}: true,
66+
},
67+
rbacRules: []rbacv1.PolicyRule{
68+
{APIGroups: []string{""}, Resources: []string{"pods"}},
69+
},
70+
expErr: false,
71+
},
72+
{
73+
// empty group name is normalized to "core"
74+
informers: map[v1.GroupVersionKind]bool{
75+
v1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}: true,
76+
},
77+
rbacRules: []rbacv1.PolicyRule{
78+
{APIGroups: []string{"core"}, Resources: []string{"pods"}},
79+
},
80+
expErr: false,
81+
},
82+
}
83+
84+
for _, test := range tests {
85+
err := checkRBACMatchesInformers(test.informers, test.rbacRules)
86+
if test.expErr {
87+
if err == nil {
88+
t.Errorf("RBAC rules %+v shouldn't match with informers %+v", test.rbacRules, test.informers)
89+
}
90+
} else {
91+
if err != nil {
92+
t.Errorf("RBAC rules %+v should match informers %+v, but got a mismatch error: %v", test.rbacRules, test.informers, err)
93+
}
94+
}
95+
}
96+
}

cmd/internal/codegen/parse/rbac.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,15 @@ func parseRBACTag(tag string) rbacv1.PolicyRule {
6464
values = strings.Split(value, ";")
6565
switch kv[0] {
6666
case "groups":
67-
result.APIGroups = values
67+
normalized := []string{}
68+
for _, v := range values {
69+
if v == "core" {
70+
normalized = append(normalized, "")
71+
} else {
72+
normalized = append(normalized, v)
73+
}
74+
}
75+
result.APIGroups = normalized
6876
case "resources":
6977
result.Resources = values
7078
case "verbs":
@@ -112,6 +120,9 @@ func parseInformerTag(tag string) v1.GroupVersionKind {
112120
value := kv[1]
113121
switch kv[0] {
114122
case "group":
123+
if value == "" {
124+
value = "core"
125+
}
115126
result.Group = value
116127
case "version":
117128
result.Version = value

cmd/internal/codegen/parse/util.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ import (
2727
)
2828

2929
type ParseOptions struct {
30-
SkipMapValidation bool
30+
SkipMapValidation bool
31+
32+
// SkipRBACValidation flag determines whether to check RBAC annotations
33+
// for the controller or not at parse stage.
34+
SkipRBACValidation bool
3135
}
3236

3337
// IsAPIResource returns true if t has a +resource/+kubebuilder:resource comment tag

cmd/kubebuilder-gen/codegen/run/generator.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import (
2020
"github.com/golang/glog"
2121
"github.com/kubernetes-sigs/kubebuilder/cmd/internal/codegen/parse"
2222
"github.com/kubernetes-sigs/kubebuilder/cmd/kubebuilder-gen/codegen"
23+
"github.com/spf13/pflag"
2324
"k8s.io/gengo/args"
2425
"k8s.io/gengo/generator"
25-
"github.com/spf13/pflag"
2626
)
2727

2828
// CodeGenerator generates code for Kubernetes resources and controllers
@@ -54,6 +54,8 @@ func (g *CodeGenerator) Execute() error {
5454
customArgs := &parse.ParseOptions{}
5555
pflag.CommandLine.BoolVar(&customArgs.SkipMapValidation, "skip-map-validation", true,
5656
"if set to true, skip generating validation schema for map type in CRD.")
57+
pflag.CommandLine.BoolVar(&customArgs.SkipRBACValidation, "skip-rbac-validation", false,
58+
"if set to true, skip validation for RBAC annotations for the controller.")
5759
arguments.CustomArgs = customArgs
5860

5961
arguments.OutputFileBaseName = g.OutputFileBaseName

cmd/kubebuilder/create/controller/controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ func (bc *{{.Kind}}Controller) Reconcile(k types.ReconcileKey) error {
7979
return nil
8080
}
8181
{{if .CoreType}}// +kubebuilder:informers:group={{ .Group }},version={{ .Version }},kind={{ .Kind }}{{end}}
82+
{{if .CoreType}}// +kubebuilder:rbac:groups={{ .Group }},resources={{ .Resource }},verbs=get;watch;list{{end}}
8283
// +kubebuilder:controller:group={{ .Group }},version={{ .Version }},kind={{ .Kind}},resource={{ .Resource }}
8384
type {{.Kind}}Controller struct {
8485
// INSERT ADDITIONAL FIELDS HERE

cmd/kubebuilder/create/controller/run.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,20 @@ import (
2121
"log"
2222
"os"
2323

24+
"strings"
25+
2426
createutil "github.com/kubernetes-sigs/kubebuilder/cmd/kubebuilder/create/util"
2527
generatecmd "github.com/kubernetes-sigs/kubebuilder/cmd/kubebuilder/generate"
2628
"github.com/kubernetes-sigs/kubebuilder/cmd/kubebuilder/util"
2729
"github.com/markbates/inflect"
2830
"github.com/spf13/cobra"
29-
"strings"
3031
)
3132

3233
type ControllerArguments struct {
33-
nonNamespacedKind bool
34-
generate bool
35-
CoreType bool
34+
nonNamespacedKind bool
35+
generate bool
36+
CoreType bool
37+
SkipRBACValidation bool
3638
}
3739

3840
func AddCreateController(cmd *cobra.Command) {
@@ -59,6 +61,7 @@ kubebuilder create controller --group apps --version v1beta2 --kind Deployment -
5961
createControllerCmd.Flags().BoolVar(&c.nonNamespacedKind, "non-namespaced", false, "if set, the API kind will be non namespaced")
6062
createControllerCmd.Flags().BoolVar(&c.generate, "generate", true, "generate controller code")
6163
createControllerCmd.Flags().BoolVar(&c.CoreType, "core-type", false, "generate controller for core type")
64+
createControllerCmd.Flags().BoolVar(&c.SkipRBACValidation, "skip-rbac-validation", false, "if set to true, skip validation for RBAC annotations for the controller.")
6265
cmd.AddCommand(createControllerCmd)
6366
}
6467

@@ -77,6 +80,9 @@ func (c *ControllerArguments) RunCreateController(cmd *cobra.Command, args []str
7780
if c.generate {
7881
fmt.Printf("Generating code for new controller... " +
7982
"Regenerate after editing controller files by running `kubebuilder generate clean; kubebuilder generate`.\n")
83+
if c.SkipRBACValidation {
84+
args = append(args, "--skip-rbac-validation")
85+
}
8086
generatecmd.RunGenerate(cmd, args)
8187
}
8288
fmt.Printf("Next: Run the controller and create an instance with:\n" +

cmd/kubebuilder/create/resource/run.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ import (
3030
"github.com/spf13/cobra"
3131
)
3232

33-
var nonNamespacedKind bool
34-
var controller bool
35-
var generate bool
33+
var (
34+
nonNamespacedKind bool
35+
controller bool
36+
generate bool
37+
skipRBACValidation bool
38+
)
3639

3740
var createResourceCmd = &cobra.Command{
3841
Use: "resource",
@@ -63,6 +66,7 @@ func AddCreateResource(cmd *cobra.Command) {
6366
createResourceCmd.Flags().BoolVar(&controller, "controller", true, "if true, generate the controller code for the resource")
6467
createResourceCmd.Flags().BoolVar(&generate, "generate", true, "generate source code")
6568
createResourceCmd.Flags().BoolVar(&createutil.AllowPluralKind, "plural-kind", false, "allow the kind to be plural")
69+
createResourceCmd.Flags().BoolVar(&skipRBACValidation, "skip-rbac-validation", false, "if set to true, skip validation for RBAC annotations")
6670
cmd.AddCommand(createResourceCmd)
6771
}
6872

@@ -83,6 +87,9 @@ func RunCreateResource(cmd *cobra.Command, args []string) {
8387
if generate {
8488
fmt.Printf("Generating code for new resource... " +
8589
"Regenerate after editing resources files by running `kubebuilder build generated`.\n")
90+
if skipRBACValidation {
91+
args = append(args, "--skip-rbac-validation")
92+
}
8693
generatecmd.RunGenerate(cmd, args)
8794
}
8895
fmt.Printf("Next: Install the API, run the controller and create an instance with:\n" +
@@ -115,7 +122,10 @@ func createResource(boilerplate string) {
115122

116123
if controller {
117124
fmt.Printf("Creating controller ...\n")
118-
c := controllerct.ControllerArguments{CoreType: false}
125+
c := controllerct.ControllerArguments{
126+
CoreType: false,
127+
SkipRBACValidation: skipRBACValidation,
128+
}
119129
c.CreateController(boilerplate)
120130
}
121131

0 commit comments

Comments
 (0)