...

Source file src/k8s.io/kubectl/pkg/cmd/create/create_role.go

Documentation: k8s.io/kubectl/pkg/cmd/create

     1  /*
     2  Copyright 2017 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  
    17  package create
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"github.com/spf13/cobra"
    25  
    26  	rbacv1 "k8s.io/api/rbac/v1"
    27  	"k8s.io/apimachinery/pkg/api/meta"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	"k8s.io/cli-runtime/pkg/genericclioptions"
    33  	"k8s.io/cli-runtime/pkg/genericiooptions"
    34  	clientgorbacv1 "k8s.io/client-go/kubernetes/typed/rbac/v1"
    35  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    36  	"k8s.io/kubectl/pkg/scheme"
    37  	"k8s.io/kubectl/pkg/util"
    38  	"k8s.io/kubectl/pkg/util/i18n"
    39  	"k8s.io/kubectl/pkg/util/templates"
    40  )
    41  
    42  var (
    43  	roleLong = templates.LongDesc(i18n.T(`
    44  		Create a role with single rule.`))
    45  
    46  	roleExample = templates.Examples(i18n.T(`
    47  		# Create a role named "pod-reader" that allows user to perform "get", "watch" and "list" on pods
    48  		kubectl create role pod-reader --verb=get --verb=list --verb=watch --resource=pods
    49  
    50  		# Create a role named "pod-reader" with ResourceName specified
    51  		kubectl create role pod-reader --verb=get --resource=pods --resource-name=readablepod --resource-name=anotherpod
    52  
    53  		# Create a role named "foo" with API Group specified
    54  		kubectl create role foo --verb=get,list,watch --resource=rs.apps
    55  
    56  		# Create a role named "foo" with SubResource specified
    57  		kubectl create role foo --verb=get,list,watch --resource=pods,pods/status`))
    58  
    59  	// Valid resource verb list for validation.
    60  	validResourceVerbs = []string{"*", "get", "delete", "list", "create", "update", "patch", "watch", "proxy", "deletecollection", "use", "bind", "escalate", "impersonate"}
    61  
    62  	// Specialized verbs and GroupResources
    63  	specialVerbs = map[string][]schema.GroupResource{
    64  		"use": {
    65  			{
    66  				Group:    "policy",
    67  				Resource: "podsecuritypolicies",
    68  			},
    69  			{
    70  				Group:    "extensions",
    71  				Resource: "podsecuritypolicies",
    72  			},
    73  		},
    74  		"bind": {
    75  			{
    76  				Group:    "rbac.authorization.k8s.io",
    77  				Resource: "roles",
    78  			},
    79  			{
    80  				Group:    "rbac.authorization.k8s.io",
    81  				Resource: "clusterroles",
    82  			},
    83  		},
    84  		"escalate": {
    85  			{
    86  				Group:    "rbac.authorization.k8s.io",
    87  				Resource: "roles",
    88  			},
    89  			{
    90  				Group:    "rbac.authorization.k8s.io",
    91  				Resource: "clusterroles",
    92  			},
    93  		},
    94  		"impersonate": {
    95  			{
    96  				Group:    "",
    97  				Resource: "users",
    98  			},
    99  			{
   100  				Group:    "",
   101  				Resource: "serviceaccounts",
   102  			},
   103  			{
   104  				Group:    "",
   105  				Resource: "groups",
   106  			},
   107  			{
   108  				Group:    "authentication.k8s.io",
   109  				Resource: "userextras",
   110  			},
   111  		},
   112  	}
   113  )
   114  
   115  // AddSpecialVerb allows the addition of items to the `specialVerbs` map for non-k8s native resources.
   116  func AddSpecialVerb(verb string, gr schema.GroupResource) {
   117  	resources, ok := specialVerbs[verb]
   118  	if !ok {
   119  		resources = make([]schema.GroupResource, 1)
   120  	}
   121  	resources = append(resources, gr)
   122  	specialVerbs[verb] = resources
   123  }
   124  
   125  // ResourceOptions holds the related options for '--resource' option
   126  type ResourceOptions struct {
   127  	Group       string
   128  	Resource    string
   129  	SubResource string
   130  }
   131  
   132  // CreateRoleOptions holds the options for 'create role' sub command
   133  type CreateRoleOptions struct {
   134  	PrintFlags *genericclioptions.PrintFlags
   135  
   136  	Name          string
   137  	Verbs         []string
   138  	Resources     []ResourceOptions
   139  	ResourceNames []string
   140  
   141  	DryRunStrategy      cmdutil.DryRunStrategy
   142  	ValidationDirective string
   143  	OutputFormat        string
   144  	Namespace           string
   145  	EnforceNamespace    bool
   146  	Client              clientgorbacv1.RbacV1Interface
   147  	Mapper              meta.RESTMapper
   148  	PrintObj            func(obj runtime.Object) error
   149  	FieldManager        string
   150  	CreateAnnotation    bool
   151  
   152  	genericiooptions.IOStreams
   153  }
   154  
   155  // NewCreateRoleOptions returns an initialized CreateRoleOptions instance
   156  func NewCreateRoleOptions(ioStreams genericiooptions.IOStreams) *CreateRoleOptions {
   157  	return &CreateRoleOptions{
   158  		PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
   159  
   160  		IOStreams: ioStreams,
   161  	}
   162  }
   163  
   164  // NewCmdCreateRole returnns an initialized Command instance for 'create role' sub command
   165  func NewCmdCreateRole(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command {
   166  	o := NewCreateRoleOptions(ioStreams)
   167  
   168  	cmd := &cobra.Command{
   169  		Use:                   "role NAME --verb=verb --resource=resource.group/subresource [--resource-name=resourcename] [--dry-run=server|client|none]",
   170  		DisableFlagsInUseLine: true,
   171  		Short:                 i18n.T("Create a role with single rule"),
   172  		Long:                  roleLong,
   173  		Example:               roleExample,
   174  		Run: func(cmd *cobra.Command, args []string) {
   175  			cmdutil.CheckErr(o.Complete(f, cmd, args))
   176  			cmdutil.CheckErr(o.Validate())
   177  			cmdutil.CheckErr(o.RunCreateRole())
   178  		},
   179  	}
   180  
   181  	o.PrintFlags.AddFlags(cmd)
   182  
   183  	cmdutil.AddApplyAnnotationFlags(cmd)
   184  	cmdutil.AddValidateFlags(cmd)
   185  	cmdutil.AddDryRunFlag(cmd)
   186  	cmd.Flags().StringSliceVar(&o.Verbs, "verb", o.Verbs, "Verb that applies to the resources contained in the rule")
   187  	cmd.Flags().StringSlice("resource", []string{}, "Resource that the rule applies to")
   188  	cmd.Flags().StringArrayVar(&o.ResourceNames, "resource-name", o.ResourceNames, "Resource in the white list that the rule applies to, repeat this flag for multiple items")
   189  	cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create")
   190  	return cmd
   191  }
   192  
   193  // Complete completes all the required options
   194  func (o *CreateRoleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
   195  	name, err := NameFromCommandArgs(cmd, args)
   196  	if err != nil {
   197  		return err
   198  	}
   199  	o.Name = name
   200  
   201  	// Remove duplicate verbs.
   202  	verbs := []string{}
   203  	for _, v := range o.Verbs {
   204  		// VerbAll respresents all kinds of verbs.
   205  		if v == "*" {
   206  			verbs = []string{"*"}
   207  			break
   208  		}
   209  		if !arrayContains(verbs, v) {
   210  			verbs = append(verbs, v)
   211  		}
   212  	}
   213  	o.Verbs = verbs
   214  
   215  	// Support resource.group pattern. If no API Group specified, use "" as core API Group.
   216  	// e.g. --resource=pods,deployments.extensions
   217  	resources := cmdutil.GetFlagStringSlice(cmd, "resource")
   218  	for _, r := range resources {
   219  		sections := strings.SplitN(r, "/", 2)
   220  
   221  		resource := &ResourceOptions{}
   222  		if len(sections) == 2 {
   223  			resource.SubResource = sections[1]
   224  		}
   225  
   226  		parts := strings.SplitN(sections[0], ".", 2)
   227  		if len(parts) == 2 {
   228  			resource.Group = parts[1]
   229  		}
   230  		resource.Resource = parts[0]
   231  
   232  		if resource.Resource == "*" && len(parts) == 1 && len(sections) == 1 {
   233  			o.Resources = []ResourceOptions{*resource}
   234  			break
   235  		}
   236  
   237  		o.Resources = append(o.Resources, *resource)
   238  	}
   239  
   240  	// Remove duplicate resource names.
   241  	resourceNames := []string{}
   242  	for _, n := range o.ResourceNames {
   243  		if !arrayContains(resourceNames, n) {
   244  			resourceNames = append(resourceNames, n)
   245  		}
   246  	}
   247  	o.ResourceNames = resourceNames
   248  
   249  	// Complete other options for Run.
   250  	o.Mapper, err = f.ToRESTMapper()
   251  	if err != nil {
   252  		return err
   253  	}
   254  
   255  	o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
   256  	if err != nil {
   257  		return err
   258  	}
   259  	o.OutputFormat = cmdutil.GetFlagString(cmd, "output")
   260  	o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag)
   261  
   262  	cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
   263  	printer, err := o.PrintFlags.ToPrinter()
   264  	if err != nil {
   265  		return err
   266  	}
   267  	o.PrintObj = func(obj runtime.Object) error {
   268  		return printer.PrintObj(obj, o.Out)
   269  	}
   270  
   271  	o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd)
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	clientset, err := f.KubernetesClientSet()
   282  	if err != nil {
   283  		return err
   284  	}
   285  	o.Client = clientset.RbacV1()
   286  
   287  	return nil
   288  }
   289  
   290  // Validate makes sure there is no discrepency in provided option values
   291  func (o *CreateRoleOptions) Validate() error {
   292  	if o.Name == "" {
   293  		return fmt.Errorf("name must be specified")
   294  	}
   295  
   296  	// validate verbs.
   297  	if len(o.Verbs) == 0 {
   298  		return fmt.Errorf("at least one verb must be specified")
   299  	}
   300  
   301  	for _, v := range o.Verbs {
   302  		if !arrayContains(validResourceVerbs, v) {
   303  			fmt.Fprintf(o.ErrOut, "Warning: '%s' is not a standard resource verb\n", v)
   304  		}
   305  	}
   306  
   307  	// validate resources.
   308  	if len(o.Resources) == 0 {
   309  		return fmt.Errorf("at least one resource must be specified")
   310  	}
   311  
   312  	return o.validateResource()
   313  }
   314  
   315  func (o *CreateRoleOptions) validateResource() error {
   316  	for _, r := range o.Resources {
   317  		if len(r.Resource) == 0 {
   318  			return fmt.Errorf("resource must be specified if apiGroup/subresource specified")
   319  		}
   320  		if r.Resource == "*" {
   321  			return nil
   322  		}
   323  
   324  		resource := schema.GroupVersionResource{Resource: r.Resource, Group: r.Group}
   325  		groupVersionResource, err := o.Mapper.ResourceFor(schema.GroupVersionResource{Resource: r.Resource, Group: r.Group})
   326  		if err == nil {
   327  			resource = groupVersionResource
   328  		}
   329  
   330  		for _, v := range o.Verbs {
   331  			if groupResources, ok := specialVerbs[v]; ok {
   332  				match := false
   333  				for _, extra := range groupResources {
   334  					if resource.Resource == extra.Resource && resource.Group == extra.Group {
   335  						match = true
   336  						err = nil
   337  						break
   338  					}
   339  				}
   340  				if !match {
   341  					return fmt.Errorf("can not perform '%s' on '%s' in group '%s'", v, resource.Resource, resource.Group)
   342  				}
   343  			}
   344  		}
   345  
   346  		if err != nil {
   347  			return err
   348  		}
   349  	}
   350  	return nil
   351  }
   352  
   353  // RunCreateRole performs the execution of 'create role' sub command
   354  func (o *CreateRoleOptions) RunCreateRole() error {
   355  	role := &rbacv1.Role{
   356  		// this is ok because we know exactly how we want to be serialized
   357  		TypeMeta: metav1.TypeMeta{APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: "Role"},
   358  	}
   359  	role.Name = o.Name
   360  	rules, err := generateResourcePolicyRules(o.Mapper, o.Verbs, o.Resources, o.ResourceNames, []string{})
   361  	if err != nil {
   362  		return err
   363  	}
   364  	role.Rules = rules
   365  	if o.EnforceNamespace {
   366  		role.Namespace = o.Namespace
   367  	}
   368  
   369  	if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, role, scheme.DefaultJSONEncoder()); err != nil {
   370  		return err
   371  	}
   372  
   373  	// Create role.
   374  	if o.DryRunStrategy != cmdutil.DryRunClient {
   375  		createOptions := metav1.CreateOptions{}
   376  		if o.FieldManager != "" {
   377  			createOptions.FieldManager = o.FieldManager
   378  		}
   379  		createOptions.FieldValidation = o.ValidationDirective
   380  		if o.DryRunStrategy == cmdutil.DryRunServer {
   381  			createOptions.DryRun = []string{metav1.DryRunAll}
   382  		}
   383  		role, err = o.Client.Roles(o.Namespace).Create(context.TODO(), role, createOptions)
   384  		if err != nil {
   385  			return err
   386  		}
   387  	}
   388  
   389  	return o.PrintObj(role)
   390  }
   391  
   392  func arrayContains(s []string, e string) bool {
   393  	for _, a := range s {
   394  		if a == e {
   395  			return true
   396  		}
   397  	}
   398  	return false
   399  }
   400  
   401  func generateResourcePolicyRules(mapper meta.RESTMapper, verbs []string, resources []ResourceOptions, resourceNames []string, nonResourceURLs []string) ([]rbacv1.PolicyRule, error) {
   402  	// groupResourceMapping is a apigroup-resource map. The key of this map is api group, while the value
   403  	// is a string array of resources under this api group.
   404  	// E.g.  groupResourceMapping = {"extensions": ["replicasets", "deployments"], "batch":["jobs"]}
   405  	groupResourceMapping := map[string][]string{}
   406  
   407  	// This loop does the following work:
   408  	// 1. Constructs groupResourceMapping based on input resources.
   409  	// 2. Prevents pointing to non-existent resources.
   410  	// 3. Transfers resource short name to long name. E.g. rs.extensions is transferred to replicasets.extensions
   411  	for _, r := range resources {
   412  		resource := schema.GroupVersionResource{Resource: r.Resource, Group: r.Group}
   413  		groupVersionResource, err := mapper.ResourceFor(schema.GroupVersionResource{Resource: r.Resource, Group: r.Group})
   414  		if err == nil {
   415  			resource = groupVersionResource
   416  		}
   417  
   418  		if len(r.SubResource) > 0 {
   419  			resource.Resource = resource.Resource + "/" + r.SubResource
   420  		}
   421  		if !arrayContains(groupResourceMapping[resource.Group], resource.Resource) {
   422  			groupResourceMapping[resource.Group] = append(groupResourceMapping[resource.Group], resource.Resource)
   423  		}
   424  	}
   425  
   426  	// Create separate rule for each of the api group.
   427  	rules := []rbacv1.PolicyRule{}
   428  	for _, g := range sets.StringKeySet(groupResourceMapping).List() {
   429  		rule := rbacv1.PolicyRule{}
   430  		rule.Verbs = verbs
   431  		rule.Resources = groupResourceMapping[g]
   432  		rule.APIGroups = []string{g}
   433  		rule.ResourceNames = resourceNames
   434  		rules = append(rules, rule)
   435  	}
   436  
   437  	if len(nonResourceURLs) > 0 {
   438  		rule := rbacv1.PolicyRule{}
   439  		rule.Verbs = verbs
   440  		rule.NonResourceURLs = nonResourceURLs
   441  		rules = append(rules, rule)
   442  	}
   443  
   444  	return rules, nil
   445  }
   446  

View as plain text