1
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
59
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
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
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
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
156
157
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
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
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
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
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
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