set_subject.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. /*
  2. Copyright 2017 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package set
  14. import (
  15. "fmt"
  16. "strings"
  17. "github.com/spf13/cobra"
  18. rbacv1 "k8s.io/api/rbac/v1"
  19. "k8s.io/apimachinery/pkg/runtime"
  20. "k8s.io/apimachinery/pkg/types"
  21. utilerrors "k8s.io/apimachinery/pkg/util/errors"
  22. "k8s.io/apimachinery/pkg/util/sets"
  23. "k8s.io/cli-runtime/pkg/genericclioptions"
  24. "k8s.io/cli-runtime/pkg/printers"
  25. "k8s.io/cli-runtime/pkg/resource"
  26. cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
  27. "k8s.io/kubernetes/pkg/kubectl/scheme"
  28. "k8s.io/kubernetes/pkg/kubectl/util/i18n"
  29. "k8s.io/kubernetes/pkg/kubectl/util/templates"
  30. )
  31. var (
  32. subjectLong = templates.LongDesc(`
  33. Update User, Group or ServiceAccount in a RoleBinding/ClusterRoleBinding.`)
  34. subjectExample = templates.Examples(`
  35. # Update a ClusterRoleBinding for serviceaccount1
  36. kubectl set subject clusterrolebinding admin --serviceaccount=namespace:serviceaccount1
  37. # Update a RoleBinding for user1, user2, and group1
  38. kubectl set subject rolebinding admin --user=user1 --user=user2 --group=group1
  39. # Print the result (in yaml format) of updating rolebinding subjects from a local, without hitting the server
  40. kubectl create rolebinding admin --role=admin --user=admin -o yaml --dry-run | kubectl set subject --local -f - --user=foo -o yaml`)
  41. )
  42. type updateSubjects func(existings []rbacv1.Subject, targets []rbacv1.Subject) (bool, []rbacv1.Subject)
  43. // SubjectOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of
  44. // referencing the cmd.Flags
  45. type SubjectOptions struct {
  46. PrintFlags *genericclioptions.PrintFlags
  47. resource.FilenameOptions
  48. Infos []*resource.Info
  49. Selector string
  50. ContainerSelector string
  51. Output string
  52. All bool
  53. DryRun bool
  54. Local bool
  55. Users []string
  56. Groups []string
  57. ServiceAccounts []string
  58. namespace string
  59. PrintObj printers.ResourcePrinterFunc
  60. genericclioptions.IOStreams
  61. }
  62. // NewSubjectOptions returns an initialized SubjectOptions instance
  63. func NewSubjectOptions(streams genericclioptions.IOStreams) *SubjectOptions {
  64. return &SubjectOptions{
  65. PrintFlags: genericclioptions.NewPrintFlags("subjects updated").WithTypeSetter(scheme.Scheme),
  66. IOStreams: streams,
  67. }
  68. }
  69. // NewCmdSubject returns the "new subject" sub command
  70. func NewCmdSubject(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
  71. o := NewSubjectOptions(streams)
  72. cmd := &cobra.Command{
  73. Use: "subject (-f FILENAME | TYPE NAME) [--user=username] [--group=groupname] [--serviceaccount=namespace:serviceaccountname] [--dry-run]",
  74. DisableFlagsInUseLine: true,
  75. Short: i18n.T("Update User, Group or ServiceAccount in a RoleBinding/ClusterRoleBinding"),
  76. Long: subjectLong,
  77. Example: subjectExample,
  78. Run: func(cmd *cobra.Command, args []string) {
  79. cmdutil.CheckErr(o.Complete(f, cmd, args))
  80. cmdutil.CheckErr(o.Validate())
  81. cmdutil.CheckErr(o.Run(addSubjects))
  82. },
  83. }
  84. o.PrintFlags.AddFlags(cmd)
  85. cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "the resource to update the subjects")
  86. cmd.Flags().BoolVar(&o.All, "all", o.All, "Select all resources, including uninitialized ones, in the namespace of the specified resource types")
  87. cmd.Flags().StringVarP(&o.Selector, "selector", "l", o.Selector, "Selector (label query) to filter on, not including uninitialized ones, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
  88. cmd.Flags().BoolVar(&o.Local, "local", o.Local, "If true, set subject will NOT contact api-server but run locally.")
  89. cmdutil.AddDryRunFlag(cmd)
  90. cmd.Flags().StringArrayVar(&o.Users, "user", o.Users, "Usernames to bind to the role")
  91. cmd.Flags().StringArrayVar(&o.Groups, "group", o.Groups, "Groups to bind to the role")
  92. cmd.Flags().StringArrayVar(&o.ServiceAccounts, "serviceaccount", o.ServiceAccounts, "Service accounts to bind to the role")
  93. cmdutil.AddIncludeUninitializedFlag(cmd)
  94. return cmd
  95. }
  96. // Complete completes all required options
  97. func (o *SubjectOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
  98. o.Output = cmdutil.GetFlagString(cmd, "output")
  99. o.DryRun = cmdutil.GetDryRunFlag(cmd)
  100. if o.DryRun {
  101. o.PrintFlags.Complete("%s (dry run)")
  102. }
  103. printer, err := o.PrintFlags.ToPrinter()
  104. if err != nil {
  105. return err
  106. }
  107. o.PrintObj = printer.PrintObj
  108. var enforceNamespace bool
  109. o.namespace, enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
  110. if err != nil {
  111. return err
  112. }
  113. builder := f.NewBuilder().
  114. WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
  115. LocalParam(o.Local).
  116. ContinueOnError().
  117. NamespaceParam(o.namespace).DefaultNamespace().
  118. FilenameParam(enforceNamespace, &o.FilenameOptions).
  119. Flatten()
  120. if o.Local {
  121. // if a --local flag was provided, and a resource was specified in the form
  122. // <resource>/<name>, fail immediately as --local cannot query the api server
  123. // for the specified resource.
  124. if len(args) > 0 {
  125. return resource.LocalResourceError
  126. }
  127. } else {
  128. builder = builder.
  129. LabelSelectorParam(o.Selector).
  130. ResourceTypeOrNameArgs(o.All, args...).
  131. Latest()
  132. }
  133. o.Infos, err = builder.Do().Infos()
  134. if err != nil {
  135. return err
  136. }
  137. return nil
  138. }
  139. // Validate makes sure provided values in SubjectOptions are valid
  140. func (o *SubjectOptions) Validate() error {
  141. if o.All && len(o.Selector) > 0 {
  142. return fmt.Errorf("cannot set --all and --selector at the same time")
  143. }
  144. if len(o.Users) == 0 && len(o.Groups) == 0 && len(o.ServiceAccounts) == 0 {
  145. return fmt.Errorf("you must specify at least one value of user, group or serviceaccount")
  146. }
  147. for _, sa := range o.ServiceAccounts {
  148. tokens := strings.Split(sa, ":")
  149. if len(tokens) != 2 || tokens[1] == "" {
  150. return fmt.Errorf("serviceaccount must be <namespace>:<name>")
  151. }
  152. for _, info := range o.Infos {
  153. _, ok := info.Object.(*rbacv1.ClusterRoleBinding)
  154. if ok && tokens[0] == "" {
  155. return fmt.Errorf("serviceaccount must be <namespace>:<name>, namespace must be specified")
  156. }
  157. }
  158. }
  159. return nil
  160. }
  161. // Run performs the execution of "set subject" sub command
  162. func (o *SubjectOptions) Run(fn updateSubjects) error {
  163. patches := CalculatePatches(o.Infos, scheme.DefaultJSONEncoder(), func(obj runtime.Object) ([]byte, error) {
  164. subjects := []rbacv1.Subject{}
  165. for _, user := range sets.NewString(o.Users...).List() {
  166. subject := rbacv1.Subject{
  167. Kind: rbacv1.UserKind,
  168. APIGroup: rbacv1.GroupName,
  169. Name: user,
  170. }
  171. subjects = append(subjects, subject)
  172. }
  173. for _, group := range sets.NewString(o.Groups...).List() {
  174. subject := rbacv1.Subject{
  175. Kind: rbacv1.GroupKind,
  176. APIGroup: rbacv1.GroupName,
  177. Name: group,
  178. }
  179. subjects = append(subjects, subject)
  180. }
  181. for _, sa := range sets.NewString(o.ServiceAccounts...).List() {
  182. tokens := strings.Split(sa, ":")
  183. namespace := tokens[0]
  184. name := tokens[1]
  185. if len(namespace) == 0 {
  186. namespace = o.namespace
  187. }
  188. subject := rbacv1.Subject{
  189. Kind: rbacv1.ServiceAccountKind,
  190. Namespace: namespace,
  191. Name: name,
  192. }
  193. subjects = append(subjects, subject)
  194. }
  195. transformed, err := updateSubjectForObject(obj, subjects, fn)
  196. if transformed && err == nil {
  197. // TODO: switch UpdatePodSpecForObject to work on v1.PodSpec
  198. return runtime.Encode(scheme.DefaultJSONEncoder(), obj)
  199. }
  200. return nil, err
  201. })
  202. allErrs := []error{}
  203. for _, patch := range patches {
  204. info := patch.Info
  205. name := info.ObjectName()
  206. if patch.Err != nil {
  207. allErrs = append(allErrs, fmt.Errorf("error: %s %v\n", name, patch.Err))
  208. continue
  209. }
  210. //no changes
  211. if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
  212. allErrs = append(allErrs, fmt.Errorf("info: %s was not changed\n", name))
  213. continue
  214. }
  215. if o.Local || o.DryRun {
  216. if err := o.PrintObj(info.Object, o.Out); err != nil {
  217. allErrs = append(allErrs, err)
  218. }
  219. continue
  220. }
  221. actual, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil)
  222. if err != nil {
  223. allErrs = append(allErrs, fmt.Errorf("failed to patch subjects to rolebinding: %v", err))
  224. continue
  225. }
  226. if err := o.PrintObj(actual, o.Out); err != nil {
  227. allErrs = append(allErrs, err)
  228. }
  229. }
  230. return utilerrors.NewAggregate(allErrs)
  231. }
  232. //Note: the obj mutates in the function
  233. func updateSubjectForObject(obj runtime.Object, subjects []rbacv1.Subject, fn updateSubjects) (bool, error) {
  234. switch t := obj.(type) {
  235. case *rbacv1.RoleBinding:
  236. transformed, result := fn(t.Subjects, subjects)
  237. t.Subjects = result
  238. return transformed, nil
  239. case *rbacv1.ClusterRoleBinding:
  240. transformed, result := fn(t.Subjects, subjects)
  241. t.Subjects = result
  242. return transformed, nil
  243. default:
  244. return false, fmt.Errorf("setting subjects is only supported for RoleBinding/ClusterRoleBinding")
  245. }
  246. }
  247. func addSubjects(existings []rbacv1.Subject, targets []rbacv1.Subject) (bool, []rbacv1.Subject) {
  248. transformed := false
  249. updated := existings
  250. for _, item := range targets {
  251. if !contain(existings, item) {
  252. updated = append(updated, item)
  253. transformed = true
  254. }
  255. }
  256. return transformed, updated
  257. }
  258. func contain(slice []rbacv1.Subject, item rbacv1.Subject) bool {
  259. for _, v := range slice {
  260. if v == item {
  261. return true
  262. }
  263. }
  264. return false
  265. }