diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 96d10ac6ec9..d30b21b3eab 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -331,15 +331,19 @@ _kubectl_update() flags_with_completion=() flags_completion=() + flags+=("--cascade") flags+=("--filename=") flags_with_completion+=("--filename") flags_completion+=("__handle_filename_extension_flag json|yaml|yml") two_word_flags+=("-f") flags_with_completion+=("-f") flags_completion+=("__handle_filename_extension_flag json|yaml|yml") + flags+=("--force") + flags+=("--grace-period=") flags+=("--help") flags+=("-h") flags+=("--patch=") + flags+=("--timeout=") must_have_one_flag=() must_have_one_flag+=("--filename=") diff --git a/docs/kubectl_delete.md b/docs/kubectl_delete.md index 9df402f78ba..21190255f0f 100644 --- a/docs/kubectl_delete.md +++ b/docs/kubectl_delete.md @@ -43,7 +43,7 @@ $ kubectl delete pods --all ``` --all=false: [-all] to select all the specified resources. - --cascade=true: If true, cascade the delete resources managed by this resource (e.g. Pods created by a ReplicationController). Default true. + --cascade=true: If true, cascade the deletion of the resources managed by this resource (e.g. Pods created by a ReplicationController). Default true. -f, --filename=[]: Filename, directory, or URL to a file containing the resource to delete. --grace-period=-1: Period of time in seconds given to the resource to terminate gracefully. Ignored if negative. -h, --help=false: help for delete @@ -84,6 +84,6 @@ $ kubectl delete pods --all ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-06-03 18:21:01.053120485 +0000 UTC +###### Auto generated by spf13/cobra at 2015-06-26 00:15:55.835198432 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_delete.md?pixel)]() diff --git a/docs/kubectl_update.md b/docs/kubectl_update.md index d4b1adbac3f..b3c8ab650a2 100644 --- a/docs/kubectl_update.md +++ b/docs/kubectl_update.md @@ -24,14 +24,21 @@ $ cat pod.json | kubectl update -f - // Partially update a node using strategic merge patch kubectl --api-version=v1 update node k8s-node-1 --patch='{"spec":{"unschedulable":true}}' + +// Force update, delete and then re-create the resource +kubectl update --force -f pod.json ``` ### Options ``` + --cascade=false: Only relevant during a force update. If true, cascade the deletion of the resources managed by this resource (e.g. Pods created by a ReplicationController). Default true. -f, --filename=[]: Filename, directory, or URL to file to use to update the resource. + --force=false: Delete and re-create the specified resource + --grace-period=-1: Only relevant during a force update. Period of time in seconds given to the old resource to terminate gracefully. Ignored if negative. -h, --help=false: help for update --patch="": A JSON document to override the existing resource. The resource is downloaded, patched with the JSON, then updated. + --timeout=0: Only relevant during a force update. The length of time to wait before giving up on a delete of the old resource, zero means determine a timeout from the size of the object ``` ### Options inherited from parent commands @@ -66,6 +73,6 @@ kubectl --api-version=v1 update node k8s-node-1 --patch='{"spec":{"unschedulable ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-06-18 19:03:00.935576604 +0000 UTC +###### Auto generated by spf13/cobra at 2015-06-26 00:15:55.835055081 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_update.md?pixel)]() diff --git a/docs/man/man1/kubectl-delete.1 b/docs/man/man1/kubectl-delete.1 index c1c74f614fd..7324b92f6f0 100644 --- a/docs/man/man1/kubectl-delete.1 +++ b/docs/man/man1/kubectl-delete.1 @@ -35,7 +35,7 @@ will be lost along with the rest of the resource. .PP \fB\-\-cascade\fP=true - If true, cascade the delete resources managed by this resource (e.g. Pods created by a ReplicationController). Default true. + If true, cascade the deletion of the resources managed by this resource (e.g. Pods created by a ReplicationController). Default true. .PP \fB\-f\fP, \fB\-\-filename\fP=[] diff --git a/docs/man/man1/kubectl-update.1 b/docs/man/man1/kubectl-update.1 index 8feba20c617..3b5632c3dc1 100644 --- a/docs/man/man1/kubectl-update.1 +++ b/docs/man/man1/kubectl-update.1 @@ -20,10 +20,22 @@ JSON and YAML formats are accepted. .SH OPTIONS +.PP +\fB\-\-cascade\fP=false + Only relevant during a force update. If true, cascade the deletion of the resources managed by this resource (e.g. Pods created by a ReplicationController). Default true. + .PP \fB\-f\fP, \fB\-\-filename\fP=[] Filename, directory, or URL to file to use to update the resource. +.PP +\fB\-\-force\fP=false + Delete and re\-create the specified resource + +.PP +\fB\-\-grace\-period\fP=\-1 + Only relevant during a force update. Period of time in seconds given to the old resource to terminate gracefully. Ignored if negative. + .PP \fB\-h\fP, \fB\-\-help\fP=false help for update @@ -32,6 +44,10 @@ JSON and YAML formats are accepted. \fB\-\-patch\fP="" A JSON document to override the existing resource. The resource is downloaded, patched with the JSON, then updated. +.PP +\fB\-\-timeout\fP=0 + Only relevant during a force update. The length of time to wait before giving up on a delete of the old resource, zero means determine a timeout from the size of the object + .SH OPTIONS INHERITED FROM PARENT COMMANDS .PP @@ -145,6 +161,9 @@ $ cat pod.json | kubectl update \-f \- // Partially update a node using strategic merge patch kubectl \-\-api\-version=v1 update node k8s\-node\-1 \-\-patch='\{"spec":\{"unschedulable":true\}\}' +// Force update, delete and then re\-create the resource +kubectl update \-\-force \-f pod.json + .fi .RE diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 4500627d730..939c5f371a9 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -387,6 +387,13 @@ for version in "${kube_api_versions[@]}"; do # Post-condition: valid-pod POD has image nginx kube::test::get_object_assert pods "{{range.items}}{{$image_field}}:{{end}}" 'nginx:' + ## --force update pod can change other field, e.g., spec.container.name + # Command + kubectl get "${kube_flags[@]}" pod valid-pod -o json | sed 's/"kubernetes-serve-hostname"/"update-k8s-serve-hostname"/g' > tmp-valid-pod.json + kubectl update "${kube_flags[@]}" --force -f tmp-valid-pod.json + # Post-condition: spec.container.name = "update-k8s-serve-hostname" + kube::test::get_object_assert 'pod valid-pod' "{{(index .spec.containers 0).name}}" 'update-k8s-serve-hostname' + ### Overwriting an existing label is not permitted # Pre-condition: name is valid-pod kube::test::get_object_assert 'pod valid-pod' "{{${labels_field}.name}}" 'valid-pod' diff --git a/pkg/kubectl/cmd/delete.go b/pkg/kubectl/cmd/delete.go index 8780ae2bb7a..0ee85c86c51 100644 --- a/pkg/kubectl/cmd/delete.go +++ b/pkg/kubectl/cmd/delete.go @@ -75,7 +75,7 @@ func NewCmdDelete(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd.Flags().StringP("selector", "l", "", "Selector (label query) to filter on.") cmd.Flags().Bool("all", false, "[-all] to select all the specified resources.") cmd.Flags().Bool("ignore-not-found", false, "Treat \"resource not found\" as a successful delete.") - cmd.Flags().Bool("cascade", true, "If true, cascade the delete resources managed by this resource (e.g. Pods created by a ReplicationController). Default true.") + cmd.Flags().Bool("cascade", true, "If true, cascade the deletion of the resources managed by this resource (e.g. Pods created by a ReplicationController). Default true.") cmd.Flags().Int("grace-period", -1, "Period of time in seconds given to the resource to terminate gracefully. Ignored if negative.") cmd.Flags().Duration("timeout", 0, "The length of time to wait before giving up on a delete, zero means determine a timeout from the size of the object") return cmd diff --git a/pkg/kubectl/cmd/update.go b/pkg/kubectl/cmd/update.go index 84378040afc..149e8236390 100644 --- a/pkg/kubectl/cmd/update.go +++ b/pkg/kubectl/cmd/update.go @@ -27,6 +27,7 @@ import ( cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/golang/glog" ) const ( @@ -40,7 +41,10 @@ $ kubectl update -f pod.json $ cat pod.json | kubectl update -f - // Partially update a node using strategic merge patch -kubectl --api-version=v1 update node k8s-node-1 --patch='{"spec":{"unschedulable":true}}'` +kubectl --api-version=v1 update node k8s-node-1 --patch='{"spec":{"unschedulable":true}}' + +// Force update, delete and then re-create the resource +kubectl update --force -f pod.json` ) func NewCmdUpdate(f *cmdutil.Factory, out io.Writer) *cobra.Command { @@ -60,6 +64,10 @@ func NewCmdUpdate(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd.MarkFlagRequired("filename") cmd.Flags().String("patch", "", "A JSON document to override the existing resource. The resource is downloaded, patched with the JSON, then updated.") cmd.MarkFlagRequired("patch") + cmd.Flags().Bool("force", false, "Delete and re-create the specified resource") + cmd.Flags().Bool("cascade", false, "Only relevant during a force update. If true, cascade the deletion of the resources managed by this resource (e.g. Pods created by a ReplicationController). Default true.") + cmd.Flags().Int("grace-period", -1, "Only relevant during a force update. Period of time in seconds given to the old resource to terminate gracefully. Ignored if negative.") + cmd.Flags().Duration("timeout", 0, "Only relevant during a force update. The length of time to wait before giving up on a delete of the old resource, zero means determine a timeout from the size of the object") return cmd } @@ -74,6 +82,7 @@ func RunUpdate(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str return err } + force := cmdutil.GetFlagBool(cmd, "force") patch := cmdutil.GetFlagString(cmd, "patch") if len(filenames) == 0 && len(patch) == 0 { return cmdutil.UsageError(cmd, "Must specify --filename or --patch to update") @@ -81,6 +90,9 @@ func RunUpdate(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str if len(filenames) != 0 && len(patch) != 0 { return cmdutil.UsageError(cmd, "Can not specify both --filename and --patch") } + if len(filenames) == 0 && force { + return cmdutil.UsageError(cmd, "--force can only be used with --filename") + } // TODO: Make patching work with -f, updating with patched JSON input files if len(filenames) == 0 { @@ -95,6 +107,10 @@ func RunUpdate(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str return cmdutil.UsageError(cmd, "Must specify --filename to update") } + if force { + return forceUpdate(f, out, cmd, args, filenames) + } + mapper, typer := f.Object() r := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()). Schema(schema). @@ -160,3 +176,76 @@ func updateWithPatch(cmd *cobra.Command, args []string, f *cmdutil.Factory, patc _, err = helper.Patch(namespace, name, api.StrategicMergePatchType, []byte(patch)) return name, err } + +func forceUpdate(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string, filenames util.StringList) error { + schema, err := f.Validator() + if err != nil { + return err + } + + cmdNamespace, err := f.DefaultNamespace() + if err != nil { + return err + } + + mapper, typer := f.Object() + r := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()). + ContinueOnError(). + NamespaceParam(cmdNamespace).DefaultNamespace(). + FilenameParam(filenames...). + ResourceTypeOrNameArgs(false, args...).RequireObject(false). + Flatten(). + Do() + err = r.Err() + if err != nil { + return err + } + //Update will create a resource if it doesn't exist already, so ignore not found error + ignoreNotFound := true + // By default use a reaper to delete all related resources. + if cmdutil.GetFlagBool(cmd, "cascade") { + glog.Warningf("\"cascade\" is set, kubectl will delete and re-create all resources managed by this resource (e.g. Pods created by a ReplicationController). Consider using \"kubectl rolling-update\" if you want to update a ReplicationController together with its Pods.") + err = ReapResult(r, f, out, cmdutil.GetFlagBool(cmd, "cascade"), ignoreNotFound, cmdutil.GetFlagDuration(cmd, "timeout"), cmdutil.GetFlagInt(cmd, "grace-period")) + } else { + err = DeleteResult(r, out, ignoreNotFound) + } + if err != nil { + return err + } + + r = resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()). + Schema(schema). + ContinueOnError(). + NamespaceParam(cmdNamespace).RequireNamespace(). + FilenameParam(filenames...). + Flatten(). + Do() + err = r.Err() + if err != nil { + return err + } + + count := 0 + err = r.Visit(func(info *resource.Info) error { + data, err := info.Mapping.Codec.Encode(info.Object) + if err != nil { + return err + } + obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, data) + if err != nil { + return err + } + count++ + info.Refresh(obj, true) + printObjectSpecificMessage(obj, out) + fmt.Fprintf(out, "%s/%s\n", info.Mapping.Resource, info.Name) + return nil + }) + if err != nil { + return err + } + if count == 0 { + return fmt.Errorf("no objects passed to update") + } + return nil +} diff --git a/pkg/kubectl/cmd/update_test.go b/pkg/kubectl/cmd/update_test.go index ef56aabb11a..b0221295a7c 100644 --- a/pkg/kubectl/cmd/update_test.go +++ b/pkg/kubectl/cmd/update_test.go @@ -34,10 +34,10 @@ func TestUpdateObject(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "GET": - return &http.Response{StatusCode: 200, Body: objBody(codec, &rc.Items[0])}, nil - case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "PUT": + case p == "/namespaces/test/replicationcontrollers/redis-master" && (m == "GET" || m == "PUT" || m == "DELETE"): return &http.Response{StatusCode: 200, Body: objBody(codec, &rc.Items[0])}, nil + case p == "/namespaces/test/replicationcontrollers" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil @@ -55,6 +55,15 @@ func TestUpdateObject(t *testing.T) { if buf.String() != "replicationcontrollers/rc1\n" { t.Errorf("unexpected output: %s", buf.String()) } + + buf.Reset() + cmd.Flags().Set("force", "true") + cmd.Flags().Set("cascade", "false") + cmd.Run(cmd, []string{}) + + if buf.String() != "replicationcontrollers/redis-master\nreplicationcontrollers/rc1\n" { + t.Errorf("unexpected output: %s", buf.String()) + } } func TestUpdateMultipleObject(t *testing.T) { @@ -66,14 +75,14 @@ func TestUpdateMultipleObject(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "GET": + case p == "/namespaces/test/replicationcontrollers/redis-master" && (m == "GET" || m == "PUT" || m == "DELETE"): return &http.Response{StatusCode: 200, Body: objBody(codec, &rc.Items[0])}, nil - case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "PUT": - return &http.Response{StatusCode: 200, Body: objBody(codec, &rc.Items[0])}, nil - case p == "/namespaces/test/services/frontend" && m == "GET": - return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil - case p == "/namespaces/test/services/frontend" && m == "PUT": + case p == "/namespaces/test/replicationcontrollers" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &rc.Items[0])}, nil + case p == "/namespaces/test/services/frontend" && (m == "GET" || m == "PUT" || m == "DELETE"): return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil + case p == "/namespaces/test/services" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &svc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil @@ -91,6 +100,15 @@ func TestUpdateMultipleObject(t *testing.T) { if buf.String() != "replicationcontrollers/rc1\nservices/baz\n" { t.Errorf("unexpected output: %s", buf.String()) } + + buf.Reset() + cmd.Flags().Set("force", "true") + cmd.Flags().Set("cascade", "false") + cmd.Run(cmd, []string{}) + + if buf.String() != "replicationcontrollers/redis-master\nservices/frontend\nreplicationcontrollers/rc1\nservices/baz\n" { + t.Errorf("unexpected output: %s", buf.String()) + } } func TestUpdateDirectory(t *testing.T) { @@ -102,10 +120,14 @@ func TestUpdateDirectory(t *testing.T) { Codec: codec, Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { - case strings.HasPrefix(p, "/namespaces/test/services/") && (m == "GET" || m == "PUT"): + case strings.HasPrefix(p, "/namespaces/test/services/") && (m == "GET" || m == "PUT" || m == "DELETE"): return &http.Response{StatusCode: 200, Body: objBody(codec, &svc.Items[0])}, nil - case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers/") && (m == "GET" || m == "PUT"): + case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers/") && (m == "GET" || m == "PUT" || m == "DELETE"): return &http.Response{StatusCode: 200, Body: objBody(codec, &rc.Items[0])}, nil + case strings.HasPrefix(p, "/namespaces/test/services") && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &svc.Items[0])}, nil + case strings.HasPrefix(p, "/namespaces/test/replicationcontrollers") && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &rc.Items[0])}, nil default: t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) return nil, nil @@ -123,4 +145,47 @@ func TestUpdateDirectory(t *testing.T) { if buf.String() != "replicationcontrollers/rc1\nservices/baz\nreplicationcontrollers/rc1\nservices/baz\nreplicationcontrollers/rc1\nservices/baz\n" { t.Errorf("unexpected output: %s", buf.String()) } + + buf.Reset() + cmd.Flags().Set("force", "true") + cmd.Flags().Set("cascade", "false") + cmd.Run(cmd, []string{}) + + if buf.String() != "replicationcontrollers/frontend\nservices/frontend\nreplicationcontrollers/redis-master\nservices/redis-master\nreplicationcontrollers/redis-slave\nservices/redis-slave\n"+ + "replicationcontrollers/rc1\nservices/baz\nreplicationcontrollers/rc1\nservices/baz\nreplicationcontrollers/rc1\nservices/baz\n" { + t.Errorf("unexpected output: %s", buf.String()) + } +} + +func TestForceUpdateObjectNotFound(t *testing.T) { + _, _, rc := testData() + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE": + return &http.Response{StatusCode: 404, Body: stringBody("")}, nil + case p == "/namespaces/test/replicationcontrollers" && m == "POST": + return &http.Response{StatusCode: 201, Body: objBody(codec, &rc.Items[0])}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) + + cmd := NewCmdUpdate(f, buf) + cmd.Flags().Set("filename", "../../../examples/guestbook/redis-master-controller.yaml") + cmd.Flags().Set("force", "true") + cmd.Flags().Set("cascade", "false") + cmd.Run(cmd, []string{}) + + if buf.String() != "replicationcontrollers/rc1\n" { + t.Errorf("unexpected output: %s", buf.String()) + } }