From e9a465d6355447246bcfc1d63b142cceef93cc7c Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Tue, 6 Oct 2015 11:31:48 -0400 Subject: [PATCH] Make kubectl run attach behave like docker run Have stdin closed by default, can be left open with --leave-stdin-open. Add e2e tests for the behavior. --- contrib/completions/bash/kubectl | 1 + docs/man/man1/kubectl-run.1 | 4 ++ docs/user-guide/kubectl/kubectl_run.md | 1 + hack/verify-flags/known-flags.txt | 1 + pkg/kubectl/cmd/run.go | 3 +- pkg/kubectl/run.go | 6 +++ pkg/kubectl/run_test.go | 57 ++++++++++++++++++++++++++ test/e2e/kubectl.go | 48 +++++++++++++++++++--- test/e2e/util.go | 5 +++ 9 files changed, 120 insertions(+), 6 deletions(-) diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 2d707aebcaec5..cdd77d3f467c5 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -753,6 +753,7 @@ _kubectl_run() flags+=("--image=") flags+=("--labels=") two_word_flags+=("-l") + flags+=("--leave-stdin-open") flags+=("--limits=") flags+=("--no-headers") flags+=("--output=") diff --git a/docs/man/man1/kubectl-run.1 b/docs/man/man1/kubectl-run.1 index 033437b3f91cc..5e7cfc6177fdf 100644 --- a/docs/man/man1/kubectl-run.1 +++ b/docs/man/man1/kubectl-run.1 @@ -50,6 +50,10 @@ Creates a replication controller to manage the created container(s). \fB\-l\fP, \fB\-\-labels\fP="" Labels to apply to the pod(s). +.PP +\fB\-\-leave\-stdin\-open\fP=false + If the pod is started in interactive mode or with stdin, leave stdin open after the first attach completes. By default, stdin will be closed after the first attach completes. + .PP \fB\-\-limits\fP="" The resource requirement limits for this container. For example, 'cpu=200m,memory=512Mi' diff --git a/docs/user-guide/kubectl/kubectl_run.md b/docs/user-guide/kubectl/kubectl_run.md index 993b70c57bd56..768f198734437 100644 --- a/docs/user-guide/kubectl/kubectl_run.md +++ b/docs/user-guide/kubectl/kubectl_run.md @@ -87,6 +87,7 @@ $ kubectl run nginx --image=nginx --command -- ... --hostport=-1: The host port mapping for the container port. To demonstrate a single-machine container. --image="": The image for the container to run. -l, --labels="": Labels to apply to the pod(s). + --leave-stdin-open[=false]: If the pod is started in interactive mode or with stdin, leave stdin open after the first attach completes. By default, stdin will be closed after the first attach completes. --limits="": The resource requirement limits for this container. For example, 'cpu=200m,memory=512Mi' --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]. diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index ef5d4ee24a12a..6d21755020cc1 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -148,6 +148,7 @@ kubelet-timeout kube-master label-columns last-release-pr +leave-stdin-open limit-bytes load-balancer-ip log-flush-frequency diff --git a/pkg/kubectl/cmd/run.go b/pkg/kubectl/cmd/run.go index a4a3b01826929..ec04c14cf45cd 100644 --- a/pkg/kubectl/cmd/run.go +++ b/pkg/kubectl/cmd/run.go @@ -90,6 +90,7 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c cmd.Flags().BoolP("stdin", "i", false, "Keep stdin open on the container(s) in the pod, even if nothing is attached.") cmd.Flags().Bool("tty", false, "Allocated a TTY for each container in the pod. Because -t is currently shorthand for --template, -t is not supported for --tty. This shorthand is deprecated and we expect to adopt -t for --tty soon.") cmd.Flags().Bool("attach", false, "If true, wait for the Pod to start running, and then attach to the Pod as if 'kubectl attach ...' were called. Default false, unless '-i/--interactive' is set, in which case the default is true.") + cmd.Flags().Bool("leave-stdin-open", false, "If the pod is started in interactive mode or with stdin, leave stdin open after the first attach completes. By default, stdin will be closed after the first attach completes.") cmd.Flags().String("restart", "Always", "The restart policy for this Pod. Legal values [Always, OnFailure, Never]. If set to 'Always' a replication controller is created for this pod, if set to OnFailure or Never, only the Pod is created and --replicas must be 1. Default 'Always'") cmd.Flags().Bool("command", false, "If true and extra arguments are present, use them as the 'command' field in the container, rather than the 'args' field which is the default.") cmd.Flags().String("requests", "", "The resource requirement requests for this container. For example, 'cpu=100m,memory=256Mi'") @@ -126,7 +127,7 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob return err } if restartPolicy != api.RestartPolicyAlways && replicas != 1 { - return cmdutil.UsageError(cmd, fmt.Sprintf("--restart=%s requires that --repliacs=1, found %d", restartPolicy, replicas)) + return cmdutil.UsageError(cmd, fmt.Sprintf("--restart=%s requires that --replicas=1, found %d", restartPolicy, replicas)) } generatorName := cmdutil.GetFlagString(cmd, "generator") if len(generatorName) == 0 { diff --git a/pkg/kubectl/run.go b/pkg/kubectl/run.go index 9add392bf45a5..31b82bae339ca 100644 --- a/pkg/kubectl/run.go +++ b/pkg/kubectl/run.go @@ -267,6 +267,7 @@ func (BasicPod) ParamNames() []GeneratorParam { {"port", false}, {"hostport", false}, {"stdin", false}, + {"leave-stdin-open", false}, {"tty", false}, {"restart", false}, {"command", false}, @@ -333,6 +334,10 @@ func (BasicPod) Generate(genericParams map[string]interface{}) (runtime.Object, if err != nil { return nil, err } + leaveStdinOpen, err := GetBool(params, "leave-stdin-open", false) + if err != nil { + return nil, err + } tty, err := GetBool(params, "tty", false) if err != nil { @@ -360,6 +365,7 @@ func (BasicPod) Generate(genericParams map[string]interface{}) (runtime.Object, Image: params["image"], ImagePullPolicy: api.PullIfNotPresent, Stdin: stdin, + StdinOnce: !leaveStdinOpen && stdin, TTY: tty, Resources: resourceRequirements, }, diff --git a/pkg/kubectl/run_test.go b/pkg/kubectl/run_test.go index e16f3edd329f5..d6eab99d35d5d 100644 --- a/pkg/kubectl/run_test.go +++ b/pkg/kubectl/run_test.go @@ -553,6 +553,63 @@ func TestGeneratePod(t *testing.T) { }, }, }, + { + params: map[string]interface{}{ + "name": "foo", + "image": "someimage", + "replicas": "1", + "labels": "foo=bar,baz=blah", + "stdin": "true", + }, + expected: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"foo": "bar", "baz": "blah"}, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: "foo", + Image: "someimage", + ImagePullPolicy: api.PullIfNotPresent, + Stdin: true, + StdinOnce: true, + }, + }, + DNSPolicy: api.DNSClusterFirst, + RestartPolicy: api.RestartPolicyAlways, + }, + }, + }, + { + params: map[string]interface{}{ + "name": "foo", + "image": "someimage", + "replicas": "1", + "labels": "foo=bar,baz=blah", + "stdin": "true", + "leave-stdin-open": "true", + }, + expected: &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"foo": "bar", "baz": "blah"}, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: "foo", + Image: "someimage", + ImagePullPolicy: api.PullIfNotPresent, + Stdin: true, + StdinOnce: false, + }, + }, + DNSPolicy: api.DNSClusterFirst, + RestartPolicy: api.RestartPolicyAlways, + }, + }, + }, } generator := BasicPod{} for _, test := range tests { diff --git a/test/e2e/kubectl.go b/test/e2e/kubectl.go index 8f821f2512cc2..677ca78de8b69 100644 --- a/test/e2e/kubectl.go +++ b/test/e2e/kubectl.go @@ -360,11 +360,49 @@ var _ = Describe("Kubectl client", func() { }) It("should support inline execution and attach", func() { - By("executing a command with run and attach") - runOutput := runKubectl(fmt.Sprintf("--namespace=%v", ns), "run", "run-test", "--image=busybox", "--restart=Never", "--attach=true", "echo", "running", "in", "container") - expectedRunOutput := "running in container" - Expect(runOutput).To(ContainSubstring(expectedRunOutput)) - // everything in the ns will be deleted at the end of the test + nsFlag := fmt.Sprintf("--namespace=%v", ns) + + By("executing a command with run and attach with stdin") + runOutput := newKubectlCommand(nsFlag, "run", "run-test", "--image=busybox", "--restart=Never", "--attach=true", "--stdin", "--", "sh", "-c", "cat && echo 'stdin closed'"). + withStdinData("abcd1234"). + exec() + Expect(runOutput).To(ContainSubstring("abcd1234")) + Expect(runOutput).To(ContainSubstring("stdin closed")) + Expect(c.Pods(ns).Delete("run-test", api.NewDeleteOptions(0))).To(BeNil()) + + By("executing a command with run and attach without stdin") + runOutput = newKubectlCommand(fmt.Sprintf("--namespace=%v", ns), "run", "run-test-2", "--image=busybox", "--restart=Never", "--attach=true", "--leave-stdin-open=true", "--", "sh", "-c", "cat && echo 'stdin closed'"). + withStdinData("abcd1234"). + exec() + Expect(runOutput).ToNot(ContainSubstring("abcd1234")) + Expect(runOutput).To(ContainSubstring("stdin closed")) + Expect(c.Pods(ns).Delete("run-test-2", api.NewDeleteOptions(0))).To(BeNil()) + + By("executing a command with run and attach with stdin with open stdin should remain running") + runOutput = newKubectlCommand(nsFlag, "run", "run-test-3", "--image=busybox", "--restart=Never", "--attach=true", "--leave-stdin-open=true", "--stdin", "--", "sh", "-c", "cat && echo 'stdin closed'"). + withStdinData("abcd1234\n"). + exec() + Expect(runOutput).ToNot(ContainSubstring("stdin closed")) + if !checkPodsRunningReady(c, ns, []string{"run-test-3"}, time.Minute) { + Failf("Pod %q should still be running", "run-test-3") + } + + // NOTE: we cannot guarantee our output showed up in the container logs before stdin was closed, so we have + // to loop test. + err := wait.PollImmediate(time.Second, time.Minute, func() (bool, error) { + if !checkPodsRunningReady(c, ns, []string{"run-test-3"}, 1*time.Second) { + Failf("Pod %q should still be running", "run-test-3") + } + logOutput := runKubectl(nsFlag, "logs", "run-test-3") + Expect(logOutput).ToNot(ContainSubstring("stdin closed")) + return strings.Contains(logOutput, "abcd1234"), nil + }) + if err != nil { + os.Exit(1) + } + Expect(err).To(BeNil()) + + Expect(c.Pods(ns).Delete("run-test-3", api.NewDeleteOptions(0))).To(BeNil()) }) It("should support port-forward", func() { diff --git a/test/e2e/util.go b/test/e2e/util.go index 625314bae958d..4bc8e78e794c3 100644 --- a/test/e2e/util.go +++ b/test/e2e/util.go @@ -1061,6 +1061,11 @@ func runKubectl(args ...string) string { return newKubectlCommand(args...).exec() } +// runKubectlInput is a convenience wrapper over kubectlBuilder that takes input to stdin +func runKubectlInput(data string, args ...string) string { + return newKubectlCommand(args...).withStdinData(data).exec() +} + func startCmdAndStreamOutput(cmd *exec.Cmd) (stdout, stderr io.ReadCloser, err error) { stdout, err = cmd.StdoutPipe() if err != nil {