Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make kubectl apply create resources if not found #16832

Merged
merged 1 commit into from
Nov 9, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/man/man1/kubectl-apply.1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion docs/user-guide/kubectl/kubectl_apply.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_apply.md?pixel)]()
Expand Down
12 changes: 12 additions & 0 deletions hack/test-cmd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
##############
Expand Down
18 changes: 17 additions & 1 deletion pkg/kubectl/cmd/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
38 changes: 38 additions & 0 deletions pkg/kubectl/cmd/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions pkg/kubectl/cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}