From 4332472bdea415e90fddb23f63771f6e963d0279 Mon Sep 17 00:00:00 2001 From: Janet Kuo Date: Tue, 10 May 2016 17:26:39 -0700 Subject: [PATCH] Add 'kubectl set image' --- .generated_docs | 2 + contrib/completions/bash/kubectl | 68 +++++ docs/man/man1/kubectl-autoscale.1 | 2 +- docs/man/man1/kubectl-expose.1 | 2 +- docs/man/man1/kubectl-rolling-update.1 | 2 +- docs/man/man1/kubectl-set-image.1 | 206 +++++++++++++++ docs/man/man1/kubectl-set.1 | 2 +- docs/user-guide/kubectl/kubectl_autoscale.md | 4 +- docs/user-guide/kubectl/kubectl_expose.md | 4 +- .../kubectl/kubectl_rolling-update.md | 4 +- docs/user-guide/kubectl/kubectl_set.md | 1 + docs/user-guide/kubectl/kubectl_set_image.md | 116 +++++++++ docs/yaml/kubectl/kubectl_autoscale.yaml | 2 +- docs/yaml/kubectl/kubectl_expose.yaml | 2 +- docs/yaml/kubectl/kubectl_rolling-update.yaml | 2 +- docs/yaml/kubectl/kubectl_set.yaml | 1 + hack/test-cmd.sh | 39 +++ hack/testdata/deployment-multicontainer.yaml | 23 ++ pkg/kubectl/cmd/annotate.go | 48 +--- pkg/kubectl/cmd/autoscale.go | 5 +- pkg/kubectl/cmd/create_configmap.go | 2 +- pkg/kubectl/cmd/create_namespace.go | 2 +- pkg/kubectl/cmd/create_secret.go | 4 +- pkg/kubectl/cmd/create_serviceaccount.go | 2 +- pkg/kubectl/cmd/expose.go | 5 +- pkg/kubectl/cmd/label.go | 22 +- pkg/kubectl/cmd/rollingupdate.go | 4 +- pkg/kubectl/cmd/run.go | 5 +- pkg/kubectl/cmd/set/helper.go | 151 +++++++++++ pkg/kubectl/cmd/set/set.go | 3 +- pkg/kubectl/cmd/set/set_image.go | 239 ++++++++++++++++++ pkg/kubectl/cmd/util/factory.go | 28 ++ pkg/kubectl/cmd/util/helpers.go | 68 ++++- 33 files changed, 979 insertions(+), 91 deletions(-) create mode 100644 docs/man/man1/kubectl-set-image.1 create mode 100644 docs/user-guide/kubectl/kubectl_set_image.md create mode 100644 hack/testdata/deployment-multicontainer.yaml create mode 100644 pkg/kubectl/cmd/set/helper.go create mode 100644 pkg/kubectl/cmd/set/set_image.go diff --git a/.generated_docs b/.generated_docs index 680f36075a6..f3c1a4e7d46 100644 --- a/.generated_docs +++ b/.generated_docs @@ -53,6 +53,7 @@ docs/man/man1/kubectl-rollout-undo.1 docs/man/man1/kubectl-rollout.1 docs/man/man1/kubectl-run.1 docs/man/man1/kubectl-scale.1 +docs/man/man1/kubectl-set-image.1 docs/man/man1/kubectl-set.1 docs/man/man1/kubectl-stop.1 docs/man/man1/kubectl-taint.1 @@ -109,6 +110,7 @@ docs/user-guide/kubectl/kubectl_rollout_undo.md docs/user-guide/kubectl/kubectl_run.md docs/user-guide/kubectl/kubectl_scale.md docs/user-guide/kubectl/kubectl_set.md +docs/user-guide/kubectl/kubectl_set_image.md docs/user-guide/kubectl/kubectl_taint.md docs/user-guide/kubectl/kubectl_uncordon.md docs/user-guide/kubectl/kubectl_version.md diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 3bd85301d20..b4470c89cbc 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -478,10 +478,78 @@ _kubectl_get() noun_aliases+=("thirdpartyresources") } +_kubectl_set_image() +{ + last_command="kubectl_set_image" + commands=() + + flags=() + two_word_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--all") + 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+=("--local") + flags+=("--no-headers") + flags+=("--output=") + two_word_flags+=("-o") + flags+=("--output-version=") + flags+=("--record") + flags+=("--recursive") + flags+=("-R") + flags+=("--selector=") + two_word_flags+=("-l") + flags+=("--show-all") + flags+=("-a") + flags+=("--show-labels") + flags+=("--sort-by=") + flags+=("--template=") + flags_with_completion+=("--template") + flags_completion+=("_filedir") + flags+=("--alsologtostderr") + flags+=("--api-version=") + flags+=("--as=") + flags+=("--certificate-authority=") + flags+=("--client-certificate=") + flags+=("--client-key=") + flags+=("--cluster=") + flags+=("--context=") + flags+=("--insecure-skip-tls-verify") + flags+=("--kubeconfig=") + flags+=("--log-backtrace-at=") + flags+=("--log-dir=") + flags+=("--log-flush-frequency=") + flags+=("--logtostderr") + flags+=("--match-server-version") + flags+=("--namespace=") + flags_with_completion+=("--namespace") + flags_completion+=("__kubectl_get_namespaces") + flags+=("--password=") + flags+=("--server=") + two_word_flags+=("-s") + flags+=("--stderrthreshold=") + flags+=("--token=") + flags+=("--user=") + flags+=("--username=") + flags+=("--v=") + flags+=("--vmodule=") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _kubectl_set() { last_command="kubectl_set" commands=() + commands+=("image") flags=() two_word_flags=() diff --git a/docs/man/man1/kubectl-autoscale.1 b/docs/man/man1/kubectl-autoscale.1 index ed56eef8721..4789f636e03 100644 --- a/docs/man/man1/kubectl-autoscale.1 +++ b/docs/man/man1/kubectl-autoscale.1 @@ -27,7 +27,7 @@ An autoscaler can automatically increase or decrease number of pods deployed wit .PP \fB\-\-dry\-run\fP=false - If true, only print the object that would be sent, without creating it. + If true, only print the object that would be sent, without sending it. .PP \fB\-f\fP, \fB\-\-filename\fP=[] diff --git a/docs/man/man1/kubectl-expose.1 b/docs/man/man1/kubectl-expose.1 index afc6104a940..d4e509beb72 100644 --- a/docs/man/man1/kubectl-expose.1 +++ b/docs/man/man1/kubectl-expose.1 @@ -40,7 +40,7 @@ Possible resources include (case insensitive): .PP \fB\-\-dry\-run\fP=false - If true, only print the object that would be sent, without creating it. + If true, only print the object that would be sent, without sending it. .PP \fB\-\-external\-ip\fP="" diff --git a/docs/man/man1/kubectl-rolling-update.1 b/docs/man/man1/kubectl-rolling-update.1 index 9ac6140b280..2f8d3ced180 100644 --- a/docs/man/man1/kubectl-rolling-update.1 +++ b/docs/man/man1/kubectl-rolling-update.1 @@ -32,7 +32,7 @@ existing replication controller and overwrite at least one (common) label in its .PP \fB\-\-dry\-run\fP=false - If true, print out the changes that would be made, but don't actually make them. + If true, only print the object that would be sent, without sending it. .PP \fB\-f\fP, \fB\-\-filename\fP=[] diff --git a/docs/man/man1/kubectl-set-image.1 b/docs/man/man1/kubectl-set-image.1 new file mode 100644 index 00000000000..a48d16d5912 --- /dev/null +++ b/docs/man/man1/kubectl-set-image.1 @@ -0,0 +1,206 @@ +.TH "KUBERNETES" "1" " kubernetes User Manuals" "Eric Paris" "Jan 2015" "" + + +.SH NAME +.PP +kubectl set image \- Update image of a pod template + + +.SH SYNOPSIS +.PP +\fBkubectl set image\fP [OPTIONS] + + +.SH DESCRIPTION +.PP +Update existing container image(s) of resources. + +.PP +Possible resources include (case insensitive): + pod (po), replicationcontroller (rc), deployment, daemonset (ds), job, replicaset (rs) + + +.SH OPTIONS +.PP +\fB\-\-all\fP=false + select all resources in the namespace of the specified resource types + +.PP +\fB\-f\fP, \fB\-\-filename\fP=[] + Filename, directory, or URL to a file identifying the resource to get from a server. + +.PP +\fB\-\-local\fP=false + If true, set image will NOT contact api\-server but run locally. + +.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|wide|name|go\-template=...|go\-template\-file=...|jsonpath=...|jsonpath\-file=... See golang template [ +\[la]http://golang.org/pkg/text/template/#pkg-overview\[ra]] and jsonpath template [ +\[la]http://releases.k8s.io/HEAD/docs/user-guide/jsonpath.md\[ra]]. + +.PP +\fB\-\-output\-version\fP="" + Output the formatted object with the given group version (for ex: 'extensions/v1beta1'). + +.PP +\fB\-\-record\fP=false + Record current kubectl command in the resource annotation. + +.PP +\fB\-R\fP, \fB\-\-recursive\fP=false + If true, process directory recursively. + +.PP +\fB\-l\fP, \fB\-\-selector\fP="" + Selector (label query) to filter on + +.PP +\fB\-a\fP, \fB\-\-show\-all\fP=false + When printing, show all resources (default hide terminated pods.) + +.PP +\fB\-\-show\-labels\fP=false + When printing, show all labels as the last column (default hide labels column) + +.PP +\fB\-\-sort\-by\fP="" + If non\-empty, sort list types using this field specification. The field specification is expressed as a JSONPath expression (e.g. '{.metadata.name}'). The field in the API resource specified by this JSONPath expression must be an integer or a string. + +.PP +\fB\-\-template\fP="" + Template string or path to template file to use when \-o=go\-template, \-o=go\-template\-file. The template format is golang templates [ +\[la]http://golang.org/pkg/text/template/#pkg-overview\[ra]]. + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +.PP +\fB\-\-alsologtostderr\fP=false + log to standard error as well as files + +.PP +\fB\-\-api\-version\fP="" + DEPRECATED: The API version to use when talking to the server + +.PP +\fB\-\-as\fP="" + Username to impersonate for the operation. + +.PP +\fB\-\-certificate\-authority\fP="" + Path to a cert. file for the certificate authority. + +.PP +\fB\-\-client\-certificate\fP="" + Path to a client certificate file for TLS. + +.PP +\fB\-\-client\-key\fP="" + Path to a client key file for TLS. + +.PP +\fB\-\-cluster\fP="" + The name of the kubeconfig cluster to use + +.PP +\fB\-\-context\fP="" + The name of the kubeconfig context to use + +.PP +\fB\-\-insecure\-skip\-tls\-verify\fP=false + If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure. + +.PP +\fB\-\-kubeconfig\fP="" + Path to the kubeconfig file to use for CLI requests. + +.PP +\fB\-\-log\-backtrace\-at\fP=:0 + when logging hits line file:N, emit a stack trace + +.PP +\fB\-\-log\-dir\fP="" + If non\-empty, write log files in this directory + +.PP +\fB\-\-log\-flush\-frequency\fP=5s + Maximum number of seconds between log flushes + +.PP +\fB\-\-logtostderr\fP=true + log to standard error instead of files + +.PP +\fB\-\-match\-server\-version\fP=false + Require server version to match client version + +.PP +\fB\-\-namespace\fP="" + If present, the namespace scope for this CLI request. + +.PP +\fB\-\-password\fP="" + Password for basic authentication to the API server. + +.PP +\fB\-s\fP, \fB\-\-server\fP="" + The address and port of the Kubernetes API server + +.PP +\fB\-\-stderrthreshold\fP=2 + logs at or above this threshold go to stderr + +.PP +\fB\-\-token\fP="" + Bearer token for authentication to the API server. + +.PP +\fB\-\-user\fP="" + The name of the kubeconfig user to use + +.PP +\fB\-\-username\fP="" + Username for basic authentication to the API server. + +.PP +\fB\-\-v\fP=0 + log level for V logs + +.PP +\fB\-\-vmodule\fP= + comma\-separated list of pattern=N settings for file\-filtered logging + + +.SH EXAMPLE +.PP +.RS + +.nf +# Set a deployment's nginx container image to 'nginx:1.9.1', and its busybox container image to 'busybox'. +kubectl set image deployment/nginx busybox=busybox nginx=nginx:1.9.1 + +# Update all deployments' and rc's nginx container's image to 'nginx:1.9.1' +kubectl set image deployments,rc nginx=nginx:1.9.1 \-\-all + +# Update image of all containers of daemonset abc to 'nginx:1.9.1' +kubectl set image daemonset abc *=nginx:1.9.1 + +# Print result (in yaml format) of updating nginx container image from local file, without hitting the server +kubectl set image \-f path/to/file.yaml nginx=nginx:1.9.1 \-\-local \-o yaml + +.fi +.RE + + +.SH SEE ALSO +.PP +\fBkubectl\-set(1)\fP, + + +.SH HISTORY +.PP +January 2015, Originally compiled by Eric Paris (eparis at redhat dot com) based on the kubernetes source material, but hopefully they have been automatically generated since! diff --git a/docs/man/man1/kubectl-set.1 b/docs/man/man1/kubectl-set.1 index bb072a87f0a..b89dfbd76f3 100644 --- a/docs/man/man1/kubectl-set.1 +++ b/docs/man/man1/kubectl-set.1 @@ -119,7 +119,7 @@ These commands help you make changes to existing application resources. .SH SEE ALSO .PP -\fBkubectl(1)\fP, +\fBkubectl(1)\fP, \fBkubectl\-set\-image(1)\fP, .SH HISTORY diff --git a/docs/user-guide/kubectl/kubectl_autoscale.md b/docs/user-guide/kubectl/kubectl_autoscale.md index cd79b5d5205..43f0f0da97d 100644 --- a/docs/user-guide/kubectl/kubectl_autoscale.md +++ b/docs/user-guide/kubectl/kubectl_autoscale.md @@ -62,7 +62,7 @@ kubectl autoscale rc foo --max=5 --cpu-percent=80 ``` --cpu-percent=-1: The target average CPU utilization (represented as a percent of requested CPU) over all the pods. If it's not specified or negative, the server will apply a default value. - --dry-run[=false]: If true, only print the object that would be sent, without creating it. + --dry-run[=false]: If true, only print the object that would be sent, without sending it. -f, --filename=[]: Filename, directory, or URL to a file identifying the resource to autoscale. --generator="horizontalpodautoscaler/v1beta1": The name of the API generator to use. Currently there is only 1 generator. --include-extended-apis[=true]: If true, include definitions of new APIs via calls to the API server. [default true] @@ -113,7 +113,7 @@ kubectl autoscale rc foo --max=5 --cpu-percent=80 * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra on 30-Mar-2016 +###### Auto generated by spf13/cobra on 13-May-2016 [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_autoscale.md?pixel)]() diff --git a/docs/user-guide/kubectl/kubectl_expose.md b/docs/user-guide/kubectl/kubectl_expose.md index 5e391d5ebcc..503ce447e20 100644 --- a/docs/user-guide/kubectl/kubectl_expose.md +++ b/docs/user-guide/kubectl/kubectl_expose.md @@ -85,7 +85,7 @@ kubectl expose deployment nginx --port=80 --target-port=8000 ### Options ``` - --dry-run[=false]: If true, only print the object that would be sent, without creating it. + --dry-run[=false]: If true, only print the object that would be sent, without sending it. --external-ip="": Additional external IP address (not managed by Kubernetes) to accept for the service. If this IP is routed to a node, the service can be accessed by this IP in addition to its generated service IP. -f, --filename=[]: Filename, directory, or URL to a file identifying the resource to expose a service --generator="service/v2": The name of the API generator to use. There are 2 generators: 'service/v1' and 'service/v2'. The only difference between them is that service port in v1 is named 'default', while it is left unnamed in v2. Default is 'service/v2'. @@ -143,7 +143,7 @@ kubectl expose deployment nginx --port=80 --target-port=8000 * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra on 11-May-2016 +###### Auto generated by spf13/cobra on 13-May-2016 [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_expose.md?pixel)]() diff --git a/docs/user-guide/kubectl/kubectl_rolling-update.md b/docs/user-guide/kubectl/kubectl_rolling-update.md index 172a30d59cb..f7c1d34cda4 100644 --- a/docs/user-guide/kubectl/kubectl_rolling-update.md +++ b/docs/user-guide/kubectl/kubectl_rolling-update.md @@ -75,7 +75,7 @@ kubectl rolling-update frontend-v1 frontend-v2 --rollback ``` --container="": Container name which will have its image upgraded. Only relevant when --image is specified, ignored otherwise. Required when using --image on a multi-container pod --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. + --dry-run[=false]: If true, only print the object that would be sent, without sending it. -f, --filename=[]: Filename or URL to file to use to create the new replication controller. --image="": Image to use for upgrading the replication controller. Must be distinct from the existing image (either new image or new image tag). Can not be used with --filename/-f --image-pull-policy="": Explicit policy for when to pull container images. Required when --image is same as existing image, ignored otherwise. @@ -127,7 +127,7 @@ kubectl rolling-update frontend-v1 frontend-v2 --rollback * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra on 21-Apr-2016 +###### Auto generated by spf13/cobra on 13-May-2016 [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_rolling-update.md?pixel)]() diff --git a/docs/user-guide/kubectl/kubectl_set.md b/docs/user-guide/kubectl/kubectl_set.md index 4ee90dfc69f..f72666226bb 100644 --- a/docs/user-guide/kubectl/kubectl_set.md +++ b/docs/user-guide/kubectl/kubectl_set.md @@ -73,6 +73,7 @@ kubectl set SUBCOMMAND ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager +* [kubectl set image](kubectl_set_image.md) - Update image of a pod template ###### Auto generated by spf13/cobra on 10-May-2016 diff --git a/docs/user-guide/kubectl/kubectl_set_image.md b/docs/user-guide/kubectl/kubectl_set_image.md new file mode 100644 index 00000000000..f8c4344e0d1 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_set_image.md @@ -0,0 +1,116 @@ + + + + +WARNING +WARNING +WARNING +WARNING +WARNING + +

PLEASE NOTE: This document applies to the HEAD of the source tree

+ +If you are using a released version of Kubernetes, you should +refer to the docs that go with that version. + +Documentation for other releases can be found at +[releases.k8s.io](http://releases.k8s.io). + +-- + + + + + +## kubectl set image + +Update image of a pod template + +### Synopsis + + +Update existing container image(s) of resources. + +Possible resources include (case insensitive): + pod (po), replicationcontroller (rc), deployment, daemonset (ds), job, replicaset (rs) + +``` +kubectl set image (-f FILENAME | TYPE NAME) CONTAINER_NAME_1=CONTAINER_IMAGE_1 ... CONTAINER_NAME_N=CONTAINER_IMAGE_N +``` + +### Examples + +``` +# Set a deployment's nginx container image to 'nginx:1.9.1', and its busybox container image to 'busybox'. +kubectl set image deployment/nginx busybox=busybox nginx=nginx:1.9.1 + +# Update all deployments' and rc's nginx container's image to 'nginx:1.9.1' +kubectl set image deployments,rc nginx=nginx:1.9.1 --all + +# Update image of all containers of daemonset abc to 'nginx:1.9.1' +kubectl set image daemonset abc *=nginx:1.9.1 + +# Print result (in yaml format) of updating nginx container image from local file, without hitting the server +kubectl set image -f path/to/file.yaml nginx=nginx:1.9.1 --local -o yaml +``` + +### Options + +``` + --all[=false]: select all resources in the namespace of the specified resource types + -f, --filename=[]: Filename, directory, or URL to a file identifying the resource to get from a server. + --local[=false]: If true, set image will NOT contact api-server but run locally. + --no-headers[=false]: When using the default output, don't print headers. + -o, --output="": Output format. One of: json|yaml|wide|name|go-template=...|go-template-file=...|jsonpath=...|jsonpath-file=... See golang template [http://golang.org/pkg/text/template/#pkg-overview] and jsonpath template [http://releases.k8s.io/HEAD/docs/user-guide/jsonpath.md]. + --output-version="": Output the formatted object with the given group version (for ex: 'extensions/v1beta1'). + --record[=false]: Record current kubectl command in the resource annotation. + -R, --recursive[=false]: If true, process directory recursively. + -l, --selector="": Selector (label query) to filter on + -a, --show-all[=false]: When printing, show all resources (default hide terminated pods.) + --show-labels[=false]: When printing, show all labels as the last column (default hide labels column) + --sort-by="": If non-empty, sort list types using this field specification. The field specification is expressed as a JSONPath expression (e.g. '{.metadata.name}'). The field in the API resource specified by this JSONPath expression must be an integer or a string. + --template="": Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]. +``` + +### Options inherited from parent commands + +``` + --alsologtostderr[=false]: log to standard error as well as files + --as="": Username to impersonate for the operation. + --certificate-authority="": Path to a cert. file for the certificate authority. + --client-certificate="": Path to a client certificate file for TLS. + --client-key="": Path to a client key file for TLS. + --cluster="": The name of the kubeconfig cluster to use + --context="": The name of the kubeconfig context to use + --insecure-skip-tls-verify[=false]: If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure. + --kubeconfig="": Path to the kubeconfig file to use for CLI requests. + --log-backtrace-at=:0: when logging hits line file:N, emit a stack trace + --log-dir="": If non-empty, write log files in this directory + --log-flush-frequency=5s: Maximum number of seconds between log flushes + --logtostderr[=true]: log to standard error instead of files + --match-server-version[=false]: Require server version to match client version + --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. + -s, --server="": The address and port of the Kubernetes API server + --stderrthreshold=2: logs at or above this threshold go to stderr + --token="": Bearer token for authentication to the API server. + --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. + --v=0: log level for V logs + --vmodule=: comma-separated list of pattern=N settings for file-filtered logging +``` + +### SEE ALSO + +* [kubectl set](kubectl_set.md) - Set specific features on objects + +###### Auto generated by spf13/cobra on 17-May-2016 + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_set_image.md?pixel)]() + diff --git a/docs/yaml/kubectl/kubectl_autoscale.yaml b/docs/yaml/kubectl/kubectl_autoscale.yaml index 722d60ba9c4..22af59e2d01 100644 --- a/docs/yaml/kubectl/kubectl_autoscale.yaml +++ b/docs/yaml/kubectl/kubectl_autoscale.yaml @@ -14,7 +14,7 @@ options: - name: dry-run default_value: "false" usage: | - If true, only print the object that would be sent, without creating it. + If true, only print the object that would be sent, without sending it. - name: filename shorthand: f default_value: '[]' diff --git a/docs/yaml/kubectl/kubectl_expose.yaml b/docs/yaml/kubectl/kubectl_expose.yaml index 608d1fdec3d..e9525b00a5c 100644 --- a/docs/yaml/kubectl/kubectl_expose.yaml +++ b/docs/yaml/kubectl/kubectl_expose.yaml @@ -21,7 +21,7 @@ options: - name: dry-run default_value: "false" usage: | - If true, only print the object that would be sent, without creating it. + If true, only print the object that would be sent, without sending it. - name: external-ip usage: | Additional external IP address (not managed by Kubernetes) to accept for the service. If this IP is routed to a node, the service can be accessed by this IP in addition to its generated service IP. diff --git a/docs/yaml/kubectl/kubectl_rolling-update.yaml b/docs/yaml/kubectl/kubectl_rolling-update.yaml index 28d2bfa9d5b..b30ebfbfe71 100644 --- a/docs/yaml/kubectl/kubectl_rolling-update.yaml +++ b/docs/yaml/kubectl/kubectl_rolling-update.yaml @@ -17,7 +17,7 @@ options: - name: dry-run default_value: "false" usage: | - If true, print out the changes that would be made, but don't actually make them. + If true, only print the object that would be sent, without sending it. - name: filename shorthand: f default_value: '[]' diff --git a/docs/yaml/kubectl/kubectl_set.yaml b/docs/yaml/kubectl/kubectl_set.yaml index 26c4afd3585..555ba331212 100644 --- a/docs/yaml/kubectl/kubectl_set.yaml +++ b/docs/yaml/kubectl/kubectl_set.yaml @@ -65,3 +65,4 @@ inherited_options: comma-separated list of pattern=N settings for file-filtered logging see_also: - kubectl +- image diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index aefb998bc6c..1fda2229c54 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -291,6 +291,7 @@ runTests() { secret_data=".data" secret_type=".type" deployment_image_field="(index .spec.template.spec.containers 0).image" + deployment_second_image_field="(index .spec.template.spec.containers 1).image" change_cause_annotation='.*kubernetes.io/change-cause.*' # Passing no arguments to create is an error @@ -1620,6 +1621,11 @@ __EOF__ # Clean up kubectl delete rc frontend "${kube_flags[@]}" + + ###################### + # Deployments # + ###################### + ### Auto scale deployment # Pre-condition: no deployment exists kube::test::get_object_assert deployment "{{range.items}}{{$id_field}}:{{end}}" '' @@ -1670,6 +1676,39 @@ __EOF__ # Clean up kubectl delete deployment nginx-deployment "${kube_flags[@]}" + ### Set image of a deployment + # Pre-condition: no deployment exists + kube::test::get_object_assert deployment "{{range.items}}{{$id_field}}:{{end}}" '' + # Create a deployment + kubectl create -f hack/testdata/deployment-multicontainer.yaml "${kube_flags[@]}" + kube::test::get_object_assert deployment "{{range.items}}{{$id_field}}:{{end}}" 'nginx-deployment:' + kube::test::get_object_assert deployment "{{range.items}}{{$deployment_image_field}}:{{end}}" "${IMAGE_DEPLOYMENT_R1}:" + kube::test::get_object_assert deployment "{{range.items}}{{$deployment_second_image_field}}:{{end}}" "${IMAGE_PERL}:" + # Set the deployment's image + kubectl set image deployment nginx-deployment nginx="${IMAGE_DEPLOYMENT_R2}" "${kube_flags[@]}" + kube::test::get_object_assert deployment "{{range.items}}{{$deployment_image_field}}:{{end}}" "${IMAGE_DEPLOYMENT_R2}:" + kube::test::get_object_assert deployment "{{range.items}}{{$deployment_second_image_field}}:{{end}}" "${IMAGE_PERL}:" + # Set non-existing container should fail + ! kubectl set image deployment nginx-deployment redis=redis "${kube_flags[@]}" + # Set image of deployments without specifying name + kubectl set image deployments --all nginx="${IMAGE_DEPLOYMENT_R1}" "${kube_flags[@]}" + kube::test::get_object_assert deployment "{{range.items}}{{$deployment_image_field}}:{{end}}" "${IMAGE_DEPLOYMENT_R1}:" + kube::test::get_object_assert deployment "{{range.items}}{{$deployment_second_image_field}}:{{end}}" "${IMAGE_PERL}:" + # Set image of a deployment specified by file + kubectl set image -f hack/testdata/deployment-multicontainer.yaml nginx="${IMAGE_DEPLOYMENT_R2}" "${kube_flags[@]}" + kube::test::get_object_assert deployment "{{range.items}}{{$deployment_image_field}}:{{end}}" "${IMAGE_DEPLOYMENT_R2}:" + kube::test::get_object_assert deployment "{{range.items}}{{$deployment_second_image_field}}:{{end}}" "${IMAGE_PERL}:" + # Set image of a local file without talking to the server + kubectl set image -f hack/testdata/deployment-multicontainer.yaml nginx="${IMAGE_DEPLOYMENT_R1}" "${kube_flags[@]}" --local -o yaml + kube::test::get_object_assert deployment "{{range.items}}{{$deployment_image_field}}:{{end}}" "${IMAGE_DEPLOYMENT_R2}:" + kube::test::get_object_assert deployment "{{range.items}}{{$deployment_second_image_field}}:{{end}}" "${IMAGE_PERL}:" + # Set image of all containers of the deployment + kubectl set image deployment nginx-deployment "*"="${IMAGE_DEPLOYMENT_R1}" "${kube_flags[@]}" + kube::test::get_object_assert deployment "{{range.items}}{{$deployment_image_field}}:{{end}}" "${IMAGE_DEPLOYMENT_R1}:" + kube::test::get_object_assert deployment "{{range.items}}{{$deployment_second_image_field}}:{{end}}" "${IMAGE_DEPLOYMENT_R1}:" + # Clean up + kubectl delete deployment nginx-deployment "${kube_flags[@]}" + ###################### # Replica Sets # diff --git a/hack/testdata/deployment-multicontainer.yaml b/hack/testdata/deployment-multicontainer.yaml new file mode 100644 index 00000000000..115888a0290 --- /dev/null +++ b/hack/testdata/deployment-multicontainer.yaml @@ -0,0 +1,23 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + name: nginx-deployment +spec: + replicas: 3 + selector: + matchLabels: + name: nginx + template: + metadata: + labels: + name: nginx + spec: + containers: + - name: nginx + image: gcr.io/google-containers/nginx:test-cmd + ports: + - containerPort: 80 + - name: perl + image: gcr.io/google-containers/perl diff --git a/pkg/kubectl/cmd/annotate.go b/pkg/kubectl/cmd/annotate.go index bb0266ddfed..b2e4e3051d4 100644 --- a/pkg/kubectl/cmd/annotate.go +++ b/pkg/kubectl/cmd/annotate.go @@ -148,22 +148,11 @@ func (o *AnnotateOptions) Complete(f *cmdutil.Factory, out io.Writer, cmd *cobra // retrieves resource and annotation args from args // also checks args to verify that all resources are specified before annotations - annotationArgs := []string{} - metAnnotaionArg := false - for _, s := range args { - isAnnotation := strings.Contains(s, "=") || strings.HasSuffix(s, "-") - switch { - case !metAnnotaionArg && isAnnotation: - metAnnotaionArg = true - fallthrough - case metAnnotaionArg && isAnnotation: - annotationArgs = append(annotationArgs, s) - case !metAnnotaionArg && !isAnnotation: - o.resources = append(o.resources, s) - case metAnnotaionArg && !isAnnotation: - return fmt.Errorf("all resources must be specified before annotation changes: %s", s) - } + resources, annotationArgs, err := cmdutil.GetResourcesAndPairs(args, "annotation") + if err != nil { + return err } + o.resources = resources if len(o.resources) < 1 && len(o.filenames) == 0 { return fmt.Errorf("one or more resources must be specified as or /") } @@ -277,34 +266,7 @@ func (o AnnotateOptions) RunAnnotate() error { // parseAnnotations retrieves new and remove annotations from annotation args func parseAnnotations(annotationArgs []string) (map[string]string, []string, error) { - var invalidBuf bytes.Buffer - newAnnotations := map[string]string{} - removeAnnotations := []string{} - for _, annotationArg := range annotationArgs { - if strings.Index(annotationArg, "=") != -1 { - parts := strings.SplitN(annotationArg, "=", 2) - if len(parts) != 2 || len(parts[1]) == 0 { - if invalidBuf.Len() > 0 { - invalidBuf.WriteString(", ") - } - invalidBuf.WriteString(fmt.Sprintf(annotationArg)) - } else { - newAnnotations[parts[0]] = parts[1] - } - } else if strings.HasSuffix(annotationArg, "-") { - removeAnnotations = append(removeAnnotations, annotationArg[:len(annotationArg)-1]) - } else { - if invalidBuf.Len() > 0 { - invalidBuf.WriteString(", ") - } - invalidBuf.WriteString(fmt.Sprintf(annotationArg)) - } - } - if invalidBuf.Len() > 0 { - return newAnnotations, removeAnnotations, fmt.Errorf("invalid annotation format: %s", invalidBuf.String()) - } - - return newAnnotations, removeAnnotations, nil + return cmdutil.ParsePairs(annotationArgs, "annotation", true) } // validateAnnotations checks the format of annotation args and checks removed annotations aren't in the new annotations map diff --git a/pkg/kubectl/cmd/autoscale.go b/pkg/kubectl/cmd/autoscale.go index cb40c6def33..75bf13daef0 100644 --- a/pkg/kubectl/cmd/autoscale.go +++ b/pkg/kubectl/cmd/autoscale.go @@ -68,7 +68,7 @@ func NewCmdAutoscale(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd.MarkFlagRequired("max") cmd.Flags().Int("cpu-percent", -1, fmt.Sprintf("The target average CPU utilization (represented as a percent of requested CPU) over all the pods. If it's not specified or negative, the server will apply a default value.")) cmd.Flags().String("name", "", "The name for the newly created object. If not specified, the name of the input resource will be used.") - cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without creating it.") + cmdutil.AddDryRunFlag(cmd) usage := "Filename, directory, or URL to a file identifying the resource to autoscale." kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage) cmdutil.AddRecursiveFlag(cmd, &options.Recursive) @@ -160,8 +160,7 @@ func RunAutoscale(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args [] } object = hpa.Object } - // TODO: extract this flag to a central location, when such a location exists. - if cmdutil.GetFlagBool(cmd, "dry-run") { + if cmdutil.GetDryRunFlag(cmd) { return f.PrintObject(cmd, mapper, object, out) } diff --git a/pkg/kubectl/cmd/create_configmap.go b/pkg/kubectl/cmd/create_configmap.go index 6af07920583..a81e4c19665 100644 --- a/pkg/kubectl/cmd/create_configmap.go +++ b/pkg/kubectl/cmd/create_configmap.go @@ -90,7 +90,7 @@ func CreateConfigMap(f *cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Command, a return RunCreateSubcommand(f, cmd, cmdOut, &CreateSubcommandOptions{ Name: name, StructuredGenerator: generator, - DryRun: cmdutil.GetFlagBool(cmd, "dry-run"), + DryRun: cmdutil.GetDryRunFlag(cmd), OutputFormat: cmdutil.GetFlagString(cmd, "output"), }) } diff --git a/pkg/kubectl/cmd/create_namespace.go b/pkg/kubectl/cmd/create_namespace.go index 299c5e9408c..49da9518f6e 100644 --- a/pkg/kubectl/cmd/create_namespace.go +++ b/pkg/kubectl/cmd/create_namespace.go @@ -71,7 +71,7 @@ func CreateNamespace(f *cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Command, a return RunCreateSubcommand(f, cmd, cmdOut, &CreateSubcommandOptions{ Name: name, StructuredGenerator: generator, - DryRun: cmdutil.GetFlagBool(cmd, "dry-run"), + DryRun: cmdutil.GetDryRunFlag(cmd), OutputFormat: cmdutil.GetFlagString(cmd, "output"), }) } diff --git a/pkg/kubectl/cmd/create_secret.go b/pkg/kubectl/cmd/create_secret.go index 48cb8f99b08..ed4540db679 100644 --- a/pkg/kubectl/cmd/create_secret.go +++ b/pkg/kubectl/cmd/create_secret.go @@ -109,7 +109,7 @@ func CreateSecretGeneric(f *cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Comman return RunCreateSubcommand(f, cmd, cmdOut, &CreateSubcommandOptions{ Name: name, StructuredGenerator: generator, - DryRun: cmdutil.GetFlagBool(cmd, "dry-run"), + DryRun: cmdutil.GetDryRunFlag(cmd), OutputFormat: cmdutil.GetFlagString(cmd, "output"), }) } @@ -188,7 +188,7 @@ func CreateSecretDockerRegistry(f *cmdutil.Factory, cmdOut io.Writer, cmd *cobra return RunCreateSubcommand(f, cmd, cmdOut, &CreateSubcommandOptions{ Name: name, StructuredGenerator: generator, - DryRun: cmdutil.GetFlagBool(cmd, "dry-run"), + DryRun: cmdutil.GetDryRunFlag(cmd), OutputFormat: cmdutil.GetFlagString(cmd, "output"), }) } diff --git a/pkg/kubectl/cmd/create_serviceaccount.go b/pkg/kubectl/cmd/create_serviceaccount.go index 61d2d3cc516..89d1882c0b1 100644 --- a/pkg/kubectl/cmd/create_serviceaccount.go +++ b/pkg/kubectl/cmd/create_serviceaccount.go @@ -71,7 +71,7 @@ func CreateServiceAccount(f *cmdutil.Factory, cmdOut io.Writer, cmd *cobra.Comma return RunCreateSubcommand(f, cmd, cmdOut, &CreateSubcommandOptions{ Name: name, StructuredGenerator: generator, - DryRun: cmdutil.GetFlagBool(cmd, "dry-run"), + DryRun: cmdutil.GetDryRunFlag(cmd), OutputFormat: cmdutil.GetFlagString(cmd, "output"), }) } diff --git a/pkg/kubectl/cmd/expose.go b/pkg/kubectl/cmd/expose.go index 82251e541c7..7ed7529c34b 100644 --- a/pkg/kubectl/cmd/expose.go +++ b/pkg/kubectl/cmd/expose.go @@ -110,7 +110,6 @@ func NewCmdExposeService(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd.Flags().String("load-balancer-ip", "", "IP to assign to to the Load Balancer. If empty, an ephemeral IP will be created and used (cloud-provider specific).") cmd.Flags().String("selector", "", "A label selector to use for this service. Only equality-based selector requirements are supported. If empty (the default) infer the selector from the replication controller or replica set.") cmd.Flags().StringP("labels", "l", "", "Labels to apply to the service created by this call.") - cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without creating it.") cmd.Flags().String("container-port", "", "Synonym for --target-port") cmd.Flags().MarkDeprecated("container-port", "--container-port will be removed in the future, please use --target-port instead") cmd.Flags().String("target-port", "", "Name or number for the port on the container that the service should direct traffic to. Optional.") @@ -121,6 +120,7 @@ func NewCmdExposeService(f *cmdutil.Factory, out io.Writer) *cobra.Command { usage := "Filename, directory, or URL to a file identifying the resource to expose a service" kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage) + cmdutil.AddDryRunFlag(cmd) cmdutil.AddRecursiveFlag(cmd, &options.Recursive) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddRecordFlag(cmd) @@ -256,8 +256,7 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str } } info.Refresh(object, true) - // TODO: extract this flag to a central location, when such a location exists. - if cmdutil.GetFlagBool(cmd, "dry-run") { + if cmdutil.GetDryRunFlag(cmd) { return f.PrintObject(cmd, mapper, object, out) } if err := kubectl.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), info, f.JSONEncoder()); err != nil { diff --git a/pkg/kubectl/cmd/label.go b/pkg/kubectl/cmd/label.go index 97f18268112..8d87b913963 100644 --- a/pkg/kubectl/cmd/label.go +++ b/pkg/kubectl/cmd/label.go @@ -101,7 +101,7 @@ func NewCmdLabel(f *cmdutil.Factory, out io.Writer) *cobra.Command { usage := "Filename, directory, or URL to a file identifying the resource to update the labels" kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage) cmdutil.AddRecursiveFlag(cmd, &options.Recursive) - cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without sending it.") + cmdutil.AddDryRunFlag(cmd) cmdutil.AddRecordFlag(cmd) cmdutil.AddInclude3rdPartyFlags(cmd) @@ -176,21 +176,9 @@ func labelFunc(obj runtime.Object, overwrite bool, resourceVersion string, label } func RunLabel(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string, options *LabelOptions) error { - resources, labelArgs := []string{}, []string{} - first := true - for _, s := range args { - isLabel := strings.Contains(s, "=") || strings.HasSuffix(s, "-") - switch { - case first && isLabel: - first = false - fallthrough - case !first && isLabel: - labelArgs = append(labelArgs, s) - case first && !isLabel: - resources = append(resources, s) - case !first && !isLabel: - return cmdutil.UsageError(cmd, "all resources must be specified before label changes: %s", s) - } + resources, labelArgs, err := cmdutil.GetResourcesAndPairs(args, "label") + if err != nil { + return err } if len(resources) < 1 && len(options.Filenames) == 0 { return cmdutil.UsageError(cmd, "one or more resources must be specified as or /") @@ -242,7 +230,7 @@ func RunLabel(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []stri var outputObj runtime.Object dataChangeMsg := "not labeled" - if cmdutil.GetFlagBool(cmd, "dry-run") { + if cmdutil.GetDryRunFlag(cmd) { err = labelFunc(info.Object, overwrite, resourceVersion, lbls, remove) if err != nil { return err diff --git a/pkg/kubectl/cmd/rollingupdate.go b/pkg/kubectl/cmd/rollingupdate.go index 8f745e06d86..1170aea7d63 100644 --- a/pkg/kubectl/cmd/rollingupdate.go +++ b/pkg/kubectl/cmd/rollingupdate.go @@ -98,8 +98,8 @@ func NewCmdRollingUpdate(f *cmdutil.Factory, out io.Writer) *cobra.Command { 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().String("container", "", "Container name which will have its image upgraded. Only relevant when --image is specified, ignored otherwise. Required when using --image on a multi-container pod") cmd.Flags().String("image-pull-policy", "", "Explicit policy for when to pull container images. Required when --image is same as existing image, ignored otherwise.") - cmd.Flags().Bool("dry-run", false, "If true, print out the changes that would be made, but don't actually make them.") cmd.Flags().Bool("rollback", false, "If true, this is a request to abort an existing rollout that is partially rolled out. It effectively reverses current and next and runs a rollout") + cmdutil.AddDryRunFlag(cmd) cmdutil.AddValidateFlags(cmd) cmdutil.AddPrinterFlags(cmd) cmdutil.AddInclude3rdPartyFlags(cmd) @@ -157,7 +157,7 @@ func RunRollingUpdate(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, arg period := cmdutil.GetFlagDuration(cmd, "update-period") interval := cmdutil.GetFlagDuration(cmd, "poll-interval") timeout := cmdutil.GetFlagDuration(cmd, "timeout") - dryrun := cmdutil.GetFlagBool(cmd, "dry-run") + dryrun := cmdutil.GetDryRunFlag(cmd) outputFormat := cmdutil.GetFlagString(cmd, "output") container := cmdutil.GetFlagString(cmd, "container") diff --git a/pkg/kubectl/cmd/run.go b/pkg/kubectl/cmd/run.go index d1b8e4f2dd5..236e758dce7 100644 --- a/pkg/kubectl/cmd/run.go +++ b/pkg/kubectl/cmd/run.go @@ -92,12 +92,12 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c } func addRunFlags(cmd *cobra.Command) { + cmdutil.AddDryRunFlag(cmd) cmd.Flags().String("generator", "", "The name of the API generator to use. Default is 'deployment/v1beta1' if --restart=Always, otherwise the default is 'job/v1'. This will happen only for cluster version at least 1.2, for olders we will fallback to 'run/v1' for --restart=Always, 'run-pod/v1' for others.") cmd.Flags().String("image", "", "The image for the container to run.") cmd.MarkFlagRequired("image") cmd.Flags().IntP("replicas", "r", 1, "Number of replicas to create for this container. Default is 1.") cmd.Flags().Bool("rm", false, "If true, delete resources created in this command for attached containers.") - cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without sending it.") cmd.Flags().String("overrides", "", "An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field.") cmd.Flags().StringSlice("env", []string{}, "Environment variables to set in the container") cmd.Flags().Int("port", -1, "The port that this container exposes. If --expose is true, this is also the port used by the service that is created.") @@ -474,8 +474,7 @@ func createGeneratedObject(f *cmdutil.Factory, cmd *cobra.Command, generator kub return nil, "", nil, nil, err } } - // TODO: extract this flag to a central location, when such a location exists. - if !cmdutil.GetFlagBool(cmd, "dry-run") { + if !cmdutil.GetDryRunFlag(cmd) { resourceMapper := &resource.Mapper{ ObjectTyper: typer, RESTMapper: mapper, diff --git a/pkg/kubectl/cmd/set/helper.go b/pkg/kubectl/cmd/set/helper.go new file mode 100644 index 00000000000..7a3a04a3d1f --- /dev/null +++ b/pkg/kubectl/cmd/set/helper.go @@ -0,0 +1,151 @@ +/* +Copyright 2016 The Kubernetes Authors 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 set + +import ( + "fmt" + "io" + "strings" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/strategicpatch" +) + +// selectContainers allows one or more containers to be matched against a string or wildcard +func selectContainers(containers []api.Container, spec string) ([]*api.Container, []*api.Container) { + out := []*api.Container{} + skipped := []*api.Container{} + for i, c := range containers { + if selectString(c.Name, spec) { + out = append(out, &containers[i]) + } else { + skipped = append(skipped, &containers[i]) + } + } + return out, skipped +} + +// handlePodUpdateError prints a more useful error to the end user when mutating a pod. +func handlePodUpdateError(out io.Writer, err error, resource string) { + if statusError, ok := err.(*errors.StatusError); ok && errors.IsInvalid(err) { + errorDetails := statusError.Status().Details + if errorDetails.Kind == "Pod" { + all, match := true, false + for _, cause := range errorDetails.Causes { + if cause.Field == "spec" && strings.Contains(cause.Message, "may not update fields other than") { + fmt.Fprintf(out, "error: may not update %s in pod %q directly\n", resource, errorDetails.Name) + match = true + } else { + all = false + } + } + if all && match { + return + } + } + } + + fmt.Fprintf(out, "error: %v\n", err) +} + +// selectString returns true if the provided string matches spec, where spec is a string with +// a non-greedy '*' wildcard operator. +// TODO: turn into a regex and handle greedy matches and backtracking. +func selectString(s, spec string) bool { + if spec == "*" { + return true + } + if !strings.Contains(spec, "*") { + return s == spec + } + + pos := 0 + match := true + parts := strings.Split(spec, "*") + for i, part := range parts { + if len(part) == 0 { + continue + } + next := strings.Index(s[pos:], part) + switch { + // next part not in string + case next < pos: + fallthrough + // first part does not match start of string + case i == 0 && pos != 0: + fallthrough + // last part does not exactly match remaining part of string + case i == (len(parts)-1) && len(s) != (len(part)+next): + match = false + break + default: + pos = next + } + } + return match +} + +// Patch represents the result of a mutation to an object. +type Patch struct { + Info *resource.Info + Err error + + Before []byte + After []byte + Patch []byte +} + +// CalculatePatches calls the mutation function on each provided info object, and generates a strategic merge patch for +// the changes in the object. Encoder must be able to encode the info into the appropriate destination type. If mutateFn +// returns false, the object is not included in the final list of patches. +func CalculatePatches(infos []*resource.Info, encoder runtime.Encoder, mutateFn func(*resource.Info) (bool, error)) []*Patch { + var patches []*Patch + for _, info := range infos { + patch := &Patch{Info: info} + patch.Before, patch.Err = runtime.Encode(encoder, info.Object) + + ok, err := mutateFn(info) + if !ok { + continue + } + if err != nil { + patch.Err = err + } + patches = append(patches, patch) + if patch.Err != nil { + continue + } + + patch.After, patch.Err = runtime.Encode(encoder, info.Object) + if patch.Err != nil { + continue + } + + // TODO: should be via New + versioned, err := info.Mapping.ConvertToVersion(info.Object, info.Mapping.GroupVersionKind.GroupVersion()) + if err != nil { + patch.Err = err + continue + } + + patch.Patch, patch.Err = strategicpatch.CreateTwoWayMergePatch(patch.Before, patch.After, versioned) + } + return patches +} diff --git a/pkg/kubectl/cmd/set/set.go b/pkg/kubectl/cmd/set/set.go index 09a0956aa0b..10d3d379013 100644 --- a/pkg/kubectl/cmd/set/set.go +++ b/pkg/kubectl/cmd/set/set.go @@ -42,7 +42,8 @@ func NewCmdSet(f *cmdutil.Factory, out io.Writer) *cobra.Command { }, } - // TODO: add subcommands + // add subcommands + cmd.AddCommand(NewCmdImage(f, out)) return cmd } diff --git a/pkg/kubectl/cmd/set/set_image.go b/pkg/kubectl/cmd/set/set_image.go new file mode 100644 index 00000000000..afe65adf4d5 --- /dev/null +++ b/pkg/kubectl/cmd/set/set_image.go @@ -0,0 +1,239 @@ +/* +Copyright 2016 The Kubernetes Authors 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 set + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/meta" + "k8s.io/kubernetes/pkg/kubectl" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/runtime" + utilerrors "k8s.io/kubernetes/pkg/util/errors" +) + +// ImageOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of +// referencing the cmd.Flags() +type ImageOptions struct { + Mapper meta.RESTMapper + Typer runtime.ObjectTyper + Infos []*resource.Info + Encoder runtime.Encoder + Selector string + Out io.Writer + Err io.Writer + Filenames []string + Recursive bool + ShortOutput bool + All bool + Record bool + ChangeCause string + Local bool + Cmd *cobra.Command + + PrintObject func(cmd *cobra.Command, mapper meta.RESTMapper, obj runtime.Object, out io.Writer) error + UpdatePodSpecForObject func(obj runtime.Object, fn func(*api.PodSpec) error) (bool, error) + Resources []string + ContainerImages map[string]string +} + +const ( + image_resources = ` + pod (po), replicationcontroller (rc), deployment, daemonset (ds), job, replicaset (rs)` + + image_long = `Update existing container image(s) of resources. + +Possible resources include (case insensitive):` + image_resources + + image_example = `# Set a deployment's nginx container image to 'nginx:1.9.1', and its busybox container image to 'busybox'. +kubectl set image deployment/nginx busybox=busybox nginx=nginx:1.9.1 + +# Update all deployments' and rc's nginx container's image to 'nginx:1.9.1' +kubectl set image deployments,rc nginx=nginx:1.9.1 --all + +# Update image of all containers of daemonset abc to 'nginx:1.9.1' +kubectl set image daemonset abc *=nginx:1.9.1 + +# Print result (in yaml format) of updating nginx container image from local file, without hitting the server +kubectl set image -f path/to/file.yaml nginx=nginx:1.9.1 --local -o yaml` +) + +func NewCmdImage(f *cmdutil.Factory, out io.Writer) *cobra.Command { + options := &ImageOptions{ + Out: out, + } + + cmd := &cobra.Command{ + Use: "image (-f FILENAME | TYPE NAME) CONTAINER_NAME_1=CONTAINER_IMAGE_1 ... CONTAINER_NAME_N=CONTAINER_IMAGE_N", + Short: "Update image of a pod template", + Long: image_long, + Example: image_example, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(options.Complete(f, cmd, args)) + cmdutil.CheckErr(options.Validate()) + cmdutil.CheckErr(options.Run()) + }, + } + + cmdutil.AddPrinterFlags(cmd) + usage := "Filename, directory, or URL to a file identifying the resource to get from a server." + kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage) + cmd.Flags().BoolVar(&options.All, "all", false, "select all resources in the namespace of the specified resource types") + cmd.Flags().StringVarP(&options.Selector, "selector", "l", "", "Selector (label query) to filter on") + cmd.Flags().BoolVar(&options.Local, "local", false, "If true, set image will NOT contact api-server but run locally.") + cmdutil.AddRecordFlag(cmd) + cmdutil.AddRecursiveFlag(cmd, &options.Recursive) + return cmd +} + +func (o *ImageOptions) Complete(f *cmdutil.Factory, cmd *cobra.Command, args []string) error { + o.Mapper, o.Typer = f.Object(cmdutil.GetIncludeThirdPartyAPIs(cmd)) + o.UpdatePodSpecForObject = f.UpdatePodSpecForObject + o.Encoder = f.JSONEncoder() + o.ShortOutput = cmdutil.GetFlagString(cmd, "output") == "name" + o.Record = cmdutil.GetRecordFlag(cmd) + o.ChangeCause = f.Command() + o.PrintObject = f.PrintObject + o.Cmd = cmd + + cmdNamespace, enforceNamespace, err := f.DefaultNamespace() + if err != nil { + return err + } + + o.Resources, o.ContainerImages, err = getResourcesAndImages(args) + if err != nil { + return err + } + + builder := resource.NewBuilder(o.Mapper, o.Typer, resource.ClientMapperFunc(f.ClientForMapping), f.Decoder(true)). + ContinueOnError(). + NamespaceParam(cmdNamespace).DefaultNamespace(). + FilenameParam(enforceNamespace, o.Recursive, o.Filenames...). + Flatten() + if !o.Local { + builder = builder. + SelectorParam(o.Selector). + ResourceTypeOrNameArgs(o.All, o.Resources...). + Latest() + } + o.Infos, err = builder.Do().Infos() + if err != nil { + return err + } + + return nil +} + +func (o *ImageOptions) Validate() error { + if len(o.Resources) < 1 && len(o.Filenames) == 0 { + return fmt.Errorf("one or more resources must be specified as or /") + } + if len(o.ContainerImages) < 1 { + return fmt.Errorf("at least one image update is required") + } else if len(o.ContainerImages) > 1 && hasWildcardKey(o.ContainerImages) { + return fmt.Errorf("all containers are already specified by *, but saw more than one container_name=container_image pairs") + } + return nil +} + +func (o *ImageOptions) Run() error { + allErrs := []error{} + + patches := CalculatePatches(o.Infos, o.Encoder, func(info *resource.Info) (bool, error) { + transformed := false + _, err := o.UpdatePodSpecForObject(info.Object, func(spec *api.PodSpec) error { + for name, image := range o.ContainerImages { + containerFound := false + // Find the container to update, and update its image + for i, c := range spec.Containers { + if c.Name == name || name == "*" { + spec.Containers[i].Image = image + containerFound = true + // Perform updates + transformed = true + } + } + // Add a new container if not found + if !containerFound { + allErrs = append(allErrs, fmt.Errorf("error: unable to find container named %q", name)) + } + } + return nil + }) + return transformed, err + }) + + for _, patch := range patches { + info := patch.Info + if patch.Err != nil { + allErrs = append(allErrs, fmt.Errorf("error: %s/%s %v\n", info.Mapping.Resource, info.Name, patch.Err)) + continue + } + + // no changes + if string(patch.Patch) == "{}" || len(patch.Patch) == 0 { + continue + } + + if o.Local { + fmt.Fprintln(o.Out, "running in local mode...") + return o.PrintObject(o.Cmd, o.Mapper, info.Object, o.Out) + } + + // patch the change + obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, api.StrategicMergePatchType, patch.Patch) + if err != nil { + allErrs = append(allErrs, fmt.Errorf("failed to patch image update to pod template: %v\n", err)) + continue + } + info.Refresh(obj, true) + + // record this change (for rollout history) + if o.Record || cmdutil.ContainsChangeCause(info) { + if err := cmdutil.RecordChangeCause(obj, o.ChangeCause); err == nil { + if obj, err = resource.NewHelper(info.Client, info.Mapping).Replace(info.Namespace, info.Name, false, obj); err != nil { + allErrs = append(allErrs, fmt.Errorf("changes to %s/%s can't be recorded: %v\n", info.Mapping.Resource, info.Name, err)) + } + } + } + + info.Refresh(obj, true) + cmdutil.PrintSuccess(o.Mapper, o.ShortOutput, o.Out, info.Mapping.Resource, info.Name, "image updated") + } + return utilerrors.NewAggregate(allErrs) +} + +// getResourcesAndImages retrieves resources and container name:images pair from given args +func getResourcesAndImages(args []string) (resources []string, containerImages map[string]string, err error) { + pairType := "image" + resources, imageArgs, err := cmdutil.GetResourcesAndPairs(args, pairType) + if err != nil { + return + } + containerImages, _, err = cmdutil.ParsePairs(imageArgs, pairType, false) + return +} + +func hasWildcardKey(containerImages map[string]string) bool { + _, ok := containerImages["*"] + return ok +} diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index f2f213e3796..280387822d8 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -139,6 +139,9 @@ type Factory struct { CanBeAutoscaled func(kind unversioned.GroupKind) error // AttachablePodForObject returns the pod to which to attach given an object. AttachablePodForObject func(object runtime.Object) (*api.Pod, error) + // UpdatePodSpecForObject will call the provided function on the pod spec this object supports, + // return false if no pod spec is supported, or return an error. + UpdatePodSpecForObject func(obj runtime.Object, fn func(*api.PodSpec) error) (bool, error) // EditorEnvs returns a group of environment variables that the edit command // can range over in order to determine if the user has specified an editor // of their choice. @@ -708,6 +711,31 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { return nil, fmt.Errorf("cannot attach to %v: not implemented", gvk) } }, + // UpdatePodSpecForObject update the pod specification for the provided object + UpdatePodSpecForObject: func(obj runtime.Object, fn func(*api.PodSpec) error) (bool, error) { + // TODO: replace with a swagger schema based approach (identify pod template via schema introspection) + switch t := obj.(type) { + case *api.Pod: + return true, fn(&t.Spec) + case *api.ReplicationController: + if t.Spec.Template == nil { + t.Spec.Template = &api.PodTemplateSpec{} + } + return true, fn(&t.Spec.Template.Spec) + case *extensions.Deployment: + return true, fn(&t.Spec.Template.Spec) + case *extensions.DaemonSet: + return true, fn(&t.Spec.Template.Spec) + case *extensions.ReplicaSet: + return true, fn(&t.Spec.Template.Spec) + case *apps.PetSet: + return true, fn(&t.Spec.Template.Spec) + case *batch.Job: + return true, fn(&t.Spec.Template.Spec) + default: + return false, fmt.Errorf("the object is not a pod or does not have a pod template") + } + }, EditorEnvs: func() []string { return []string{"KUBE_EDITOR", "EDITOR"} }, diff --git a/pkg/kubectl/cmd/util/helpers.go b/pkg/kubectl/cmd/util/helpers.go index aac691e8ef0..d65841e2f38 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -336,6 +336,11 @@ func AddRecursiveFlag(cmd *cobra.Command, value *bool) { cmd.Flags().BoolVarP(value, "recursive", "R", *value, "If true, process directory recursively.") } +// AddDryRunFlag adds dry-run flag to a command. Usually used by mutations. +func AddDryRunFlag(cmd *cobra.Command) { + cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without sending it.") +} + func AddApplyAnnotationFlags(cmd *cobra.Command) { cmd.Flags().Bool(ApplyAnnotationsFlag, false, "If true, the configuration of current object will be saved in its annotation. This is useful when you want to perform kubectl apply on this object in the future.") } @@ -344,7 +349,7 @@ func AddApplyAnnotationFlags(cmd *cobra.Command) { // TODO: need to take a pass at other generator commands to use this set of flags func AddGeneratorFlags(cmd *cobra.Command, defaultGenerator string) { cmd.Flags().String("generator", defaultGenerator, "The name of the API generator to use.") - cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without sending it.") + AddDryRunFlag(cmd) } func ReadConfigDataFromReader(reader io.Reader, source string) ([]byte, error) { @@ -433,6 +438,10 @@ func GetRecordFlag(cmd *cobra.Command) bool { return GetFlagBool(cmd, "record") } +func GetDryRunFlag(cmd *cobra.Command) bool { + return GetFlagBool(cmd, "dry-run") +} + // RecordChangeCause annotate change-cause to input runtime object. func RecordChangeCause(obj runtime.Object, changeCause string) error { accessor, err := meta.Accessor(obj) @@ -528,3 +537,60 @@ func GetIncludeThirdPartyAPIs(cmd *cobra.Command) bool { func AddInclude3rdPartyFlags(cmd *cobra.Command) { cmd.Flags().Bool("include-extended-apis", true, "If true, include definitions of new APIs via calls to the API server. [default true]") } + +// GetResourcesAndPairs retrieves resources and "KEY=VALUE or KEY-" pair args from given args +func GetResourcesAndPairs(args []string, pairType string) (resources []string, pairArgs []string, err error) { + foundPair := false + for _, s := range args { + nonResource := strings.Contains(s, "=") || strings.HasSuffix(s, "-") + switch { + case !foundPair && nonResource: + foundPair = true + fallthrough + case foundPair && nonResource: + pairArgs = append(pairArgs, s) + case !foundPair && !nonResource: + resources = append(resources, s) + case foundPair && !nonResource: + err = fmt.Errorf("all resources must be specified before %s changes: %s", pairType, s) + return + } + } + return +} + +// ParsePairs retrieves new and remove pairs (if supportRemove is true) from "KEY=VALUE or KEY-" pair args +func ParsePairs(pairArgs []string, pairType string, supportRemove bool) (newPairs map[string]string, removePairs []string, err error) { + newPairs = map[string]string{} + if supportRemove { + removePairs = []string{} + } + var invalidBuf bytes.Buffer + + for _, pairArg := range pairArgs { + if strings.Index(pairArg, "=") != -1 { + parts := strings.SplitN(pairArg, "=", 2) + if len(parts) != 2 || len(parts[1]) == 0 { + if invalidBuf.Len() > 0 { + invalidBuf.WriteString(", ") + } + invalidBuf.WriteString(fmt.Sprintf(pairArg)) + } else { + newPairs[parts[0]] = parts[1] + } + } else if supportRemove && strings.HasSuffix(pairArg, "-") { + removePairs = append(removePairs, pairArg[:len(pairArg)-1]) + } else { + if invalidBuf.Len() > 0 { + invalidBuf.WriteString(", ") + } + invalidBuf.WriteString(fmt.Sprintf(pairArg)) + } + } + if invalidBuf.Len() > 0 { + err = fmt.Errorf("invalid %s format: %s", pairType, invalidBuf.String()) + return + } + + return +}