diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 2d707aebcae..cdd77d3f467 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 033437b3f91..5e7cfc6177f 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 993b70c57bd..768f1987344 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 ef5d4ee24a1..6d21755020c 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 a4a3b018269..ec04c14cf45 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 9add392bf45..31b82bae339 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 e16f3edd329..d6eab99d35d 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 8f821f2512c..677ca78de8b 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 625314bae95..4bc8e78e794 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 {