Change:
Is this a BUG REPORT or FEATURE REQUEST? (choose one): FEATURE REQUEST Kubernetes version (use ): HEAD Now that RBAC is going to GA, using ServiceAccounts is getting more and more important. Still a lot of users run their apps/addons/whatever in the SA. In order for the admin to be able to change ServiceAccount easily after an application has been installed (which supposedly is crashlooping due to insufficient privs), a command could exist. I know I can do this: but for new users to Kubernetes, it's non-obvious. Proposed command: Tracking issue: WDYT? This would be a good way for a new contributor to get aquainted with the code by contributing this feature.
Is anyone working on this? If not I would like to take this up.
Nobody is working on this. I would be super happy if you could contribute this feature. Be aware of that there isn't consensus around this proposal yet (I proposed it yesterday, saturday, so people haven't had time to catch up on it) However, I do think that this is in line with , so feel free to start working on this if you want to ;) Also, don't hesitate to ask for help if you don't know how to get started and we'll help out! Thanks!
I'm in favor of this command. ?
Thanks, I will start working on this.
Is the SA on a Pod mutable or immutable? What about DaemonSet and StatefulSet? Do those have SA as well? Is there a way to do this so that it dynamically supports anything with a PodTemplate? Maybe this should be done as a subresource that can be dynamically discovered by kubectl using the discovery service.
Maybe helpful:
Pod is immutable. All the other objects (with the exception of stateful set) are mutable.
That pattern is the same for all the things that set aspects of a pod spec (except image, which is also mutable in pods)
Statefulsets are going to loosen their validation soon I expect.
This sounds good to me. just go ahead, I will help review this. Thanks ~
The outout of this command kubectl get rc/nginx-controller -o=yaml , has service acccount at the following path '' , and the corresponding pod has service account at 'spec.serviceAccountName'. Both refer to the 'serviceAccountName' field of PodSpec struct but can be made to be different by patching the ReplicationController resource. How does ReplicationController and Pod use serviceAccountName differently? Shouldn't the ReplicationControllerSpec have a serviceAccountName field?
ptal, I think you are more familiar with ReplicationController implementation than me.
ReplicationController contains a pod spec object, which contains a serviceAccountName.
Hi , how to go about writing e2e test cases for this. My development is mostly complete.
I wouldn't mind if you posted the PR now and then we'd see what e2e test makes sense to have. In order to move forward...
what are the next steps?

Discussion:
docs/man/man1/kubectl-set-image.1 docs/man/man1/kubectl-set-resources.1 docs/man/man1/kubectl-set-selector.1 <ins> docs/man/man1/kubectl-set-serviceaccount.1 </ins> docs/man/man1/kubectl-set-subject.1 docs/man/man1/kubectl-set.1 docs/man/man1/kubectl-stop.1

docs/user-guide/kubectl/kubectl_set_image.md docs/user-guide/kubectl/kubectl_set_resources.md docs/user-guide/kubectl/kubectl_set_selector.md <ins> docs/user-guide/kubectl/kubectl_set_serviceaccount.md </ins> docs/user-guide/kubectl/kubectl_set_subject.md docs/user-guide/kubectl/kubectl_taint.md docs/user-guide/kubectl/kubectl_top.md

<ins> This file is autogenerated, but we've stopped checking such files into the repository to reduce the need for rebases. Please run hack/generate-docs.sh to populate this file. </ins>

"set_image.go", "set_resources.go", "set_selector.go", <ins> "set_serviceaccount.go", </ins> "set_subject.go", ], visibility = ["//build/visible_to:pkg_kubectl_cmd_set_CONSUMERS"],

"set_image_test.go", "set_resources_test.go", "set_selector_test.go", <ins> "set_serviceaccount_test.go", </ins> "set_subject_test.go", "set_test.go", ], data = [ "//examples:config", <ins> "//test/fixtures", </ins> ], library = ":go_default_library", deps = [ "//pkg/api:go_default_library", <ins> "//pkg/api/testapi:go_default_library", "//pkg/apis/apps:go_default_library", </ins> "//pkg/apis/batch:go_default_library", "//pkg/apis/extensions:go_default_library", "//pkg/apis/rbac:go_default_library",

cmd.AddCommand(NewCmdResources(f, out, err)) cmd.AddCommand(NewCmdSelector(f, out)) cmd.AddCommand(NewCmdSubject(f, out, err)) <ins> cmd.AddCommand(NewCmdServiceAccount(f, out, err)) </ins> return cmd }

<ins> /* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "errors" "fmt" "io" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/kubectl/cmd/templates" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/util/i18n" ) var ( serviceaccountResources = ` replicationcontroller (rc), deployment (deploy), daemonset (ds), job, replicaset (rs), statefulset` serviceaccountLong = templates.LongDesc(i18n.T(` Update ServiceAccount of pod template resources. Possible resources (case insensitive) can be: ` + serviceaccountResources)) serviceaccountExample = templates.Examples(i18n.T(` # Set Deployment nginx-deployment's ServiceAccount to serviceaccount1 kubectl set serviceaccount deployment nginx-deployment serviceaccount1 # Print the result (in yaml format) of updated nginx deployment with serviceaccount from local file, without hitting apiserver kubectl set sa -f nginx-deployment.yaml serviceaccount1 --local --dry-run -o yaml `)) ) // serviceAccountConfig encapsulates the data required to perform the operation. type serviceAccountConfig struct { fileNameOptions        resource.FilenameOptions mapper                 meta.RESTMapper encoder                runtime.Encoder out                    io.Writer err                    io.Writer dryRun                 bool shortOutput            bool all                    bool record                 bool output                 string changeCause            string local                  bool saPrint                func(obj runtime.Object) error updatePodSpecForObject func(runtime.Object, func(*api.PodSpec) error) (bool, error) infos                  []*resource.Info serviceAccountName     string } // NewCmdServiceAccount returns the "set serviceaccount" command. func NewCmdServiceAccount(f cmdutil.Factory, out, err io.Writer) *cobra.Command { saConfig := &serviceAccountConfig{ out: out, err: err, } cmd := &cobra.Command{ Use:     "serviceaccount (-f FILENAME | TYPE NAME) SERVICE_ACCOUNT", Aliases: []string{"sa"}, Short:   i18n.T("Update ServiceAccount of a resource"), Long:    serviceaccountLong, Example: serviceaccountExample, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(saConfig.Complete(f, cmd, args)) cmdutil.CheckErr(saConfig.Run()) }, } cmdutil.AddPrinterFlags(cmd) usage := "identifying the resource to get from a server." cmdutil.AddFilenameOptionFlags(cmd, &saConfig.fileNameOptions, usage) cmd.Flags().BoolVar(&saConfig.all, "all", false, "Select all resources in the namespace of the specified resource types") cmd.Flags().BoolVar(&saConfig.local, "local", false, "If true, set image will NOT contact api-server but run locally.") cmdutil.AddRecordFlag(cmd) cmdutil.AddDryRunFlag(cmd) return cmd } // Complete configures serviceAccountConfig from command line args. func (saConfig *serviceAccountConfig) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { saConfig.mapper, _ = f.Object() saConfig.encoder = f.JSONEncoder() saConfig.shortOutput = cmdutil.GetFlagString(cmd, "output") == "name" saConfig.record = cmdutil.GetRecordFlag(cmd) saConfig.changeCause = f.Command(cmd, false) saConfig.dryRun = cmdutil.GetDryRunFlag(cmd) saConfig.output = cmdutil.GetFlagString(cmd, "output") saConfig.updatePodSpecForObject = f.UpdatePodSpecForObject saConfig.saPrint = func(obj runtime.Object) error { return f.PrintObject(cmd, saConfig.local, saConfig.mapper, obj, saConfig.out) } cmdNamespace, enforceNamespace, err := f.DefaultNamespace() if err != nil { return err } if len(args) == 0 { return errors.New("serviceaccount is required") } saConfig.serviceAccountName = args[len(args)-1] resources := args[:len(args)-1] builder := f.NewBuilder(!saConfig.local).ContinueOnError(). NamespaceParam(cmdNamespace).DefaultNamespace(). FilenameParam(enforceNamespace, &saConfig.fileNameOptions). Flatten() if !saConfig.local { builder.ResourceTypeOrNameArgs(saConfig.all, resources...). Latest() } saConfig.infos, err = builder.Do().Infos() if err != nil { return err } return nil } // Run creates and applies the patch either locally or calling apiserver. func (saConfig *serviceAccountConfig) Run() error { patchErrs := []error{} patchFn := func(info *resource.Info) ([]byte, error) { saConfig.updatePodSpecForObject(info.Object, func(podSpec *api.PodSpec) error { podSpec.ServiceAccountName = saConfig.serviceAccountName return nil }) return runtime.Encode(saConfig.encoder, info.Object) } patches := CalculatePatches(saConfig.infos, saConfig.encoder, patchFn) for _, patch := range patches { info := patch.Info if patch.Err != nil { patchErrs = append(patchErrs, fmt.Errorf("error: %s/%s %vn", info.Mapping.Resource, info.Name, patch.Err)) continue } if saConfig.local || saConfig.dryRun { saConfig.saPrint(patch.Info.Object) continue } patched, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch) if err != nil { patchErrs = append(patchErrs, fmt.Errorf("failed to patch ServiceAccountName %v", err)) continue } info.Refresh(patched, true) if saConfig.record || cmdutil.ContainsChangeCause(info) { if patch, patchType, err := cmdutil.ChangeResourcePatch(info, saConfig.changeCause); err == nil { if patched, err = resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, patchType, patch); err != nil { fmt.Fprintf(saConfig.err, "WARNING: changes to %s/%s can't be recorded: %vn", info.Mapping.Resource, info.Name, err) } } } if len(saConfig.output) > 0 { saConfig.saPrint(patched) } cmdutil.PrintSuccess(saConfig.mapper, saConfig.shortOutput, saConfig.out, info.Mapping.Resource, info.Name, saConfig.dryRun, "serviceaccount updated") } return utilerrors.NewAggregate(patchErrs) } </ins>

<ins> /* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package set import ( "bytes" "fmt" "io" "io/ioutil" "net/http" "path" "testing" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest/fake" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/batch" "k8s.io/kubernetes/pkg/apis/extensions" cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/printers" ) const serviceAccount = "serviceaccount1" const serviceAccountMissingErrString = "serviceaccount is required" const resourceMissingErrString = `You must provide one or more resources by argument or filename. Example resource specifications include: '-f rsrc.yaml' '--filename=rsrc.json' '<resource> <name>' '<resource>'` func TestServiceAccountLocal(t *testing.T) { inputs := []struct { yaml     string apiGroup string }{ {yaml: "../../../../test/fixtures/doc-yaml/user-guide/replication.yaml", apiGroup: api.GroupName}, {yaml: "../../../../test/fixtures/doc-yaml/admin/daemon.yaml", apiGroup: extensions.GroupName}, {yaml: "../../../../test/fixtures/doc-yaml/user-guide/replicaset/redis-slave.yaml", apiGroup: extensions.GroupName}, {yaml: "../../../../test/fixtures/doc-yaml/user-guide/job.yaml", apiGroup: batch.GroupName}, {yaml: "../../../../test/fixtures/doc-yaml/user-guide/deployment.yaml", apiGroup: extensions.GroupName}, {yaml: "../../../../examples/storage/minio/minio-distributed-statefulset.yaml", apiGroup: apps.GroupName}, } f, tf, _, _ := cmdtesting.NewAPIFactory() tf.Client = &fake.RESTClient{ APIRegistry: api.Registry, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#vn%#v", req.Method, req.URL, req) return nil, nil }), } tf.Namespace = "test" out := new(bytes.Buffer) cmd := NewCmdServiceAccount(f, out, out) cmd.SetOutput(out) cmd.Flags().Set("output", "yaml") cmd.Flags().Set("local", "true") for _, input := range inputs { testapi.Default = testapi.Groups[input.apiGroup] tf.Printer = printers.NewVersionedPrinter(&printers.YAMLPrinter{}, testapi.Default.Converter(), *testapi.Default.GroupVersion()) saConfig := serviceAccountConfig{fileNameOptions: resource.FilenameOptions{ Filenames: []string{input.yaml}}, out:   out, local: true} err := saConfig.Complete(f, cmd, []string{serviceAccount}) assert.NoError(t, err) err = saConfig.Run() assert.NoError(t, err) assert.Contains(t, out.String(), "serviceAccountName: "+serviceAccount, fmt.Sprintf("serviceaccount not updated for %s", input.yaml)) } } func TestServiceAccountRemote(t *testing.T) { inputs := []struct { object              runtime.Object apiPrefix, apiGroup string args                []string }{ { object: &extensions.ReplicaSet{ TypeMeta:   metav1.TypeMeta{Kind: "ReplicaSet", APIVersion: api.Registry.GroupOrDie(extensions.GroupName).GroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, apiPrefix: "/apis", apiGroup: extensions.GroupName, args: []string{"replicaset", "nginx", serviceAccount}, }, { object: &extensions.DaemonSet{ TypeMeta:   metav1.TypeMeta{Kind: "DaemonSet", APIVersion: api.Registry.GroupOrDie(extensions.GroupName).GroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, apiPrefix: "/apis", apiGroup: extensions.GroupName, args: []string{"daemonset", "nginx", serviceAccount}, }, { object: &api.ReplicationController{ TypeMeta:   metav1.TypeMeta{Kind: "ReplicationController", APIVersion: api.Registry.GroupOrDie(api.GroupName).GroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, apiPrefix: "/api", apiGroup: api.GroupName, args: []string{"replicationcontroller", "nginx", serviceAccount}}, { object: &extensions.Deployment{ TypeMeta:   metav1.TypeMeta{Kind: "Deployment", APIVersion: api.Registry.GroupOrDie(extensions.GroupName).GroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, apiPrefix: "/apis", apiGroup: extensions.GroupName, args: []string{"deployment", "nginx", serviceAccount}, }, { object: &batch.Job{ TypeMeta:   metav1.TypeMeta{Kind: "Job", APIVersion: api.Registry.GroupOrDie(batch.GroupName).GroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, apiPrefix: "/apis", apiGroup: batch.GroupName, args: []string{"job", "nginx", serviceAccount}, }, { object: &apps.StatefulSet{ TypeMeta:   metav1.TypeMeta{Kind: "StatefulSet", APIVersion: api.Registry.GroupOrDie(apps.GroupName).GroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{Name: "nginx"}, }, apiPrefix: "/apis", apiGroup: apps.GroupName, args: []string{"statefulset", "nginx", serviceAccount}, }, } for _, input := range inputs { groupVersion := api.Registry.GroupOrDie(input.apiGroup).GroupVersion testapi.Default = testapi.Groups[input.apiGroup] f, tf, codec, _ := cmdtesting.NewAPIFactory() tf.Printer = printers.NewVersionedPrinter(&printers.YAMLPrinter{}, testapi.Default.Converter(), *testapi.Default.GroupVersion()) tf.Namespace = "test" tf.CategoryExpander = resource.LegacyCategoryExpander tf.Client = &fake.RESTClient{ APIRegistry:          api.Registry, NegotiatedSerializer: testapi.Default.NegotiatedSerializer(), Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { resourcePath := testapi.Default.ResourcePath(input.args[0]+"s", tf.Namespace, input.args[1]) switch p, m := req.URL.Path, req.Method; { case p == resourcePath && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, input.object)}, nil case p == resourcePath && m == http.MethodPatch: stream, err := req.GetBody() if err != nil { return nil, err } bytes, err := ioutil.ReadAll(stream) if err != nil { return nil, err } assert.Contains(t, string(bytes), `"serviceAccountName":`+`"`+serviceAccount+`"`, fmt.Sprintf("serviceaccount not updated for %#v", input.object)) return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, input.object)}, nil default: t.Errorf("%s: unexpected request: %s %#vn%#v", "serviceaccount", req.Method, req.URL, req) return nil, fmt.Errorf("unexpected request") } }), VersionedAPIPath: path.Join(input.apiPrefix, groupVersion.String()), GroupName:        input.apiGroup, } out := new(bytes.Buffer) cmd := NewCmdServiceAccount(f, out, out) cmd.SetOutput(out) cmd.Flags().Set("output", "yaml") saConfig := serviceAccountConfig{ out:   out, local: false} err := saConfig.Complete(f, cmd, input.args) assert.NoError(t, err) err = saConfig.Run() assert.NoError(t, err) } } func TestServiceAccountValidation(t *testing.T) { inputs := []struct { args        []string errorString string }{ {args: []string{}, errorString: serviceAccountMissingErrString}, {args: []string{serviceAccount}, errorString: resourceMissingErrString}, } for _, input := range inputs { f, tf, _, _ := cmdtesting.NewAPIFactory() tf.Client = &fake.RESTClient{ APIRegistry: api.Registry, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { t.Fatalf("unexpected request: %s %#vn%#v", req.Method, req.URL, req) return nil, nil }), } tf.Namespace = "test" out := bytes.NewBuffer([]byte{}) cmd := NewCmdServiceAccount(f, out, out) cmd.SetOutput(out) saConfig := &serviceAccountConfig{} err := saConfig.Complete(f, cmd, input.args) assert.EqualError(t, err, input.errorString) } } func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser { return bytesBody([]byte(runtime.EncodeOrDie(codec, obj))) } func defaultHeader() http.Header { header := http.Header{} header.Set("Content-Type", runtime.ContentTypeJSON) return header } func bytesBody(bodyBytes []byte) io.ReadCloser { return ioutil.NopCloser(bytes.NewReader(bodyBytes)) } </ins>

Err                error Command            string TmpDir             string <ins> CategoryExpander   resource.CategoryExpander </ins> ClientForMappingFunc             func(mapping *meta.RESTMapping) (resource.RESTClient, error) UnstructuredClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error)

return cmdutil.NewCachedDiscoveryClient(discoveryClient, cacheDir, time.Duration(10*time.Minute)), nil } <ins> func (f *fakeAPIFactory) CategoryExpander() resource.CategoryExpander { if f.tf.CategoryExpander != nil { return f.tf.CategoryExpander } return f.Factory.CategoryExpander() } </ins> func (f *fakeAPIFactory) ClientSetForVersion(requiredVersion *schema.GroupVersion) (internalclientset.Interface, error) { return f.ClientSet() }