From 217e6ff0d0a9fc644278c773ce8aafd76d4b0579 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Thu, 23 Apr 2015 16:27:19 -0700 Subject: [PATCH] First part of improved rolling update, allow dynamic next replication controller generation. --- contrib/completions/bash/kubectl | 9 ++ docs/kubectl.md | 2 +- docs/kubectl_api-versions.md | 2 +- docs/kubectl_cluster-info.md | 2 +- docs/kubectl_config.md | 2 +- docs/kubectl_config_set-cluster.md | 2 +- docs/kubectl_config_set-context.md | 2 +- docs/kubectl_config_set-credentials.md | 2 +- docs/kubectl_config_set.md | 2 +- docs/kubectl_config_unset.md | 2 +- docs/kubectl_config_use-context.md | 2 +- docs/kubectl_config_view.md | 2 +- docs/kubectl_create.md | 2 +- docs/kubectl_delete.md | 2 +- docs/kubectl_describe.md | 2 +- docs/kubectl_exec.md | 2 +- docs/kubectl_expose.md | 2 +- docs/kubectl_get.md | 2 +- docs/kubectl_label.md | 2 +- docs/kubectl_log.md | 2 +- docs/kubectl_namespace.md | 2 +- docs/kubectl_port-forward.md | 2 +- docs/kubectl_proxy.md | 2 +- docs/kubectl_resize.md | 2 +- docs/kubectl_rolling-update.md | 13 +- docs/kubectl_run-container.md | 2 +- docs/kubectl_stop.md | 2 +- docs/kubectl_update.md | 2 +- docs/kubectl_version.md | 2 +- docs/man/man1/kubectl-rolling-update.1 | 33 ++++ pkg/kubectl/cmd/rollingupdate.go | 214 +++++++++++++++++++++---- pkg/kubectl/cmd/rollingupdate_test.go | 177 ++++++++++++++++++++ 32 files changed, 440 insertions(+), 60 deletions(-) create mode 100644 pkg/kubectl/cmd/rollingupdate_test.go diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index ea598112398..4e7e2c958a0 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -405,11 +405,20 @@ _kubectl_rolling-update() flags_with_completion=() flags_completion=() + flags+=("--deployment-label-key=") + flags+=("--dry-run") flags+=("--filename=") two_word_flags+=("-f") flags+=("--help") flags+=("-h") + flags+=("--image=") + flags+=("--no-headers") + flags+=("--output=") + two_word_flags+=("-o") + flags+=("--output-version=") flags+=("--poll-interval=") + flags+=("--template=") + two_word_flags+=("-t") flags+=("--timeout=") flags+=("--update-period=") diff --git a/docs/kubectl.md b/docs/kubectl.md index a18a0262669..12eaad356c0 100644 --- a/docs/kubectl.md +++ b/docs/kubectl.md @@ -66,4 +66,4 @@ kubectl * [kubectl update](kubectl_update.md) - Update a resource by filename or stdin. * [kubectl version](kubectl_version.md) - Print the client and server version information. -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.406236586 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.641615471 +0000 UTC diff --git a/docs/kubectl_api-versions.md b/docs/kubectl_api-versions.md index 94534a580f1..c19dd3b1d39 100644 --- a/docs/kubectl_api-versions.md +++ b/docs/kubectl_api-versions.md @@ -50,4 +50,4 @@ kubectl api-versions ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.405815046 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.640579424 +0000 UTC diff --git a/docs/kubectl_cluster-info.md b/docs/kubectl_cluster-info.md index aecaf26c981..9d7a9e4dcd5 100644 --- a/docs/kubectl_cluster-info.md +++ b/docs/kubectl_cluster-info.md @@ -50,4 +50,4 @@ kubectl cluster-info ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.405639667 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.640382538 +0000 UTC diff --git a/docs/kubectl_config.md b/docs/kubectl_config.md index f50ba53ed7e..2a247e9c52b 100644 --- a/docs/kubectl_config.md +++ b/docs/kubectl_config.md @@ -63,4 +63,4 @@ kubectl config SUBCOMMAND * [kubectl config use-context](kubectl_config_use-context.md) - Sets the current-context in a kubeconfig file * [kubectl config view](kubectl_config_view.md) - displays Merged kubeconfig settings or a specified kubeconfig file. -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.405466384 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.640154327 +0000 UTC diff --git a/docs/kubectl_config_set-cluster.md b/docs/kubectl_config_set-cluster.md index 9aaeedd7f12..3f747caec59 100644 --- a/docs/kubectl_config_set-cluster.md +++ b/docs/kubectl_config_set-cluster.md @@ -65,4 +65,4 @@ $ kubectl config set-cluster e2e --insecure-skip-tls-verify=true ### SEE ALSO * [kubectl config](kubectl_config.md) - config modifies kubeconfig files -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.404357726 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.638291301 +0000 UTC diff --git a/docs/kubectl_config_set-context.md b/docs/kubectl_config_set-context.md index 227eacc30ad..aef55a5b16d 100644 --- a/docs/kubectl_config_set-context.md +++ b/docs/kubectl_config_set-context.md @@ -58,4 +58,4 @@ $ kubectl config set-context gce --user=cluster-admin ### SEE ALSO * [kubectl config](kubectl_config.md) - config modifies kubeconfig files -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.40472172 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.638842283 +0000 UTC diff --git a/docs/kubectl_config_set-credentials.md b/docs/kubectl_config_set-credentials.md index 9d2dee9fca3..39cd1a5ff87 100644 --- a/docs/kubectl_config_set-credentials.md +++ b/docs/kubectl_config_set-credentials.md @@ -78,4 +78,4 @@ $ kubectl set-credentials cluster-admin --client-certificate=~/.kube/admin.crt - ### SEE ALSO * [kubectl config](kubectl_config.md) - config modifies kubeconfig files -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.40454463 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.638596132 +0000 UTC diff --git a/docs/kubectl_config_set.md b/docs/kubectl_config_set.md index 57dd9a1941d..2f1c3211158 100644 --- a/docs/kubectl_config_set.md +++ b/docs/kubectl_config_set.md @@ -52,4 +52,4 @@ kubectl config set PROPERTY_NAME PROPERTY_VALUE ### SEE ALSO * [kubectl config](kubectl_config.md) - config modifies kubeconfig files -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.404916515 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.639475859 +0000 UTC diff --git a/docs/kubectl_config_unset.md b/docs/kubectl_config_unset.md index 5e42124628d..8047df8def2 100644 --- a/docs/kubectl_config_unset.md +++ b/docs/kubectl_config_unset.md @@ -51,4 +51,4 @@ kubectl config unset PROPERTY_NAME ### SEE ALSO * [kubectl config](kubectl_config.md) - config modifies kubeconfig files -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.405094144 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.639702273 +0000 UTC diff --git a/docs/kubectl_config_use-context.md b/docs/kubectl_config_use-context.md index 0f7ada366d1..3a4f7ba6a43 100644 --- a/docs/kubectl_config_use-context.md +++ b/docs/kubectl_config_use-context.md @@ -50,4 +50,4 @@ kubectl config use-context CONTEXT_NAME ### SEE ALSO * [kubectl config](kubectl_config.md) - config modifies kubeconfig files -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.405277784 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.639928664 +0000 UTC diff --git a/docs/kubectl_config_view.md b/docs/kubectl_config_view.md index f3907d7366e..b52fa7768a8 100644 --- a/docs/kubectl_config_view.md +++ b/docs/kubectl_config_view.md @@ -73,4 +73,4 @@ $ kubectl config view -o template --template='{{range .users}}{{ if eq .name "e2 ### SEE ALSO * [kubectl config](kubectl_config.md) - config modifies kubeconfig files -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.404151238 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.638011058 +0000 UTC diff --git a/docs/kubectl_create.md b/docs/kubectl_create.md index d560ba5edf0..b0f5d95dd5f 100644 --- a/docs/kubectl_create.md +++ b/docs/kubectl_create.md @@ -63,4 +63,4 @@ $ cat pod.json | kubectl create -f - ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.400230448 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.633893126 +0000 UTC diff --git a/docs/kubectl_delete.md b/docs/kubectl_delete.md index c120be31f1f..cc343b9dbb2 100644 --- a/docs/kubectl_delete.md +++ b/docs/kubectl_delete.md @@ -81,4 +81,4 @@ $ kubectl delete pods --all ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.400618492 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.634491315 +0000 UTC diff --git a/docs/kubectl_describe.md b/docs/kubectl_describe.md index 71c8c778030..b83976fe616 100644 --- a/docs/kubectl_describe.md +++ b/docs/kubectl_describe.md @@ -63,4 +63,4 @@ $ kubectl describe pods/nginx ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 21:21:05.485819349 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.633584776 +0000 UTC diff --git a/docs/kubectl_exec.md b/docs/kubectl_exec.md index 366f62efe54..7af2248d2e7 100644 --- a/docs/kubectl_exec.md +++ b/docs/kubectl_exec.md @@ -64,4 +64,4 @@ $ kubectl exec -p 123456-7890 -c ruby-container -i -t -- bash -il ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.401622652 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.636226358 +0000 UTC diff --git a/docs/kubectl_expose.md b/docs/kubectl_expose.md index 2c559d94d88..d777f261ba8 100644 --- a/docs/kubectl_expose.md +++ b/docs/kubectl_expose.md @@ -82,4 +82,4 @@ $ kubectl expose streamer --port=4100 --protocol=udp --service-name=video-stream ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.403631642 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.637486796 +0000 UTC diff --git a/docs/kubectl_get.md b/docs/kubectl_get.md index f8da6d7b53b..993cf2bfdfc 100644 --- a/docs/kubectl_get.md +++ b/docs/kubectl_get.md @@ -85,4 +85,4 @@ $ kubectl get rc/web service/frontend pods/web-pod-13je7 ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.399795546 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.633184922 +0000 UTC diff --git a/docs/kubectl_label.md b/docs/kubectl_label.md index a78d96459fb..112ed0fe5cc 100644 --- a/docs/kubectl_label.md +++ b/docs/kubectl_label.md @@ -81,4 +81,4 @@ $ kubectl label pods foo bar- ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.403876136 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.637727935 +0000 UTC diff --git a/docs/kubectl_log.md b/docs/kubectl_log.md index d11b9be7ec6..8a13e3c78b7 100644 --- a/docs/kubectl_log.md +++ b/docs/kubectl_log.md @@ -62,4 +62,4 @@ $ kubectl log -f 123456-7890 ruby-container ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.400998066 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.634970712 +0000 UTC diff --git a/docs/kubectl_namespace.md b/docs/kubectl_namespace.md index 721eb077059..b5a0cb407e7 100644 --- a/docs/kubectl_namespace.md +++ b/docs/kubectl_namespace.md @@ -53,4 +53,4 @@ kubectl namespace [namespace] ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.400806182 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.634703492 +0000 UTC diff --git a/docs/kubectl_port-forward.md b/docs/kubectl_port-forward.md index e0f514679c1..5030b5494ac 100644 --- a/docs/kubectl_port-forward.md +++ b/docs/kubectl_port-forward.md @@ -68,4 +68,4 @@ $ kubectl port-forward -p mypod 0:5000 ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.40181203 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.636454335 +0000 UTC diff --git a/docs/kubectl_proxy.md b/docs/kubectl_proxy.md index b5bfc830c89..7920e0f5c65 100644 --- a/docs/kubectl_proxy.md +++ b/docs/kubectl_proxy.md @@ -65,4 +65,4 @@ $ kubectl proxy --api-prefix=k8s-api ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.402034031 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.636689051 +0000 UTC diff --git a/docs/kubectl_resize.md b/docs/kubectl_resize.md index ebad20e0ea4..83b995f69c1 100644 --- a/docs/kubectl_resize.md +++ b/docs/kubectl_resize.md @@ -68,4 +68,4 @@ $ kubectl resize --current-replicas=2 --replicas=3 replicationcontrollers foo ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.401412855 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.635916597 +0000 UTC diff --git a/docs/kubectl_rolling-update.md b/docs/kubectl_rolling-update.md index 82646afc41b..53757150f86 100644 --- a/docs/kubectl_rolling-update.md +++ b/docs/kubectl_rolling-update.md @@ -23,14 +23,25 @@ $ kubectl rolling-update frontend-v1 -f frontend-v2.json // Update pods of frontend-v1 using JSON data passed into stdin. $ cat frontend-v2.json | kubectl rolling-update frontend-v1 -f - + +// Update the pods of frontend-v1 to frontend-v2 by just changing the image +$ kubectl rolling-update frontend-v1 frontend-v2 --image=image:v2 + ``` ### Options ``` + --deployment-label-key="deployment": The key to use to differentiate between two different controllers, default 'deployment'. Only relevant when --image is specified, ignored otherwise + --dry-run=false: If true, print out the changes that would be made, but don't actually make them. -f, --filename="": Filename or URL to file to use to create the new controller. -h, --help=false: help for rolling-update + --image="": Image to upgrade the controller to. Can not be used with --filename/-f + --no-headers=false: When using the default output, don't print headers. + -o, --output="": Output format. One of: json|yaml|template|templatefile. + --output-version="": Output the formatted object with the given version (default api-version). --poll-interval="3s": Time delay between polling controller status after update. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + -t, --template="": Template string or path to template file to use when -o=template or -o=templatefile. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview] --timeout="5m0s": Max time to wait for a controller to update before giving up. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". --update-period="1m0s": Time to wait between updating pods. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". ``` @@ -68,4 +79,4 @@ $ cat frontend-v2.json | kubectl rolling-update frontend-v1 -f - ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.401210692 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-24 04:16:19.186748349 +0000 UTC diff --git a/docs/kubectl_run-container.md b/docs/kubectl_run-container.md index d895102ac55..1daf164e84a 100644 --- a/docs/kubectl_run-container.md +++ b/docs/kubectl_run-container.md @@ -78,4 +78,4 @@ $ kubectl run-container nginx --image=nginx --overrides='{ "apiVersion": "v1beta ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.402296924 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.636970945 +0000 UTC diff --git a/docs/kubectl_stop.md b/docs/kubectl_stop.md index 5c62076d0ee..6090f70fa5e 100644 --- a/docs/kubectl_stop.md +++ b/docs/kubectl_stop.md @@ -72,4 +72,4 @@ $ kubectl stop -f path/to/resources ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.402507426 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.637194082 +0000 UTC diff --git a/docs/kubectl_update.md b/docs/kubectl_update.md index 1cdbb651d28..3bd196f2bbb 100644 --- a/docs/kubectl_update.md +++ b/docs/kubectl_update.md @@ -67,4 +67,4 @@ $ kubectl update pods my-pod --patch='{ "apiVersion": "v1beta1", "desiredState": ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.400435658 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.634205944 +0000 UTC diff --git a/docs/kubectl_version.md b/docs/kubectl_version.md index 1b93f36b8f7..735bd74bf41 100644 --- a/docs/kubectl_version.md +++ b/docs/kubectl_version.md @@ -51,4 +51,4 @@ kubectl version ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-04-23 00:47:55.405995955 +0000 UTC +###### Auto generated by spf13/cobra at 2015-04-23 23:29:05.640847018 +0000 UTC diff --git a/docs/man/man1/kubectl-rolling-update.1 b/docs/man/man1/kubectl-rolling-update.1 index df9aca32450..e9b79a22542 100644 --- a/docs/man/man1/kubectl-rolling-update.1 +++ b/docs/man/man1/kubectl-rolling-update.1 @@ -22,6 +22,14 @@ existing controller and overwrite at least one (common) label in its replicaSele .SH OPTIONS +.PP +\fB\-\-deployment\-label\-key\fP="deployment" + The key to use to differentiate between two different controllers, default 'deployment'. Only relevant when \-\-image is specified, ignored otherwise + +.PP +\fB\-\-dry\-run\fP=false + If true, print out the changes that would be made, but don't actually make them. + .PP \fB\-f\fP, \fB\-\-filename\fP="" Filename or URL to file to use to create the new controller. @@ -30,10 +38,31 @@ existing controller and overwrite at least one (common) label in its replicaSele \fB\-h\fP, \fB\-\-help\fP=false help for rolling\-update +.PP +\fB\-\-image\fP="" + Image to upgrade the controller to. Can not be used with \-\-filename/\-f + +.PP +\fB\-\-no\-headers\fP=false + When using the default output, don't print headers. + +.PP +\fB\-o\fP, \fB\-\-output\fP="" + Output format. One of: json|yaml|template|templatefile. + +.PP +\fB\-\-output\-version\fP="" + Output the formatted object with the given version (default api\-version). + .PP \fB\-\-poll\-interval\fP="3s" Time delay between polling controller status after update. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". +.PP +\fB\-t\fP, \fB\-\-template\fP="" + Template string or path to template file to use when \-o=template or \-o=templatefile. The template format is golang templates [ +\[la]http://golang.org/pkg/text/template/#pkg-overview\[ra]] + .PP \fB\-\-timeout\fP="5m0s" Max time to wait for a controller to update before giving up. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". @@ -156,6 +185,10 @@ $ kubectl rolling\-update frontend\-v1 \-f frontend\-v2.json // Update pods of frontend\-v1 using JSON data passed into stdin. $ cat frontend\-v2.json | kubectl rolling\-update frontend\-v1 \-f \- +// Update the pods of frontend\-v1 to frontend\-v2 by just changing the image +$ kubectl rolling\-update frontend\-v1 frontend\-v2 \-\-image=image:v2 + + .fi .RE diff --git a/pkg/kubectl/cmd/rollingupdate.go b/pkg/kubectl/cmd/rollingupdate.go index 250339bf18f..76ab39cf557 100644 --- a/pkg/kubectl/cmd/rollingupdate.go +++ b/pkg/kubectl/cmd/rollingupdate.go @@ -17,14 +17,22 @@ limitations under the License. package cmd import ( + "bytes" + "crypto/md5" + "errors" "fmt" "io" "os" + "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/spf13/cobra" ) @@ -41,7 +49,11 @@ existing controller and overwrite at least one (common) label in its replicaSele $ kubectl rolling-update frontend-v1 -f frontend-v2.json // Update pods of frontend-v1 using JSON data passed into stdin. -$ cat frontend-v2.json | kubectl rolling-update frontend-v1 -f -` +$ cat frontend-v2.json | kubectl rolling-update frontend-v1 -f - + +// Update the pods of frontend-v1 to frontend-v2 by just changing the image +$ kubectl rolling-update frontend-v1 frontend-v2 --image=image:v2 +` ) func NewCmdRollingUpdate(f *cmdutil.Factory, out io.Writer) *cobra.Command { @@ -61,64 +73,125 @@ func NewCmdRollingUpdate(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd.Flags().String("poll-interval", pollInterval, `Time delay between polling controller status after update. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`) cmd.Flags().String("timeout", timeout, `Max time to wait for a controller to update before giving up. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`) cmd.Flags().StringP("filename", "f", "", "Filename or URL to file to use to create the new controller.") + cmd.Flags().String("image", "", "Image to upgrade the controller to. Can not be used with --filename/-f") + cmd.Flags().String("deployment-label-key", "deployment", "The key to use to differentiate between two different controllers, default 'deployment'. Only relevant when --image is specified, ignored otherwise") + cmd.Flags().Bool("dry-run", false, "If true, print out the changes that would be made, but don't actually make them.") + cmdutil.AddPrinterFlags(cmd) return cmd } +func validateArguments(cmd *cobra.Command, args []string) (deploymentKey, filename, image, oldName string, err error) { + deploymentKey = cmdutil.GetFlagString(cmd, "deployment-label-key") + filename = cmdutil.GetFlagString(cmd, "filename") + image = cmdutil.GetFlagString(cmd, "image") + + if len(deploymentKey) == 0 { + return "", "", "", "", cmdutil.UsageError(cmd, "--deployment-label-key can not be empty") + } + if len(filename) == 0 && len(image) == 0 { + return "", "", "", "", cmdutil.UsageError(cmd, "Must specify --filename or --image for new controller") + } + if len(filename) != 0 && len(image) != 0 { + return "", "", "", "", cmdutil.UsageError(cmd, "--filename and --image can not both be specified") + } + if len(image) > 0 && len(args) < 2 { + return "", "", "", "", cmdutil.UsageError(cmd, "You must specify a name for your new controller") + } + if len(args) < 1 { + return "", "", "", "", cmdutil.UsageError(cmd, "Must specify the controller to update") + } + + return deploymentKey, filename, image, args[0], nil +} + func RunRollingUpdate(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string) error { if os.Args[1] == "rollingupdate" { printDeprecationWarning("rolling-update", "rollingupdate") } - - filename := cmdutil.GetFlagString(cmd, "filename") - if len(filename) == 0 { - return cmdutil.UsageError(cmd, "Must specify filename for new controller") + deploymentKey, filename, image, oldName, err := validateArguments(cmd, args) + if err != nil { + return err } period := cmdutil.GetFlagDuration(cmd, "update-period") interval := cmdutil.GetFlagDuration(cmd, "poll-interval") timeout := cmdutil.GetFlagDuration(cmd, "timeout") - if len(args) != 1 { - return cmdutil.UsageError(cmd, "Must specify the controller to update") - } - oldName := args[0] + dryrun := cmdutil.GetFlagBool(cmd, "dry-run") cmdNamespace, err := f.DefaultNamespace() if err != nil { return err } - mapper, typer := f.Object() - // TODO: use resource.Builder instead - obj, err := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()). - NamespaceParam(cmdNamespace).RequireNamespace(). - FilenameParam(filename). - Do(). - Object() - if err != nil { - return err - } - newRc, ok := obj.(*api.ReplicationController) - if !ok { - return cmdutil.UsageError(cmd, "%s does not specify a valid ReplicationController", filename) - } - newName := newRc.Name - if oldName == newName { - return cmdutil.UsageError(cmd, "%s cannot have the same name as the existing ReplicationController %s", - filename, oldName) - } - client, err := f.Client() if err != nil { return err } - updater := kubectl.NewRollingUpdater(newRc.Namespace, kubectl.NewRollingUpdaterClient(client)) - // fetch rc - oldRc, err := client.ReplicationControllers(newRc.Namespace).Get(oldName) + oldRc, err := client.ReplicationControllers(cmdNamespace).Get(oldName) if err != nil { return err } + mapper, typer := f.Object() + var newRc *api.ReplicationController + + if len(filename) != 0 { + obj, err := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()). + NamespaceParam(cmdNamespace).RequireNamespace(). + FilenameParam(filename). + Do(). + Object() + if err != nil { + return err + } + var ok bool + newRc, ok = obj.(*api.ReplicationController) + if !ok { + return cmdutil.UsageError(cmd, "%s does not specify a valid ReplicationController", filename) + } + } + if len(image) != 0 { + newName := args[1] + var err error + // load the old RC into the "new" RC + if newRc, err = client.ReplicationControllers(cmdNamespace).Get(oldName); err != nil { + return err + } + + if len(newRc.Spec.Template.Spec.Containers) > 1 { + // TODO: support multi-container image update. + return errors.New("Image update is not supported for multi-container pods") + } + if len(newRc.Spec.Template.Spec.Containers) == 0 { + return cmdutil.UsageError(cmd, "Pod has no containers! (%v)", newRc) + } + newRc.Spec.Template.Spec.Containers[0].Image = image + + newHash, err := hashObject(newRc, client.Codec) + if err != nil { + return err + } + newRc.Name = newName + + newRc.Spec.Selector[deploymentKey] = newHash + newRc.Spec.Template.Labels[deploymentKey] = newHash + newRc.ResourceVersion = "" + + if _, found := oldRc.Spec.Selector[deploymentKey]; !found { + if err := addDeploymentKeyToReplicationController(oldRc, client, deploymentKey, cmdNamespace, out); err != nil { + return err + } + } + } + newName := newRc.Name + if oldName == newName { + return cmdutil.UsageError(cmd, "%s cannot have the same name as the existing ReplicationController %s", + filename, oldName) + } + + updater := kubectl.NewRollingUpdater(newRc.Namespace, kubectl.NewRollingUpdaterClient(client)) + var hasLabel bool for key, oldValue := range oldRc.Spec.Selector { if newValue, ok := newRc.Spec.Selector[key]; ok && newValue != oldValue { @@ -134,6 +207,18 @@ func RunRollingUpdate(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, arg if newRc.Spec.Replicas == 0 { newRc.Spec.Replicas = oldRc.Spec.Replicas } + if dryrun { + oldRcData := &bytes.Buffer{} + if err := f.PrintObject(cmd, oldRc, oldRcData); err != nil { + return err + } + newRcData := &bytes.Buffer{} + if err := f.PrintObject(cmd, newRc, newRcData); err != nil { + return err + } + fmt.Fprintf(out, "Rolling from:\n%s\nTo:\n%s\n", string(oldRcData.Bytes()), string(newRcData.Bytes())) + return nil + } err = updater.Update(&kubectl.RollingUpdaterConfig{ Out: out, OldRc: oldRc, @@ -150,3 +235,68 @@ func RunRollingUpdate(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, arg fmt.Fprintf(out, "%s\n", newName) return nil } + +func hashObject(obj runtime.Object, codec runtime.Codec) (string, error) { + data, err := codec.Encode(obj) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", md5.Sum(data)), nil +} + +const MaxRetries = 3 + +func addDeploymentKeyToReplicationController(oldRc *api.ReplicationController, client *client.Client, deploymentKey, namespace string, out io.Writer) error { + oldHash, err := hashObject(oldRc, client.Codec) + if err != nil { + return err + } + // Update all labels to include the new hash, so they are correctly adopted + // TODO: extract the code from the label command and re-use it here. + podList, err := client.Pods(namespace).List(labels.SelectorFromSet(oldRc.Spec.Selector), fields.Everything()) + if err != nil { + return err + } + for ix := range podList.Items { + pod := &podList.Items[ix] + if pod.Labels == nil { + pod.Labels = map[string]string{ + deploymentKey: oldHash, + } + } else { + pod.Labels[deploymentKey] = oldHash + } + err = nil + delay := 3 + for i := 0; i < MaxRetries; i++ { + _, err = client.Pods(namespace).Update(pod) + if err != nil { + fmt.Fprint(out, "Error updating pod (%v), retrying after %d seconds", err, delay) + time.Sleep(time.Second * time.Duration(delay)) + delay *= delay + } else { + break + } + } + if err != nil { + return err + } + } + if oldRc.Spec.Selector == nil { + oldRc.Spec.Selector = map[string]string{} + } + if oldRc.Spec.Template.Labels == nil { + oldRc.Spec.Template.Labels = map[string]string{} + } + oldRc.Spec.Selector[deploymentKey] = oldHash + oldRc.Spec.Template.Labels[deploymentKey] = oldHash + + if _, err := client.ReplicationControllers(namespace).Update(oldRc); err != nil { + return err + } + // Note there is still a race here, if a pod was created during the update phase. + // It's unlikely, but it could happen, and if it does, we'll create extra pods. + // TODO: Clean up orphaned pods here. + + return nil +} diff --git a/pkg/kubectl/cmd/rollingupdate_test.go b/pkg/kubectl/cmd/rollingupdate_test.go new file mode 100644 index 00000000000..9598374549a --- /dev/null +++ b/pkg/kubectl/cmd/rollingupdate_test.go @@ -0,0 +1,177 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 cmd + +import ( + "bytes" + "net/http" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +func TestValidateArgs(t *testing.T) { + f, _, _ := NewAPIFactory() + + tests := []struct { + flags map[string]string + args []string + expectErr bool + testName string + }{ + { + expectErr: true, + testName: "nothing", + }, + { + flags: map[string]string{}, + args: []string{"foo"}, + expectErr: true, + testName: "no file, no image", + }, + { + flags: map[string]string{ + "filename": "bar.yaml", + }, + args: []string{"foo"}, + testName: "valid file example", + }, + { + flags: map[string]string{ + "image": "foo:v2", + }, + args: []string{"foo"}, + expectErr: true, + testName: "missing second image name", + }, + { + flags: map[string]string{ + "image": "foo:v2", + }, + args: []string{"foo", "foo-v2"}, + testName: "valid image example", + }, + { + flags: map[string]string{ + "image": "foo:v2", + "filename": "bar.yaml", + }, + args: []string{"foo", "foo-v2"}, + expectErr: true, + testName: "both filename and image example", + }, + } + for _, test := range tests { + out := &bytes.Buffer{} + cmd := NewCmdRollingUpdate(f, out) + + if test.flags != nil { + for key, val := range test.flags { + cmd.Flags().Set(key, val) + } + } + _, _, _, _, err := validateArguments(cmd, test.args) + if err != nil && !test.expectErr { + t.Errorf("unexpected error: %v (%s)", err, test.testName) + } + if err == nil && test.expectErr { + t.Error("unexpected non-error (%s)", test.testName) + } + } +} + +func TestAddDeploymentHash(t *testing.T) { + buf := &bytes.Buffer{} + f, tf, codec := NewAPIFactory() + rc := &api.ReplicationController{ + ObjectMeta: api.ObjectMeta{Name: "rc"}, + Spec: api.ReplicationControllerSpec{ + Selector: map[string]string{ + "foo": "bar", + }, + Template: &api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + } + + podList := &api.PodList{ + Items: []api.Pod{ + {ObjectMeta: api.ObjectMeta{Name: "foo"}}, + {ObjectMeta: api.ObjectMeta{Name: "bar"}}, + {ObjectMeta: api.ObjectMeta{Name: "baz"}}, + }, + } + + seen := util.StringSet{} + updatedRc := false + 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 == "/api/v1beta3/namespaces/default/pods" && m == "GET": + if req.URL.RawQuery != "labelSelector=foo%3Dbar" { + t.Errorf("Unexpected query string: %s", req.URL.RawQuery) + } + return &http.Response{StatusCode: 200, Body: objBody(codec, podList)}, nil + case p == "/api/v1beta3/namespaces/default/pods/foo" && m == "PUT": + seen.Insert("foo") + return &http.Response{StatusCode: 200, Body: objBody(codec, &podList.Items[0])}, nil + case p == "/api/v1beta3/namespaces/default/pods/bar" && m == "PUT": + seen.Insert("bar") + return &http.Response{StatusCode: 200, Body: objBody(codec, &podList.Items[1])}, nil + case p == "/api/v1beta3/namespaces/default/pods/baz" && m == "PUT": + seen.Insert("baz") + return &http.Response{StatusCode: 200, Body: objBody(codec, &podList.Items[2])}, nil + case p == "/api/v1beta3/namespaces/default/replicationcontrollers/rc" && m == "PUT": + updatedRc = true + return &http.Response{StatusCode: 200, Body: objBody(codec, rc)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + tf.ClientConfig = &client.Config{Version: latest.Version} + tf.Namespace = "test" + + client, err := f.Client() + if err != nil { + t.Errorf("unexpected error: %v", err) + t.Fail() + return + } + + if err := addDeploymentKeyToReplicationController(rc, client, "hash", api.NamespaceDefault, buf); err != nil { + t.Errorf("unexpected error: %v", err) + } + for _, pod := range podList.Items { + if !seen.Has(pod.Name) { + t.Errorf("Missing update for pod: %s", pod.Name) + } + } + if !updatedRc { + t.Errorf("Failed to update replication controller with new labels") + } +}