From 703a3e19aabfc4050effa70cb1d4cc26d4fcb1f6 Mon Sep 17 00:00:00 2001 From: jackgr Date: Thu, 10 Sep 2015 14:32:57 -0700 Subject: [PATCH] Add the kubectl apply command. --- .generated_docs | 2 + contrib/completions/bash/kubectl | 28 +++ docs/man/man1/.files_generated | 1 + docs/man/man1/kubectl-apply.1 | 156 ++++++++++++++ docs/man/man1/kubectl.1 | 2 +- docs/user-guide/kubectl/.files_generated | 1 + docs/user-guide/kubectl/kubectl.md | 1 + docs/user-guide/kubectl/kubectl_apply.md | 104 +++++++++ pkg/kubectl/apply.go | 164 ++++++++++++++ pkg/kubectl/cmd/apply.go | 164 ++++++++++++++ pkg/kubectl/cmd/apply_test.go | 260 +++++++++++++++++++++++ pkg/kubectl/cmd/cmd.go | 1 + test/e2e/kubectl.go | 94 ++++++++ 13 files changed, 977 insertions(+), 1 deletion(-) create mode 100644 docs/man/man1/kubectl-apply.1 create mode 100644 docs/user-guide/kubectl/kubectl_apply.md create mode 100644 pkg/kubectl/apply.go create mode 100644 pkg/kubectl/cmd/apply.go create mode 100644 pkg/kubectl/cmd/apply_test.go diff --git a/.generated_docs b/.generated_docs index e29e199234..a337375038 100644 --- a/.generated_docs +++ b/.generated_docs @@ -2,6 +2,7 @@ contrib/completions/bash/kubectl docs/man/man1/kubectl-annotate.1 docs/man/man1/kubectl-api-versions.1 +docs/man/man1/kubectl-apply.1 docs/man/man1/kubectl-attach.1 docs/man/man1/kubectl-cluster-info.1 docs/man/man1/kubectl-config-set-cluster.1 @@ -36,6 +37,7 @@ docs/man/man1/kubectl.1 docs/user-guide/kubectl/kubectl.md docs/user-guide/kubectl/kubectl_annotate.md docs/user-guide/kubectl/kubectl_api-versions.md +docs/user-guide/kubectl/kubectl_apply.md docs/user-guide/kubectl/kubectl_attach.md docs/user-guide/kubectl/kubectl_cluster-info.md docs/user-guide/kubectl/kubectl_config.md diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index d2673561cd..157aa75ab9 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -504,6 +504,33 @@ _kubectl_edit() must_have_one_noun=() } +_kubectl_apply() +{ + last_command="kubectl_apply" + commands=() + + flags=() + two_word_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--filename=") + flags_with_completion+=("--filename") + flags_completion+=("__handle_filename_extension_flag json|stdin|yaml|yml") + two_word_flags+=("-f") + flags_with_completion+=("-f") + flags_completion+=("__handle_filename_extension_flag json|stdin|yaml|yml") + flags+=("--output=") + two_word_flags+=("-o") + flags+=("--schema-cache-dir=") + flags+=("--validate") + + must_have_one_flag=() + must_have_one_flag+=("--filename=") + must_have_one_flag+=("-f") + must_have_one_noun=() +} + _kubectl_namespace() { last_command="kubectl_namespace" @@ -1136,6 +1163,7 @@ _kubectl() commands+=("patch") commands+=("delete") commands+=("edit") + commands+=("apply") commands+=("namespace") commands+=("logs") commands+=("rolling-update") diff --git a/docs/man/man1/.files_generated b/docs/man/man1/.files_generated index 68d2728c71..097fb577ae 100644 --- a/docs/man/man1/.files_generated +++ b/docs/man/man1/.files_generated @@ -1,3 +1,4 @@ +kubectl-apply.1 kubectl-annotate.1 kubectl-api-versions.1 kubectl-attach.1 diff --git a/docs/man/man1/kubectl-apply.1 b/docs/man/man1/kubectl-apply.1 new file mode 100644 index 0000000000..c739dc1402 --- /dev/null +++ b/docs/man/man1/kubectl-apply.1 @@ -0,0 +1,156 @@ +.TH "KUBERNETES" "1" " kubernetes User Manuals" "Eric Paris" "Jan 2015" "" + + +.SH NAME +.PP +kubectl apply \- Apply a configuration to a resource by filename or stdin + + +.SH SYNOPSIS +.PP +\fBkubectl apply\fP [OPTIONS] + + +.SH DESCRIPTION +.PP +Apply a configuration to a resource by filename or stdin. + +.PP +JSON and YAML formats are accepted. + + +.SH OPTIONS +.PP +\fB\-f\fP, \fB\-\-filename\fP=[] + Filename, directory, or URL to file that contains the configuration to apply + +.PP +\fB\-o\fP, \fB\-\-output\fP="" + Output mode. Use "\-o name" for shorter output (resource/name). + +.PP +\fB\-\-schema\-cache\-dir\fP="\~/.kube/schema" + If non\-empty, load/store cached API schemas in this directory, default is '$HOME/.kube/schema' + +.PP +\fB\-\-validate\fP=true + If true, use a schema to validate the input before sending it + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +.PP +\fB\-\-alsologtostderr\fP=false + log to standard error as well as files + +.PP +\fB\-\-api\-version\fP="" + The API version to use when talking to the server + +.PP +\fB\-\-certificate\-authority\fP="" + Path to a cert. file for the certificate authority. + +.PP +\fB\-\-client\-certificate\fP="" + Path to a client key 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 +# Apply the configuration in pod.json to a pod. +$ kubectl apply \-f ./pod.json + +# Apply the JSON passed into stdin to a pod. +$ cat pod.json | kubectl apply \-f \- + +.fi +.RE + + +.SH SEE ALSO +.PP +\fBkubectl(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.1 b/docs/man/man1/kubectl.1 index c2acd8202d..f71e4b33ba 100644 --- a/docs/man/man1/kubectl.1 +++ b/docs/man/man1/kubectl.1 @@ -116,7 +116,7 @@ Find more information at .SH SEE ALSO .PP -\fBkubectl\-get(1)\fP, \fBkubectl\-describe(1)\fP, \fBkubectl\-create(1)\fP, \fBkubectl\-replace(1)\fP, \fBkubectl\-patch(1)\fP, \fBkubectl\-delete(1)\fP, \fBkubectl\-edit(1)\fP, \fBkubectl\-namespace(1)\fP, \fBkubectl\-logs(1)\fP, \fBkubectl\-rolling\-update(1)\fP, \fBkubectl\-scale(1)\fP, \fBkubectl\-attach(1)\fP, \fBkubectl\-exec(1)\fP, \fBkubectl\-port\-forward(1)\fP, \fBkubectl\-proxy(1)\fP, \fBkubectl\-run(1)\fP, \fBkubectl\-stop(1)\fP, \fBkubectl\-expose(1)\fP, \fBkubectl\-label(1)\fP, \fBkubectl\-annotate(1)\fP, \fBkubectl\-config(1)\fP, \fBkubectl\-cluster\-info(1)\fP, \fBkubectl\-api\-versions(1)\fP, \fBkubectl\-version(1)\fP, \fBkubectl\-explain(1)\fP, +\fBkubectl\-get(1)\fP, \fBkubectl\-describe(1)\fP, \fBkubectl\-create(1)\fP, \fBkubectl\-replace(1)\fP, \fBkubectl\-patch(1)\fP, \fBkubectl\-delete(1)\fP, \fBkubectl\-edit(1)\fP, \fBkubectl\-apply(1)\fP, \fBkubectl\-namespace(1)\fP, \fBkubectl\-logs(1)\fP, \fBkubectl\-rolling\-update(1)\fP, \fBkubectl\-scale(1)\fP, \fBkubectl\-attach(1)\fP, \fBkubectl\-exec(1)\fP, \fBkubectl\-port\-forward(1)\fP, \fBkubectl\-proxy(1)\fP, \fBkubectl\-run(1)\fP, \fBkubectl\-stop(1)\fP, \fBkubectl\-expose(1)\fP, \fBkubectl\-label(1)\fP, \fBkubectl\-annotate(1)\fP, \fBkubectl\-config(1)\fP, \fBkubectl\-cluster\-info(1)\fP, \fBkubectl\-api\-versions(1)\fP, \fBkubectl\-version(1)\fP, \fBkubectl\-explain(1)\fP, .SH HISTORY diff --git a/docs/user-guide/kubectl/.files_generated b/docs/user-guide/kubectl/.files_generated index 3ebfe4d6c6..a05143cf35 100644 --- a/docs/user-guide/kubectl/.files_generated +++ b/docs/user-guide/kubectl/.files_generated @@ -1,4 +1,5 @@ kubectl.md +kubectl_apply.md kubectl_annotate.md kubectl_api-versions.md kubectl_attach.md diff --git a/docs/user-guide/kubectl/kubectl.md b/docs/user-guide/kubectl/kubectl.md index 2d1b75b976..4fefaa22d3 100644 --- a/docs/user-guide/kubectl/kubectl.md +++ b/docs/user-guide/kubectl/kubectl.md @@ -78,6 +78,7 @@ kubectl * [kubectl annotate](kubectl_annotate.md) - Update the annotations on a resource * [kubectl api-versions](kubectl_api-versions.md) - Print available API versions. +* [kubectl apply](kubectl_apply.md) - Apply a configuration to a resource by filename or stdin * [kubectl attach](kubectl_attach.md) - Attach to a running container. * [kubectl cluster-info](kubectl_cluster-info.md) - Display cluster info * [kubectl config](kubectl_config.md) - config modifies kubeconfig files diff --git a/docs/user-guide/kubectl/kubectl_apply.md b/docs/user-guide/kubectl/kubectl_apply.md new file mode 100644 index 0000000000..7125783e2d --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_apply.md @@ -0,0 +1,104 @@ + + + + +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. + + +The latest 1.0.x release of this document can be found +[here](http://releases.k8s.io/release-1.0/docs/user-guide/kubectl/kubectl_apply.md). + +Documentation for other releases can be found at +[releases.k8s.io](http://releases.k8s.io). + +-- + + + + + +## kubectl apply + +Apply a configuration to a resource by filename or stdin + +### Synopsis + + +Apply a configuration to a resource by filename or stdin. + +JSON and YAML formats are accepted. + +``` +kubectl apply -f FILENAME +``` + +### Examples + +``` +# Apply the configuration in pod.json to a pod. +$ kubectl apply -f ./pod.json + +# Apply the JSON passed into stdin to a pod. +$ cat pod.json | kubectl apply -f - +``` + +### Options + +``` + -f, --filename=[]: Filename, directory, or URL to file that contains the configuration to apply + -o, --output="": Output mode. Use "-o name" for shorter output (resource/name). + --schema-cache-dir="~/.kube/schema": If non-empty, load/store cached API schemas in this directory, default is '$HOME/.kube/schema' + --validate[=true]: If true, use a schema to validate the input before sending it +``` + +### Options inherited from parent commands + +``` + --alsologtostderr[=false]: log to standard error as well as files + --api-version="": The API version to use when talking to the server + --certificate-authority="": Path to a cert. file for the certificate authority. + --client-certificate="": Path to a client key 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](kubectl.md) - kubectl controls the Kubernetes cluster manager + +###### Auto generated by spf13/cobra at 2015-10-01 05:36:57.66914652 +0000 UTC + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_apply.md?pixel)]() + diff --git a/pkg/kubectl/apply.go b/pkg/kubectl/apply.go new file mode 100644 index 0000000000..ea0abf2931 --- /dev/null +++ b/pkg/kubectl/apply.go @@ -0,0 +1,164 @@ +/* +Copyright 2014 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 kubectl + +import ( + "encoding/json" + + "k8s.io/kubernetes/pkg/api/meta" + "k8s.io/kubernetes/pkg/kubectl/resource" +) + +type debugError interface { + DebugError() (msg string, args []interface{}) +} + +// LastAppliedConfigAnnotation is the annotation used to store the previous +// configuration of a resource for use in a three way diff by UpdateApplyAnnotation. +const LastAppliedConfigAnnotation = kubectlAnnotationPrefix + "last-applied-configuration" + +// GetOriginalConfiguration retrieves the original configuration of the object +// from the annotation, or nil if no annotation was found. +func GetOriginalConfiguration(info *resource.Info) ([]byte, error) { + annotations, err := info.Mapping.MetadataAccessor.Annotations(info.Object) + if err != nil { + return nil, err + } + + if annotations == nil { + return nil, nil + } + + original, ok := annotations[LastAppliedConfigAnnotation] + if !ok { + return nil, nil + } + + return []byte(original), nil +} + +// SetOriginalConfiguration sets the original configuration of the object +// as the annotation on the object for later use in computing a three way patch. +func SetOriginalConfiguration(info *resource.Info, original []byte) error { + if len(original) < 1 { + return nil + } + + // Get the current annotations from the object. + annotations, err := info.Mapping.MetadataAccessor.Annotations(info.Object) + if err != nil { + return err + } + + if annotations == nil { + annotations = map[string]string{} + } + + annotations[LastAppliedConfigAnnotation] = string(original) + if err := info.Mapping.MetadataAccessor.SetAnnotations(info.Object, annotations); err != nil { + return err + } + + return nil +} + +// GetModifiedConfiguration retrieves the modified configuration of the object. +// If annotate is true, it embeds the result as an anotation in the modified +// configuration. If an object was read from the command input, it will use that +// version of the object. Otherwise, it will use the version from the server. +func GetModifiedConfiguration(info *resource.Info, annotate bool) ([]byte, error) { + // First serialize the object without the annotation to prevent recursion, + // then add that serialization to it as the annotation and serialize it again. + var modified []byte + if info.VersionedObject != nil { + // If an object was read from input, use that version. + accessor, err := meta.Accessor(info.VersionedObject) + if err != nil { + return nil, err + } + + // Get the current annotations from the object. + annotations := accessor.Annotations() + if annotations == nil { + annotations = map[string]string{} + } + + original := annotations[LastAppliedConfigAnnotation] + delete(annotations, LastAppliedConfigAnnotation) + accessor.SetAnnotations(annotations) + modified, err = json.Marshal(info.VersionedObject) + if err != nil { + return nil, err + } + + if annotate { + annotations[LastAppliedConfigAnnotation] = string(modified) + accessor.SetAnnotations(annotations) + modified, err = json.Marshal(info.VersionedObject) + if err != nil { + return nil, err + } + } + + // Restore the object to its original condition. + annotations[LastAppliedConfigAnnotation] = original + accessor.SetAnnotations(annotations) + } else { + // Otherwise, use the server side version of the object. + accessor := info.Mapping.MetadataAccessor + // Get the current annotations from the object. + annotations, err := accessor.Annotations(info.Object) + if err != nil { + return nil, err + } + + if annotations == nil { + annotations = map[string]string{} + } + + original := annotations[LastAppliedConfigAnnotation] + delete(annotations, LastAppliedConfigAnnotation) + if err := accessor.SetAnnotations(info.Object, annotations); err != nil { + return nil, err + } + + modified, err = info.Mapping.Codec.Encode(info.Object) + if err != nil { + return nil, err + } + + if annotate { + annotations[LastAppliedConfigAnnotation] = string(modified) + if err := info.Mapping.MetadataAccessor.SetAnnotations(info.Object, annotations); err != nil { + return nil, err + } + + modified, err = info.Mapping.Codec.Encode(info.Object) + if err != nil { + return nil, err + } + } + + // Restore the object to its original condition. + annotations[LastAppliedConfigAnnotation] = original + if err := info.Mapping.MetadataAccessor.SetAnnotations(info.Object, annotations); err != nil { + return nil, err + } + } + + return modified, nil +} diff --git a/pkg/kubectl/cmd/apply.go b/pkg/kubectl/cmd/apply.go new file mode 100644 index 0000000000..8daf7087e7 --- /dev/null +++ b/pkg/kubectl/cmd/apply.go @@ -0,0 +1,164 @@ +/* +Copyright 2014 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 cmd + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/kubectl" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/util/strategicpatch" +) + +// ApplyOptions stores cmd.Flag values for apply. As new fields are added, add them here instead of +// referencing the cmd.Flags() +type ApplyOptions struct { + Filenames []string +} + +const ( + apply_long = `Apply a configuration to a resource by filename or stdin. + +JSON and YAML formats are accepted.` + apply_example = `# Apply the configuration in pod.json to a pod. +$ kubectl apply -f ./pod.json + +# Apply the JSON passed into stdin to a pod. +$ cat pod.json | kubectl apply -f -` +) + +func NewCmdApply(f *cmdutil.Factory, out io.Writer) *cobra.Command { + options := &ApplyOptions{} + + cmd := &cobra.Command{ + Use: "apply -f FILENAME", + Short: "Apply a configuration to a resource by filename or stdin", + Long: apply_long, + Example: apply_example, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(validateArgs(cmd, args)) + cmdutil.CheckErr(cmdutil.ValidateOutputArgs(cmd)) + cmdutil.CheckErr(RunApply(f, cmd, out, options)) + }, + } + + usage := "Filename, directory, or URL to file that contains the configuration to apply" + kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage) + cmd.MarkFlagRequired("filename") + cmdutil.AddValidateFlags(cmd) + cmdutil.AddOutputFlagsForMutation(cmd) + return cmd +} + +func validateArgs(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return cmdutil.UsageError(cmd, "Unexpected args: %v", args) + } + + return nil +} + +func RunApply(f *cmdutil.Factory, cmd *cobra.Command, out io.Writer, options *ApplyOptions) error { + shortOutput := cmdutil.GetFlagString(cmd, "output") == "name" + schema, err := f.Validator(cmdutil.GetFlagBool(cmd, "validate"), cmdutil.GetFlagString(cmd, "schema-cache-dir")) + if err != nil { + return err + } + + cmdNamespace, enforceNamespace, err := f.DefaultNamespace() + if err != nil { + return err + } + + mapper, typer := f.Object() + r := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()). + Schema(schema). + ContinueOnError(). + NamespaceParam(cmdNamespace).DefaultNamespace(). + FilenameParam(enforceNamespace, options.Filenames...). + Flatten(). + Do() + err = r.Err() + if err != nil { + return err + } + + count := 0 + err = r.Visit(func(info *resource.Info, err error) error { + // In this method, info.Object contains the object retrieved from the server + // and info.VersionedObject contains the object decoded from the input source. + if err != nil { + return err + } + + // Get the modified configuration of the object. Embed the result + // as an annotation in the modified configuration, so that it will appear + // in the patch sent to the server. + modified, err := kubectl.GetModifiedConfiguration(info, true) + if err != nil { + return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving modified configuration from:\n%v\nfor:", info), info.Source, err) + } + + if err := info.Get(); err != nil { + return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%v\nfrom server for:", info), info.Source, err) + } + + // Serialize the current configuration of the object from the server. + current, err := info.Mapping.Codec.Encode(info.Object) + if err != nil { + return cmdutil.AddSourceToErr(fmt.Sprintf("serializing current configuration from:\n%v\nfor:", info), info.Source, err) + } + + // Retrieve the original configuration of the object from the annotation. + original, err := kubectl.GetOriginalConfiguration(info) + if err != nil { + return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving original configuration from:\n%v\nfor:", info), info.Source, err) + } + + // Compute a three way strategic merge patch to send to server. + patch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, info.VersionedObject, false) + if err != nil { + format := "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfrom:\n%v\nfor:" + return cmdutil.AddSourceToErr(fmt.Sprintf(format, original, modified, current, info), info.Source, err) + } + + helper := resource.NewHelper(info.Client, info.Mapping) + _, err = helper.Patch(info.Namespace, info.Name, api.StrategicMergePatchType, patch) + if err != nil { + return cmdutil.AddSourceToErr(fmt.Sprintf("applying patch:\n%s\nto:\n%v\nfor:", patch, info), info.Source, err) + } + + count++ + cmdutil.PrintSuccess(mapper, shortOutput, out, info.Mapping.Resource, info.Name, "configured") + return nil + }) + + if err != nil { + return err + } + + if count == 0 { + return fmt.Errorf("no objects passed to apply") + } + + return nil +} diff --git a/pkg/kubectl/cmd/apply_test.go b/pkg/kubectl/cmd/apply_test.go new file mode 100644 index 0000000000..505a324000 --- /dev/null +++ b/pkg/kubectl/cmd/apply_test.go @@ -0,0 +1,260 @@ +/* +Copyright 2014 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 cmd + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "os" + "testing" + + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/client/unversioned/fake" + "k8s.io/kubernetes/pkg/kubectl" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/runtime" +) + +func TestApplyExtraArgsFail(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + + f, _, _ := NewAPIFactory() + c := NewCmdApply(f, buf) + if validateApplyArgs(c, []string{"rc"}) == nil { + t.Fatalf("unexpected non-error") + } +} + +func validateApplyArgs(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return cmdutil.UsageError(cmd, "Unexpected args: %v", args) + } + return nil +} + +const ( + filenameRC = "../../../examples/guestbook/redis-master-controller.yaml" + filenameSVC = "../../../examples/guestbook/frontend-service.yaml" +) + +func readBytesFromFile(t *testing.T, filename string) []byte { + file, err := os.Open(filename) + if err != nil { + t.Fatal(err) + } + + data, err := ioutil.ReadAll(file) + if err != nil { + t.Fatal(err) + } + + return data +} + +func readReplicationControllerFromFile(t *testing.T, filename string) *api.ReplicationController { + data := readBytesFromFile(t, filename) + rc := api.ReplicationController{} + // TODO(jackgr): Replace with a call to testapi.Codec().Decode(). + if err := yaml.Unmarshal(data, &rc); err != nil { + t.Fatal(err) + } + + return &rc +} + +func readServiceFromFile(t *testing.T, filename string) *api.Service { + data := readBytesFromFile(t, filename) + svc := api.Service{} + // TODO(jackgr): Replace with a call to testapi.Codec().Decode(). + if err := yaml.Unmarshal(data, &svc); err != nil { + t.Fatal(err) + } + + return &svc +} + +func annotateRuntimeObject(t *testing.T, originalObj, currentObj runtime.Object, kind string) (string, []byte) { + originalMeta, err := api.ObjectMetaFor(originalObj) + if err != nil { + t.Fatal(err) + } + + originalMeta.Labels["DELETE_ME"] = "DELETE_ME" + original, err := json.Marshal(originalObj) + if err != nil { + t.Fatal(err) + } + + currentMeta, err := api.ObjectMetaFor(currentObj) + if err != nil { + t.Fatal(err) + } + + if currentMeta.Annotations == nil { + currentMeta.Annotations = map[string]string{} + } + + currentMeta.Annotations[kubectl.LastAppliedConfigAnnotation] = string(original) + current, err := json.Marshal(currentObj) + if err != nil { + t.Fatal(err) + } + + return currentMeta.Name, current +} + +func readAndAnnotateReplicationController(t *testing.T, filename string) (string, []byte) { + rc1 := readReplicationControllerFromFile(t, filename) + rc2 := readReplicationControllerFromFile(t, filename) + return annotateRuntimeObject(t, rc1, rc2, "ReplicationController") +} + +func readAndAnnotateService(t *testing.T, filename string) (string, []byte) { + svc1 := readServiceFromFile(t, filename) + svc2 := readServiceFromFile(t, filename) + return annotateRuntimeObject(t, svc1, svc2, "Service") +} + +func validatePatchApplication(t *testing.T, req *http.Request) { + patch, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Fatal(err) + } + + patchMap := map[string]interface{}{} + if err := json.Unmarshal(patch, &patchMap); err != nil { + t.Fatal(err) + } + + annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"}) + if _, ok := annotationsMap[kubectl.LastAppliedConfigAnnotation]; !ok { + t.Fatalf("patch does not contain annotation:\n%s\n", patch) + } + + labelMap := walkMapPath(t, patchMap, []string{"metadata", "labels"}) + if deleteMe, ok := labelMap["DELETE_ME"]; !ok || deleteMe != nil { + t.Fatalf("patch does not remove deleted key: DELETE_ME:\n%s\n", patch) + } +} + +func walkMapPath(t *testing.T, start map[string]interface{}, path []string) map[string]interface{} { + finish := start + for i := 0; i < len(path); i++ { + var ok bool + finish, ok = finish[path[i]].(map[string]interface{}) + if !ok { + t.Fatalf("key:%s of path:%v not found in map:%v", path[i], path, start) + } + } + + return finish +} + +func TestApplyObject(t *testing.T) { + nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) + pathRC := "/namespaces/test/replicationcontrollers/" + nameRC + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &fake.RESTClient{ + Codec: codec, + Client: fake.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == pathRC && m == "GET": + bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC)) + return &http.Response{StatusCode: 200, Body: bodyRC}, nil + case p == pathRC && m == "PATCH": + validatePatchApplication(t, req) + bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC)) + return &http.Response{StatusCode: 200, Body: bodyRC}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) + + cmd := NewCmdApply(f, buf) + cmd.Flags().Set("filename", filenameRC) + cmd.Flags().Set("output", "name") + cmd.Run(cmd, []string{}) + + // uses the name from the file, not the response + expectRC := "replicationcontroller/" + nameRC + "\n" + if buf.String() != expectRC { + t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) + } +} + +func TestApplyMultipleObject(t *testing.T) { + nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) + pathRC := "/namespaces/test/replicationcontrollers/" + nameRC + + nameSVC, currentSVC := readAndAnnotateService(t, filenameSVC) + pathSVC := "/namespaces/test/services/" + nameSVC + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &fake.RESTClient{ + Codec: codec, + Client: fake.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == pathRC && m == "GET": + bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC)) + return &http.Response{StatusCode: 200, Body: bodyRC}, nil + case p == pathRC && m == "PATCH": + validatePatchApplication(t, req) + bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC)) + return &http.Response{StatusCode: 200, Body: bodyRC}, nil + case p == pathSVC && m == "GET": + bodySVC := ioutil.NopCloser(bytes.NewReader(currentSVC)) + return &http.Response{StatusCode: 200, Body: bodySVC}, nil + case p == pathSVC && m == "PATCH": + validatePatchApplication(t, req) + bodySVC := ioutil.NopCloser(bytes.NewReader(currentSVC)) + return &http.Response{StatusCode: 200, Body: bodySVC}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) + + cmd := NewCmdApply(f, buf) + cmd.Flags().Set("filename", filenameRC) + cmd.Flags().Set("filename", filenameSVC) + cmd.Flags().Set("output", "name") + + cmd.Run(cmd, []string{}) + + // Names should come from the REST response, NOT the files + expectRC := "replicationcontroller/" + nameRC + "\n" + expectSVC := "service/" + nameSVC + "\n" + expect := expectRC + expectSVC + if buf.String() != expect { + t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expect) + } +} diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 29958e988d..7f504c1cc4 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -150,6 +150,7 @@ Find more information at https://github.com/kubernetes/kubernetes.`, cmds.AddCommand(NewCmdPatch(f, out)) cmds.AddCommand(NewCmdDelete(f, out)) cmds.AddCommand(NewCmdEdit(f, out)) + cmds.AddCommand(NewCmdApply(f, out)) cmds.AddCommand(NewCmdNamespace(out)) cmds.AddCommand(NewCmdLog(f, out)) diff --git a/test/e2e/kubectl.go b/test/e2e/kubectl.go index 46b0cf2413..126fb6a9ca 100644 --- a/test/e2e/kubectl.go +++ b/test/e2e/kubectl.go @@ -17,6 +17,7 @@ limitations under the License. package e2e import ( + "bytes" "encoding/json" "errors" "fmt" @@ -32,10 +33,13 @@ import ( "strings" "time" + "github.com/ghodss/yaml" + "k8s.io/kubernetes/pkg/api" apierrs "k8s.io/kubernetes/pkg/api/errors" client "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/kubectl" "k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/util/wait" @@ -226,6 +230,25 @@ var _ = Describe("Kubectl client", func() { }) }) + Describe("Kubectl apply", func() { + It("should apply a new configuration to an existing RC", func() { + mkpath := func(file string) string { + return filepath.Join(testContext.RepoRoot, "examples/guestbook-go", file) + } + controllerJson := mkpath("redis-master-controller.json") + nsFlag := fmt.Sprintf("--namespace=%v", ns) + By("creating Redis RC") + runKubectl("create", "-f", controllerJson, nsFlag) + By("applying a modified configuration") + stdin := modifyReplicationControllerConfiguration(controllerJson) + newKubectlCommand("apply", "-f", "-", nsFlag). + withStdinReader(stdin). + exec() + By("checking the result") + forEachReplicationController(c, ns, "app", "redis", validateReplicationControllerConfiguration) + }) + }) + Describe("Kubectl cluster-info", func() { It("should check if Kubernetes master services is included in cluster-info", func() { By("validating cluster-info") @@ -811,6 +834,77 @@ type updateDemoData struct { Image string } +const applyTestLabel = "kubectl.kubernetes.io/apply-test" + +func readBytesFromFile(filename string) []byte { + file, err := os.Open(filename) + if err != nil { + Failf(err.Error()) + } + + data, err := ioutil.ReadAll(file) + if err != nil { + Failf(err.Error()) + } + + return data +} + +func readReplicationControllerFromFile(filename string) *api.ReplicationController { + data := readBytesFromFile(filename) + rc := api.ReplicationController{} + if err := yaml.Unmarshal(data, &rc); err != nil { + Failf(err.Error()) + } + + return &rc +} + +func modifyReplicationControllerConfiguration(filename string) io.Reader { + rc := readReplicationControllerFromFile(filename) + rc.Labels[applyTestLabel] = "ADDED" + rc.Spec.Selector[applyTestLabel] = "ADDED" + rc.Spec.Template.Labels[applyTestLabel] = "ADDED" + data, err := json.Marshal(rc) + if err != nil { + Failf("json marshal failed: %s\n", err) + } + + return bytes.NewReader(data) +} + +func forEachReplicationController(c *client.Client, ns, selectorKey, selectorValue string, fn func(api.ReplicationController)) { + var rcs *api.ReplicationControllerList + var err error + for t := time.Now(); time.Since(t) < podListTimeout; time.Sleep(poll) { + rcs, err = c.ReplicationControllers(ns).List(labels.SelectorFromSet(labels.Set(map[string]string{selectorKey: selectorValue}))) + Expect(err).NotTo(HaveOccurred()) + if len(rcs.Items) > 0 { + break + } + } + + if rcs == nil || len(rcs.Items) == 0 { + Failf("No replication controllers found") + } + + for _, rc := range rcs.Items { + fn(rc) + } +} + +func validateReplicationControllerConfiguration(rc api.ReplicationController) { + if rc.Name == "redis-master" { + if _, ok := rc.Annotations[kubectl.LastAppliedConfigAnnotation]; !ok { + Failf("Annotation not found in modified configuration:\n%v\n", rc) + } + + if value, ok := rc.Labels[applyTestLabel]; !ok || value != "ADDED" { + Failf("Added label %s not found in modified configuration:\n%v\n", applyTestLabel, rc) + } + } +} + // getUDData creates a validator function based on the input string (i.e. kitten.jpg). // For example, if you send "kitten.jpg", this function veridies that the image jpg = kitten.jpg // in the container's json field.