...

Source file src/k8s.io/kubectl/pkg/cmd/set/set_subject.go

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

     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 set
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  
    23  	"github.com/spf13/cobra"
    24  
    25  	rbacv1 "k8s.io/api/rbac/v1"
    26  	"k8s.io/apimachinery/pkg/runtime"
    27  	"k8s.io/apimachinery/pkg/types"
    28  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    29  	"k8s.io/apimachinery/pkg/util/sets"
    30  	"k8s.io/cli-runtime/pkg/genericclioptions"
    31  	"k8s.io/cli-runtime/pkg/genericiooptions"
    32  	"k8s.io/cli-runtime/pkg/printers"
    33  	"k8s.io/cli-runtime/pkg/resource"
    34  	"k8s.io/client-go/tools/clientcmd"
    35  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    36  	"k8s.io/kubectl/pkg/scheme"
    37  	"k8s.io/kubectl/pkg/util/i18n"
    38  	"k8s.io/kubectl/pkg/util/templates"
    39  )
    40  
    41  var (
    42  	subjectLong = templates.LongDesc(i18n.T(`
    43  	Update the user, group, or service account in a role binding or cluster role binding.`))
    44  
    45  	subjectExample = templates.Examples(`
    46  	# Update a cluster role binding for serviceaccount1
    47  	kubectl set subject clusterrolebinding admin --serviceaccount=namespace:serviceaccount1
    48  
    49  	# Update a role binding for user1, user2, and group1
    50  	kubectl set subject rolebinding admin --user=user1 --user=user2 --group=group1
    51  
    52  	# Print the result (in YAML format) of updating rolebinding subjects from a local, without hitting the server
    53  	kubectl create rolebinding admin --role=admin --user=admin -o yaml --dry-run=client | kubectl set subject --local -f - --user=foo -o yaml`)
    54  )
    55  
    56  type updateSubjects func(existings []rbacv1.Subject, targets []rbacv1.Subject) (bool, []rbacv1.Subject)
    57  
    58  // SubjectOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of
    59  // referencing the cmd.Flags
    60  type SubjectOptions struct {
    61  	PrintFlags *genericclioptions.PrintFlags
    62  
    63  	resource.FilenameOptions
    64  
    65  	Infos             []*resource.Info
    66  	Selector          string
    67  	ContainerSelector string
    68  	Output            string
    69  	All               bool
    70  	DryRunStrategy    cmdutil.DryRunStrategy
    71  	Local             bool
    72  	fieldManager      string
    73  
    74  	Users           []string
    75  	Groups          []string
    76  	ServiceAccounts []string
    77  
    78  	namespace string
    79  
    80  	PrintObj printers.ResourcePrinterFunc
    81  
    82  	genericiooptions.IOStreams
    83  }
    84  
    85  // NewSubjectOptions returns an initialized SubjectOptions instance
    86  func NewSubjectOptions(streams genericiooptions.IOStreams) *SubjectOptions {
    87  	return &SubjectOptions{
    88  		PrintFlags: genericclioptions.NewPrintFlags("subjects updated").WithTypeSetter(scheme.Scheme),
    89  
    90  		IOStreams: streams,
    91  	}
    92  }
    93  
    94  // NewCmdSubject returns the "new subject" sub command
    95  func NewCmdSubject(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
    96  	o := NewSubjectOptions(streams)
    97  	cmd := &cobra.Command{
    98  		Use:                   "subject (-f FILENAME | TYPE NAME) [--user=username] [--group=groupname] [--serviceaccount=namespace:serviceaccountname] [--dry-run=server|client|none]",
    99  		DisableFlagsInUseLine: true,
   100  		Short:                 i18n.T("Update the user, group, or service account in a role binding or cluster role binding"),
   101  		Long:                  subjectLong,
   102  		Example:               subjectExample,
   103  		Run: func(cmd *cobra.Command, args []string) {
   104  			cmdutil.CheckErr(o.Complete(f, cmd, args))
   105  			cmdutil.CheckErr(o.Validate())
   106  			cmdutil.CheckErr(o.Run(addSubjects))
   107  		},
   108  	}
   109  
   110  	o.PrintFlags.AddFlags(cmd)
   111  
   112  	cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "the resource to update the subjects")
   113  	cmd.Flags().BoolVar(&o.All, "all", o.All, "Select all resources, in the namespace of the specified resource types")
   114  	cmdutil.AddLabelSelectorFlagVar(cmd, &o.Selector)
   115  	cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, set subject will NOT contact api-server but run locally.")
   116  	cmdutil.AddDryRunFlag(cmd)
   117  	cmd.Flags().StringArrayVar(&o.Users, "user", o.Users, "Usernames to bind to the role")
   118  	cmd.Flags().StringArrayVar(&o.Groups, "group", o.Groups, "Groups to bind to the role")
   119  	cmd.Flags().StringArrayVar(&o.ServiceAccounts, "serviceaccount", o.ServiceAccounts, "Service accounts to bind to the role")
   120  	cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-set")
   121  	return cmd
   122  }
   123  
   124  // Complete completes all required options
   125  func (o *SubjectOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
   126  	o.Output = cmdutil.GetFlagString(cmd, "output")
   127  	var err error
   128  	o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
   129  	if err != nil {
   130  		return err
   131  	}
   132  
   133  	cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
   134  	printer, err := o.PrintFlags.ToPrinter()
   135  	if err != nil {
   136  		return err
   137  	}
   138  	o.PrintObj = printer.PrintObj
   139  
   140  	var enforceNamespace bool
   141  	o.namespace, enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
   142  	if err != nil && !(o.Local && clientcmd.IsEmptyConfig(err)) {
   143  		return err
   144  	}
   145  
   146  	builder := f.NewBuilder().
   147  		WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
   148  		LocalParam(o.Local).
   149  		ContinueOnError().
   150  		NamespaceParam(o.namespace).DefaultNamespace().
   151  		FilenameParam(enforceNamespace, &o.FilenameOptions).
   152  		Flatten()
   153  
   154  	if o.Local {
   155  		// if a --local flag was provided, and a resource was specified in the form
   156  		// <resource>/<name>, fail immediately as --local cannot query the api server
   157  		// for the specified resource.
   158  		if len(args) > 0 {
   159  			return resource.LocalResourceError
   160  		}
   161  	} else {
   162  		builder = builder.
   163  			LabelSelectorParam(o.Selector).
   164  			ResourceTypeOrNameArgs(o.All, args...).
   165  			Latest()
   166  	}
   167  
   168  	o.Infos, err = builder.Do().Infos()
   169  	if err != nil {
   170  		return err
   171  	}
   172  
   173  	return nil
   174  }
   175  
   176  // Validate makes sure provided values in SubjectOptions are valid
   177  func (o *SubjectOptions) Validate() error {
   178  	if o.Local && o.DryRunStrategy == cmdutil.DryRunServer {
   179  		return fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?")
   180  	}
   181  	if o.All && len(o.Selector) > 0 {
   182  		return fmt.Errorf("cannot set --all and --selector at the same time")
   183  	}
   184  	if len(o.Users) == 0 && len(o.Groups) == 0 && len(o.ServiceAccounts) == 0 {
   185  		return fmt.Errorf("you must specify at least one value of user, group or serviceaccount")
   186  	}
   187  
   188  	for _, sa := range o.ServiceAccounts {
   189  		tokens := strings.Split(sa, ":")
   190  		if len(tokens) != 2 || tokens[1] == "" {
   191  			return fmt.Errorf("serviceaccount must be <namespace>:<name>")
   192  		}
   193  
   194  		for _, info := range o.Infos {
   195  			_, ok := info.Object.(*rbacv1.ClusterRoleBinding)
   196  			if ok && tokens[0] == "" {
   197  				return fmt.Errorf("serviceaccount must be <namespace>:<name>, namespace must be specified")
   198  			}
   199  		}
   200  	}
   201  
   202  	return nil
   203  }
   204  
   205  // Run performs the execution of "set subject" sub command
   206  func (o *SubjectOptions) Run(fn updateSubjects) error {
   207  	patches := CalculatePatches(o.Infos, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) {
   208  		subjects := []rbacv1.Subject{}
   209  		for _, user := range sets.NewString(o.Users...).List() {
   210  			subject := rbacv1.Subject{
   211  				Kind:     rbacv1.UserKind,
   212  				APIGroup: rbacv1.GroupName,
   213  				Name:     user,
   214  			}
   215  			subjects = append(subjects, subject)
   216  		}
   217  		for _, group := range sets.NewString(o.Groups...).List() {
   218  			subject := rbacv1.Subject{
   219  				Kind:     rbacv1.GroupKind,
   220  				APIGroup: rbacv1.GroupName,
   221  				Name:     group,
   222  			}
   223  			subjects = append(subjects, subject)
   224  		}
   225  		for _, sa := range sets.NewString(o.ServiceAccounts...).List() {
   226  			tokens := strings.Split(sa, ":")
   227  			namespace := tokens[0]
   228  			name := tokens[1]
   229  			if len(namespace) == 0 {
   230  				namespace = o.namespace
   231  			}
   232  			subject := rbacv1.Subject{
   233  				Kind:      rbacv1.ServiceAccountKind,
   234  				Namespace: namespace,
   235  				Name:      name,
   236  			}
   237  			subjects = append(subjects, subject)
   238  		}
   239  
   240  		transformed, err := updateSubjectForObject(obj, subjects, fn)
   241  		if transformed && err == nil {
   242  			// TODO: switch UpdatePodSpecForObject to work on v1.PodSpec
   243  			return runtime.Encode(scheme.DefaultJSONEncoder(), obj)
   244  		}
   245  		return nil, err
   246  	})
   247  
   248  	allErrs := []error{}
   249  	for _, patch := range patches {
   250  		info := patch.Info
   251  		name := info.ObjectName()
   252  		if patch.Err != nil {
   253  			allErrs = append(allErrs, fmt.Errorf("error: %s %v\n", name, patch.Err))
   254  			continue
   255  		}
   256  
   257  		//no changes
   258  		if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
   259  			continue
   260  		}
   261  
   262  		if o.Local || o.DryRunStrategy == cmdutil.DryRunClient {
   263  			if err := o.PrintObj(info.Object, o.Out); err != nil {
   264  				allErrs = append(allErrs, err)
   265  			}
   266  			continue
   267  		}
   268  
   269  		actual, err := resource.
   270  			NewHelper(info.Client, info.Mapping).
   271  			DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
   272  			WithFieldManager(o.fieldManager).
   273  			Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
   274  		if err != nil {
   275  			allErrs = append(allErrs, fmt.Errorf("failed to patch subjects to rolebinding: %v", err))
   276  			continue
   277  		}
   278  
   279  		if err := o.PrintObj(actual, o.Out); err != nil {
   280  			allErrs = append(allErrs, err)
   281  		}
   282  	}
   283  	return utilerrors.NewAggregate(allErrs)
   284  }
   285  
   286  // Note: the obj mutates in the function
   287  func updateSubjectForObject(obj runtime.Object, subjects []rbacv1.Subject, fn updateSubjects) (bool, error) {
   288  	switch t := obj.(type) {
   289  	case *rbacv1.RoleBinding:
   290  		transformed, result := fn(t.Subjects, subjects)
   291  		t.Subjects = result
   292  		return transformed, nil
   293  	case *rbacv1.ClusterRoleBinding:
   294  		transformed, result := fn(t.Subjects, subjects)
   295  		t.Subjects = result
   296  		return transformed, nil
   297  	default:
   298  		return false, fmt.Errorf("setting subjects is only supported for RoleBinding/ClusterRoleBinding")
   299  	}
   300  }
   301  
   302  func addSubjects(existings []rbacv1.Subject, targets []rbacv1.Subject) (bool, []rbacv1.Subject) {
   303  	transformed := false
   304  	updated := existings
   305  	for _, item := range targets {
   306  		if !contain(existings, item) {
   307  			updated = append(updated, item)
   308  			transformed = true
   309  		}
   310  	}
   311  	return transformed, updated
   312  }
   313  
   314  func contain(slice []rbacv1.Subject, item rbacv1.Subject) bool {
   315  	for _, v := range slice {
   316  		if v == item {
   317  			return true
   318  		}
   319  	}
   320  	return false
   321  }
   322  

View as plain text