diff --git a/docs/man/man1/kubectl-apply.1 b/docs/man/man1/kubectl-apply.1 index c739dc1402a..564b80bac40 100644 --- a/docs/man/man1/kubectl-apply.1 +++ b/docs/man/man1/kubectl-apply.1 @@ -14,6 +14,7 @@ kubectl apply \- Apply a configuration to a resource by filename or stdin .SH DESCRIPTION .PP Apply a configuration to a resource by filename or stdin. +The resource will be created if it doesn't exist yet. .PP JSON and YAML formats are accepted. diff --git a/docs/user-guide/kubectl/kubectl_apply.md b/docs/user-guide/kubectl/kubectl_apply.md index 7125783e2de..f6e01170f34 100644 --- a/docs/user-guide/kubectl/kubectl_apply.md +++ b/docs/user-guide/kubectl/kubectl_apply.md @@ -39,6 +39,7 @@ Apply a configuration to a resource by filename or stdin Apply a configuration to a resource by filename or stdin. +The resource will be created if it doesn't exist yet. JSON and YAML formats are accepted. @@ -97,7 +98,7 @@ $ cat pod.json | kubectl apply -f - * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-10-01 05:36:57.66914652 +0000 UTC +###### Auto generated by spf13/cobra on 4-Nov-2015 [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_apply.md?pixel)]() diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 29c02b02c01..271c9822ab8 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -635,6 +635,18 @@ runTests() { # Clean up kubectl delete rc,hpa frontend + ## kubectl apply should create the resource that doesn't exist yet + # Pre-Condition: no POD is running + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' + # Command: apply a pod "test-pod" (doesn't exist) should create this pod + kubectl apply -f hack/testdata/pod.yaml "${kube_flags[@]}" + # Post-Condition: pod "test-pod" is running + kube::test::get_object_assert 'pods test-pod' "{{${labels_field}.name}}" 'test-pod-label' + # Post-Condition: pod "test-pod" has configuration annotation + [[ "$(kubectl get pods test-pod -o yaml "${kube_flags[@]}" | grep kubectl.kubernetes.io/last-applied-configuration)" ]] + # Clean up + kubectl delete pods test-pod "${kube_flags[@]}" + ############## # Namespaces # ############## diff --git a/pkg/kubectl/cmd/apply.go b/pkg/kubectl/cmd/apply.go index b9da2105208..da3f68b1f29 100644 --- a/pkg/kubectl/cmd/apply.go +++ b/pkg/kubectl/cmd/apply.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/kubectl" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" @@ -37,6 +38,7 @@ type ApplyOptions struct { const ( apply_long = `Apply a configuration to a resource by filename or stdin. +The resource will be created if it doesn't exist yet. JSON and YAML formats are accepted.` apply_example = `# Apply the configuration in pod.json to a pod. @@ -119,7 +121,21 @@ func RunApply(f *cmdutil.Factory, cmd *cobra.Command, out io.Writer, options *Ap } if err := info.Get(); err != nil { - return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%v\nfrom server for:", info), info.Source, err) + if !errors.IsNotFound(err) { + return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%v\nfrom server for:", info), info.Source, err) + } + // Create the resource if it doesn't exist + // First, update the annotation used by kubectl apply + if err := kubectl.CreateApplyAnnotation(info); err != nil { + return cmdutil.AddSourceToErr("creating", info.Source, err) + } + // Then create the resource and skip the three-way merge + if err := createAndRefresh(info); err != nil { + return cmdutil.AddSourceToErr("creating", info.Source, err) + } + count++ + cmdutil.PrintSuccess(mapper, shortOutput, out, info.Mapping.Resource, info.Name, "created") + return nil } // Serialize the current configuration of the object from the server. diff --git a/pkg/kubectl/cmd/apply_test.go b/pkg/kubectl/cmd/apply_test.go index 505a3240005..517b93bf7e8 100644 --- a/pkg/kubectl/cmd/apply_test.go +++ b/pkg/kubectl/cmd/apply_test.go @@ -28,6 +28,7 @@ import ( "github.com/spf13/cobra" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/client/unversioned/fake" "k8s.io/kubernetes/pkg/kubectl" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" @@ -207,6 +208,43 @@ func TestApplyObject(t *testing.T) { } } +func TestApplyNonExistObject(t *testing.T) { + nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) + pathRC := "/namespaces/test/replicationcontrollers" + pathNameRC := pathRC + "/" + 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 == pathNameRC && m == "GET": + return &http.Response{StatusCode: 404}, errors.NewNotFound("ReplicationController", "") + case p == pathRC && m == "POST": + bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC)) + return &http.Response{StatusCode: 201, 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.Errorf("unexpected output: %s\nexpected: %s", buf.String(), expectRC) + } +} + func TestApplyMultipleObject(t *testing.T) { nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) pathRC := "/namespaces/test/replicationcontrollers/" + nameRC diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index fb4677fb36b..483ba70fedd 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -111,13 +111,11 @@ func RunCreate(f *cmdutil.Factory, cmd *cobra.Command, out io.Writer, options *C return cmdutil.AddSourceToErr("creating", info.Source, err) } - obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object) - if err != nil { + if err := createAndRefresh(info); err != nil { return cmdutil.AddSourceToErr("creating", info.Source, err) } count++ - info.Refresh(obj, true) shortOutput := cmdutil.GetFlagString(cmd, "output") == "name" if !shortOutput { printObjectSpecificMessage(info.Object, out) @@ -164,3 +162,13 @@ func makePortsString(ports []api.ServicePort, useNodePort bool) string { } return strings.Join(pieces, ",") } + +// createAndRefresh creates an object from input info and refreshes info with that object +func createAndRefresh(info *resource.Info) error { + obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object) + if err != nil { + return err + } + info.Refresh(obj, true) + return nil +}