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.
pull/6/head
Clayton Coleman 2015-10-06 11:31:48 -04:00
parent e929baf91a
commit e9a465d635
9 changed files with 120 additions and 6 deletions

View File

@ -753,6 +753,7 @@ _kubectl_run()
flags+=("--image=") flags+=("--image=")
flags+=("--labels=") flags+=("--labels=")
two_word_flags+=("-l") two_word_flags+=("-l")
flags+=("--leave-stdin-open")
flags+=("--limits=") flags+=("--limits=")
flags+=("--no-headers") flags+=("--no-headers")
flags+=("--output=") flags+=("--output=")

View File

@ -50,6 +50,10 @@ Creates a replication controller to manage the created container(s).
\fB\-l\fP, \fB\-\-labels\fP="" \fB\-l\fP, \fB\-\-labels\fP=""
Labels to apply to the pod(s). 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 .PP
\fB\-\-limits\fP="" \fB\-\-limits\fP=""
The resource requirement limits for this container. For example, 'cpu=200m,memory=512Mi' The resource requirement limits for this container. For example, 'cpu=200m,memory=512Mi'

View File

@ -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. --hostport=-1: The host port mapping for the container port. To demonstrate a single-machine container.
--image="": The image for the container to run. --image="": The image for the container to run.
-l, --labels="": Labels to apply to the pod(s). -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' --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. --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]. -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].

View File

@ -148,6 +148,7 @@ kubelet-timeout
kube-master kube-master
label-columns label-columns
last-release-pr last-release-pr
leave-stdin-open
limit-bytes limit-bytes
load-balancer-ip load-balancer-ip
log-flush-frequency log-flush-frequency

View File

@ -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().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("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("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().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().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'") 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 return err
} }
if restartPolicy != api.RestartPolicyAlways && replicas != 1 { 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") generatorName := cmdutil.GetFlagString(cmd, "generator")
if len(generatorName) == 0 { if len(generatorName) == 0 {

View File

@ -267,6 +267,7 @@ func (BasicPod) ParamNames() []GeneratorParam {
{"port", false}, {"port", false},
{"hostport", false}, {"hostport", false},
{"stdin", false}, {"stdin", false},
{"leave-stdin-open", false},
{"tty", false}, {"tty", false},
{"restart", false}, {"restart", false},
{"command", false}, {"command", false},
@ -333,6 +334,10 @@ func (BasicPod) Generate(genericParams map[string]interface{}) (runtime.Object,
if err != nil { if err != nil {
return nil, err return nil, err
} }
leaveStdinOpen, err := GetBool(params, "leave-stdin-open", false)
if err != nil {
return nil, err
}
tty, err := GetBool(params, "tty", false) tty, err := GetBool(params, "tty", false)
if err != nil { if err != nil {
@ -360,6 +365,7 @@ func (BasicPod) Generate(genericParams map[string]interface{}) (runtime.Object,
Image: params["image"], Image: params["image"],
ImagePullPolicy: api.PullIfNotPresent, ImagePullPolicy: api.PullIfNotPresent,
Stdin: stdin, Stdin: stdin,
StdinOnce: !leaveStdinOpen && stdin,
TTY: tty, TTY: tty,
Resources: resourceRequirements, Resources: resourceRequirements,
}, },

View File

@ -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{} generator := BasicPod{}
for _, test := range tests { for _, test := range tests {

View File

@ -360,11 +360,49 @@ var _ = Describe("Kubectl client", func() {
}) })
It("should support inline execution and attach", func() { It("should support inline execution and attach", func() {
By("executing a command with run and attach") nsFlag := fmt.Sprintf("--namespace=%v", ns)
runOutput := runKubectl(fmt.Sprintf("--namespace=%v", ns), "run", "run-test", "--image=busybox", "--restart=Never", "--attach=true", "echo", "running", "in", "container")
expectedRunOutput := "running in container" By("executing a command with run and attach with stdin")
Expect(runOutput).To(ContainSubstring(expectedRunOutput)) runOutput := newKubectlCommand(nsFlag, "run", "run-test", "--image=busybox", "--restart=Never", "--attach=true", "--stdin", "--", "sh", "-c", "cat && echo 'stdin closed'").
// everything in the ns will be deleted at the end of the test 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() { It("should support port-forward", func() {

View File

@ -1061,6 +1061,11 @@ func runKubectl(args ...string) string {
return newKubectlCommand(args...).exec() 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) { func startCmdAndStreamOutput(cmd *exec.Cmd) (stdout, stderr io.ReadCloser, err error) {
stdout, err = cmd.StdoutPipe() stdout, err = cmd.StdoutPipe()
if err != nil { if err != nil {