diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index bba8d4656cc..c2d6e8392b2 100755 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -3802,6 +3802,8 @@ run_clusterroles_tests() { kubectl create "${kube_flags[@]}" clusterrole url-reader --verb=get --non-resource-url=/logs/* --non-resource-url=/healthz/* kube::test::get_object_assert clusterrole/url-reader "{{range.rules}}{{range.verbs}}{{.}}:{{end}}{{end}}" 'get:' kube::test::get_object_assert clusterrole/url-reader "{{range.rules}}{{range.nonResourceURLs}}{{.}}:{{end}}{{end}}" '/logs/\*:/healthz/\*:' + kubectl create "${kube_flags[@]}" clusterrole aggregation-reader --aggregation-rule="foo1=foo2" + kube::test::get_object_assert clusterrole/aggregation-reader "{{$id_field}}" 'aggregation-reader' # test `kubectl create clusterrolebinding` # test `kubectl set subject clusterrolebinding` diff --git a/pkg/kubectl/cmd/create/BUILD b/pkg/kubectl/cmd/create/BUILD index 131dc94981d..4b54ade35f8 100644 --- a/pkg/kubectl/cmd/create/BUILD +++ b/pkg/kubectl/cmd/create/BUILD @@ -45,6 +45,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/flag:go_default_library", "//vendor/k8s.io/client-go/dynamic:go_default_library", "//vendor/k8s.io/client-go/kubernetes/typed/batch/v1:go_default_library", "//vendor/k8s.io/client-go/kubernetes/typed/rbac/v1:go_default_library", diff --git a/pkg/kubectl/cmd/create/create_clusterrole.go b/pkg/kubectl/cmd/create/create_clusterrole.go index e87e771f120..4f3002549fc 100644 --- a/pkg/kubectl/cmd/create/create_clusterrole.go +++ b/pkg/kubectl/cmd/create/create_clusterrole.go @@ -24,6 +24,7 @@ import ( rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilflag "k8s.io/apiserver/pkg/util/flag" "k8s.io/kubernetes/pkg/kubectl/cmd/templates" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/genericclioptions" @@ -48,7 +49,10 @@ var ( kubectl create clusterrole foo --verb=get,list,watch --resource=pods,pods/status # Create a ClusterRole name "foo" with NonResourceURL specified - kubectl create clusterrole "foo" --verb=get --non-resource-url=/logs/*`)) + kubectl create clusterrole "foo" --verb=get --non-resource-url=/logs/* + + # Create a ClusterRole name "monitoring" with AggregationRule specified + kubectl create clusterrole monitoring --aggregation-rule="rbac.example.com/aggregate-to-monitoring=true"`)) // Valid nonResource verb list for validation. validNonResourceVerbs = []string{"*", "get", "post", "put", "delete", "patch", "head", "options"} @@ -57,12 +61,14 @@ var ( type CreateClusterRoleOptions struct { *CreateRoleOptions NonResourceURLs []string + AggregationRule map[string]string } // ClusterRole is a command to ease creating ClusterRoles. func NewCmdCreateClusterRole(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { c := &CreateClusterRoleOptions{ CreateRoleOptions: NewCreateRoleOptions(ioStreams), + AggregationRule: map[string]string{}, } cmd := &cobra.Command{ Use: "clusterrole NAME --verb=verb --resource=resource.group [--resource-name=resourcename] [--dry-run]", @@ -86,6 +92,7 @@ func NewCmdCreateClusterRole(f cmdutil.Factory, ioStreams genericclioptions.IOSt cmd.Flags().StringSliceVar(&c.NonResourceURLs, "non-resource-url", c.NonResourceURLs, "A partial url that user should have access to.") cmd.Flags().StringSlice("resource", []string{}, "Resource that the rule applies to") cmd.Flags().StringArrayVar(&c.ResourceNames, "resource-name", c.ResourceNames, "Resource in the white list that the rule applies to, repeat this flag for multiple items") + cmd.Flags().Var(utilflag.NewMapStringString(&c.AggregationRule), "aggregation-rule", "An aggregation label selector for combining ClusterRoles.") return cmd } @@ -108,6 +115,13 @@ func (c *CreateClusterRoleOptions) Validate() error { return fmt.Errorf("name must be specified") } + if len(c.AggregationRule) > 0 { + if len(c.NonResourceURLs) > 0 || len(c.Verbs) > 0 || len(c.Resources) > 0 || len(c.ResourceNames) > 0 { + return fmt.Errorf("aggregation rule must be specified without nonResourceURLs, verbs, resources or resourceNames") + } + return nil + } + // validate verbs. if len(c.Verbs) == 0 { return fmt.Errorf("at least one verb must be specified") @@ -162,11 +176,23 @@ func (c *CreateClusterRoleOptions) RunCreateRole() error { TypeMeta: metav1.TypeMeta{APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: "ClusterRole"}, } clusterRole.Name = c.Name - rules, err := generateResourcePolicyRules(c.Mapper, c.Verbs, c.Resources, c.ResourceNames, c.NonResourceURLs) - if err != nil { - return err + + var err error + if len(c.AggregationRule) == 0 { + rules, err := generateResourcePolicyRules(c.Mapper, c.Verbs, c.Resources, c.ResourceNames, c.NonResourceURLs) + if err != nil { + return err + } + clusterRole.Rules = rules + } else { + clusterRole.AggregationRule = &rbacv1.AggregationRule{ + ClusterRoleSelectors: []metav1.LabelSelector{ + { + MatchLabels: c.AggregationRule, + }, + }, + } } - clusterRole.Rules = rules // Create ClusterRole. if !c.DryRun { diff --git a/pkg/kubectl/cmd/create/create_clusterrole_test.go b/pkg/kubectl/cmd/create/create_clusterrole_test.go index 031c9435e58..4dc876f9a9e 100644 --- a/pkg/kubectl/cmd/create/create_clusterrole_test.go +++ b/pkg/kubectl/cmd/create/create_clusterrole_test.go @@ -22,6 +22,7 @@ import ( rbac "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" restclient "k8s.io/client-go/rest" @@ -47,6 +48,7 @@ func TestCreateClusterRole(t *testing.T) { resources string nonResourceURL string resourceNames string + aggregationRule string expectedClusterRole *rbac.ClusterRole }{ "test-duplicate-resources": { @@ -130,6 +132,25 @@ func TestCreateClusterRole(t *testing.T) { }, }, }, + "test-aggregation-rules": { + aggregationRule: "foo1=foo2,foo3=foo4", + expectedClusterRole: &rbac.ClusterRole{ + TypeMeta: v1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"}, + ObjectMeta: v1.ObjectMeta{ + Name: clusterRoleName, + }, + AggregationRule: &rbac.AggregationRule{ + ClusterRoleSelectors: []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + "foo1": "foo2", + "foo3": "foo4", + }, + }, + }, + }, + }, + }, } for name, test := range tests { @@ -140,6 +161,7 @@ func TestCreateClusterRole(t *testing.T) { cmd.Flags().Set("verb", test.verbs) cmd.Flags().Set("resource", test.resources) cmd.Flags().Set("non-resource-url", test.nonResourceURL) + cmd.Flags().Set("aggregation-rule", test.aggregationRule) if test.resourceNames != "" { cmd.Flags().Set("resource-name", test.resourceNames) } @@ -433,6 +455,50 @@ func TestClusterRoleValidate(t *testing.T) { }, expectErr: false, }, + "test-aggregation-rule-with-verb": { + clusterRoleOptions: &CreateClusterRoleOptions{ + CreateRoleOptions: &CreateRoleOptions{ + Name: "my-clusterrole", + Verbs: []string{"get"}, + }, + AggregationRule: map[string]string{"foo-key": "foo-vlue"}, + }, + expectErr: true, + }, + "test-aggregation-rule-with-resource": { + clusterRoleOptions: &CreateClusterRoleOptions{ + CreateRoleOptions: &CreateRoleOptions{ + Name: "my-clusterrole", + Resources: []ResourceOptions{ + { + Resource: "replicasets", + SubResource: "scale", + }, + }, + }, + AggregationRule: map[string]string{"foo-key": "foo-vlue"}, + }, + expectErr: true, + }, + "test-aggregation-rule-with-no-resource-url": { + clusterRoleOptions: &CreateClusterRoleOptions{ + CreateRoleOptions: &CreateRoleOptions{ + Name: "my-clusterrole", + }, + NonResourceURLs: []string{"/logs/"}, + AggregationRule: map[string]string{"foo-key": "foo-vlue"}, + }, + expectErr: true, + }, + "test-aggregation-rule": { + clusterRoleOptions: &CreateClusterRoleOptions{ + CreateRoleOptions: &CreateRoleOptions{ + Name: "my-clusterrole", + }, + AggregationRule: map[string]string{"foo-key": "foo-vlue"}, + }, + expectErr: false, + }, } for name, test := range tests {