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 @@
+
+
+
+
+
+
+
+
+
+
+
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.