Skip to content

Commit

Permalink
Make kubectl run attach behave like docker run
Browse files Browse the repository at this point in the history
Have stdin closed by default, can be left open with --leave-stdin-open.
Add e2e tests for the behavior.
  • Loading branch information
smarterclayton committed Oct 17, 2015
1 parent e929baf commit e9a465d
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 6 deletions.
1 change: 1 addition & 0 deletions contrib/completions/bash/kubectl
Original file line number Diff line number Diff line change
Expand Up @@ -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=")
Expand Down
4 changes: 4 additions & 0 deletions docs/man/man1/kubectl-run.1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions docs/user-guide/kubectl/kubectl_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ $ kubectl run nginx --image=nginx --command -- <cmd> <arg1> ... <argN>
--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].
Expand Down
1 change: 1 addition & 0 deletions hack/verify-flags/known-flags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ kubelet-timeout
kube-master
label-columns
last-release-pr
leave-stdin-open
limit-bytes
load-balancer-ip
log-flush-frequency
Expand Down
3 changes: 2 additions & 1 deletion pkg/kubectl/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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'")
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions pkg/kubectl/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ func (BasicPod) ParamNames() []GeneratorParam {
{"port", false},
{"hostport", false},
{"stdin", false},
{"leave-stdin-open", false},
{"tty", false},
{"restart", false},
{"command", false},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
},
Expand Down
57 changes: 57 additions & 0 deletions pkg/kubectl/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
48 changes: 43 additions & 5 deletions test/e2e/kubectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
5 changes: 5 additions & 0 deletions test/e2e/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit e9a465d

Please sign in to comment.