Commit f3ac5572 authored by chandanmad's avatar chandanmad

Changes for updating serviceaccount of a resource.

parent 58c85e27
...@@ -106,6 +106,7 @@ docs/man/man1/kubectl-scale.1 ...@@ -106,6 +106,7 @@ docs/man/man1/kubectl-scale.1
docs/man/man1/kubectl-set-image.1 docs/man/man1/kubectl-set-image.1
docs/man/man1/kubectl-set-resources.1 docs/man/man1/kubectl-set-resources.1
docs/man/man1/kubectl-set-selector.1 docs/man/man1/kubectl-set-selector.1
docs/man/man1/kubectl-set-serviceaccount.1
docs/man/man1/kubectl-set-subject.1 docs/man/man1/kubectl-set-subject.1
docs/man/man1/kubectl-set.1 docs/man/man1/kubectl-set.1
docs/man/man1/kubectl-stop.1 docs/man/man1/kubectl-stop.1
...@@ -201,6 +202,7 @@ docs/user-guide/kubectl/kubectl_set.md ...@@ -201,6 +202,7 @@ docs/user-guide/kubectl/kubectl_set.md
docs/user-guide/kubectl/kubectl_set_image.md docs/user-guide/kubectl/kubectl_set_image.md
docs/user-guide/kubectl/kubectl_set_resources.md docs/user-guide/kubectl/kubectl_set_resources.md
docs/user-guide/kubectl/kubectl_set_selector.md docs/user-guide/kubectl/kubectl_set_selector.md
docs/user-guide/kubectl/kubectl_set_serviceaccount.md
docs/user-guide/kubectl/kubectl_set_subject.md docs/user-guide/kubectl/kubectl_set_subject.md
docs/user-guide/kubectl/kubectl_taint.md docs/user-guide/kubectl/kubectl_taint.md
docs/user-guide/kubectl/kubectl_top.md docs/user-guide/kubectl/kubectl_top.md
......
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.
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.
...@@ -12,6 +12,7 @@ go_library( ...@@ -12,6 +12,7 @@ go_library(
"set_image.go", "set_image.go",
"set_resources.go", "set_resources.go",
"set_selector.go", "set_selector.go",
"set_serviceaccount.go",
"set_subject.go", "set_subject.go",
], ],
visibility = ["//build/visible_to:pkg_kubectl_cmd_set_CONSUMERS"], visibility = ["//build/visible_to:pkg_kubectl_cmd_set_CONSUMERS"],
...@@ -42,15 +43,19 @@ go_test( ...@@ -42,15 +43,19 @@ go_test(
"set_image_test.go", "set_image_test.go",
"set_resources_test.go", "set_resources_test.go",
"set_selector_test.go", "set_selector_test.go",
"set_serviceaccount_test.go",
"set_subject_test.go", "set_subject_test.go",
"set_test.go", "set_test.go",
], ],
data = [ data = [
"//examples:config", "//examples:config",
"//test/fixtures",
], ],
library = ":go_default_library", library = ":go_default_library",
deps = [ deps = [
"//pkg/api:go_default_library", "//pkg/api:go_default_library",
"//pkg/api/testapi:go_default_library",
"//pkg/apis/apps:go_default_library",
"//pkg/apis/batch:go_default_library", "//pkg/apis/batch:go_default_library",
"//pkg/apis/extensions:go_default_library", "//pkg/apis/extensions:go_default_library",
"//pkg/apis/rbac:go_default_library", "//pkg/apis/rbac:go_default_library",
......
...@@ -45,6 +45,6 @@ func NewCmdSet(f cmdutil.Factory, out, err io.Writer) *cobra.Command { ...@@ -45,6 +45,6 @@ func NewCmdSet(f cmdutil.Factory, out, err io.Writer) *cobra.Command {
cmd.AddCommand(NewCmdResources(f, out, err)) cmd.AddCommand(NewCmdResources(f, out, err))
cmd.AddCommand(NewCmdSelector(f, out)) cmd.AddCommand(NewCmdSelector(f, out))
cmd.AddCommand(NewCmdSubject(f, out, err)) cmd.AddCommand(NewCmdSubject(f, out, err))
cmd.AddCommand(NewCmdServiceAccount(f, out, err))
return cmd return cmd
} }
/*
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 %v\n", 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: %v\n", 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)
}
/*
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 %#v\n%#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 %#v\n%#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 %#v\n%#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))
}
...@@ -240,6 +240,7 @@ type TestFactory struct { ...@@ -240,6 +240,7 @@ type TestFactory struct {
Err error Err error
Command string Command string
TmpDir string TmpDir string
CategoryExpander resource.CategoryExpander
ClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error) ClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error)
UnstructuredClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error) UnstructuredClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error)
...@@ -622,6 +623,13 @@ func (f *fakeAPIFactory) DiscoveryClient() (discovery.CachedDiscoveryInterface, ...@@ -622,6 +623,13 @@ func (f *fakeAPIFactory) DiscoveryClient() (discovery.CachedDiscoveryInterface,
return cmdutil.NewCachedDiscoveryClient(discoveryClient, cacheDir, time.Duration(10*time.Minute)), nil return cmdutil.NewCachedDiscoveryClient(discoveryClient, cacheDir, time.Duration(10*time.Minute)), nil
} }
func (f *fakeAPIFactory) CategoryExpander() resource.CategoryExpander {
if f.tf.CategoryExpander != nil {
return f.tf.CategoryExpander
}
return f.Factory.CategoryExpander()
}
func (f *fakeAPIFactory) ClientSetForVersion(requiredVersion *schema.GroupVersion) (internalclientset.Interface, error) { func (f *fakeAPIFactory) ClientSetForVersion(requiredVersion *schema.GroupVersion) (internalclientset.Interface, error) {
return f.ClientSet() return f.ClientSet()
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment