Improve terminal reuse and attach

Currently attach and the editor do not share the same logic for saving
and restoring the terminal, and are not suitable for nesting (when the
caller wants to create something, attach, and then delete something when
the attach is over).  This commit moves the interrupt protection logic
to a util package and supports nesting interrupt handlers.
This commit is contained in:
Clayton Coleman
2016-02-20 13:53:11 -05:00
parent 4d98abf26c
commit 3a29e3971b
7 changed files with 242 additions and 106 deletions

View File

@@ -20,19 +20,18 @@ import (
"fmt"
"io"
"net/url"
"os"
"os/signal"
"syscall"
"github.com/docker/docker/pkg/term"
"github.com/golang/glog"
"github.com/spf13/cobra"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/restclient"
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/remotecommand"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
utilerrors "k8s.io/kubernetes/pkg/util/errors"
"k8s.io/kubernetes/pkg/util/interrupt"
"k8s.io/kubernetes/pkg/util/term"
)
const (
@@ -53,6 +52,8 @@ func NewCmdAttach(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer)
Out: cmdOut,
Err: cmdErr,
CommandName: "kubectl attach",
Attach: &DefaultRemoteAttach{},
}
cmd := &cobra.Command{
@@ -96,11 +97,17 @@ type AttachOptions struct {
ContainerName string
Stdin bool
TTY bool
CommandName string
// InterruptParent, if set, is used to handle interrupts while attached
InterruptParent *interrupt.Handler
In io.Reader
Out io.Writer
Err io.Writer
Pod *api.Pod
Attach RemoteAttach
Client *client.Client
Config *restclient.Config
@@ -154,80 +161,65 @@ func (p *AttachOptions) Validate() error {
// Run executes a validated remote execution against a pod.
func (p *AttachOptions) Run() error {
pod, err := p.Client.Pods(p.Namespace).Get(p.PodName)
if err != nil {
return err
if p.Pod == nil {
pod, err := p.Client.Pods(p.Namespace).Get(p.PodName)
if err != nil {
return err
}
if pod.Status.Phase != api.PodRunning {
return fmt.Errorf("pod %s is not running and cannot be attached to; current phase is %s", p.PodName, pod.Status.Phase)
}
p.Pod = pod
// TODO: convert this to a clean "wait" behavior
}
pod := p.Pod
if pod.Status.Phase != api.PodRunning {
return fmt.Errorf("pod %s is not running and cannot be attached to; current phase is %s", p.PodName, pod.Status.Phase)
}
// ensure we can recover the terminal while attached
t := term.TTY{Parent: p.InterruptParent}
var stdin io.Reader
// check for TTY
tty := p.TTY
containerToAttach := p.GetContainer(pod)
if tty && !containerToAttach.TTY {
tty = false
fmt.Fprintf(p.Err, "Unable to use a TTY - container %s doesn't allocate one\n", containerToAttach.Name)
fmt.Fprintf(p.Err, "Unable to use a TTY - container %s did not allocate one\n", containerToAttach.Name)
}
// TODO: refactor with terminal helpers from the edit utility once that is merged
if p.Stdin {
stdin = p.In
if tty {
if file, ok := stdin.(*os.File); ok {
inFd := file.Fd()
if term.IsTerminal(inFd) {
oldState, err := term.SetRawTerminal(inFd)
if err != nil {
glog.Fatal(err)
}
fmt.Fprintln(p.Out, "\nHit enter for command prompt")
// this handles a clean exit, where the command finished
defer term.RestoreTerminal(inFd, oldState)
// SIGINT is handled by term.SetRawTerminal (it runs a goroutine that listens
// for SIGINT and restores the terminal before exiting)
// this handles SIGTERM
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM)
go func() {
<-sigChan
term.RestoreTerminal(inFd, oldState)
os.Exit(0)
}()
} else {
fmt.Fprintln(p.Err, "STDIN is not a terminal")
}
} else {
tty = false
fmt.Fprintln(p.Err, "Unable to use a TTY - input is not the right kind of file")
}
t.In = p.In
if tty && !t.IsTerminal() {
tty = false
fmt.Fprintln(p.Err, "Unable to use a TTY - input is not a terminal or the right kind of file")
}
}
t.Raw = tty
// TODO: consider abstracting into a client invocation or client helper
req := p.Client.RESTClient.Post().
Resource("pods").
Name(pod.Name).
Namespace(pod.Namespace).
SubResource("attach")
req.VersionedParams(&api.PodAttachOptions{
Container: containerToAttach.Name,
Stdin: stdin != nil,
Stdout: p.Out != nil,
Stderr: p.Err != nil,
TTY: tty,
}, api.ParameterCodec)
fn := func() error {
if tty {
fmt.Fprintln(p.Out, "\nHit enter for command prompt")
}
// TODO: consider abstracting into a client invocation or client helper
req := p.Client.RESTClient.Post().
Resource("pods").
Name(pod.Name).
Namespace(pod.Namespace).
SubResource("attach")
req.VersionedParams(&api.PodAttachOptions{
Container: containerToAttach.Name,
Stdin: p.In != nil,
Stdout: p.Out != nil,
Stderr: p.Err != nil,
TTY: tty,
}, api.ParameterCodec)
err = p.Attach.Attach("POST", req.URL(), p.Config, stdin, p.Out, p.Err, tty)
if err != nil {
return p.Attach.Attach("POST", req.URL(), p.Config, p.In, p.Out, p.Err, tty)
}
if err := t.Safe(fn); err != nil {
return err
}
if p.Stdin && tty && pod.Spec.RestartPolicy == api.RestartPolicyAlways {
fmt.Fprintf(p.Out, "Session ended, resume using 'kubectl attach %s -c %s -i -t' command when the pod is running\n", pod.Name, containerToAttach.Name)
fmt.Fprintf(p.Out, "Session ended, resume using '%s %s -c %s -i -t' command when the pod is running\n", p.CommandName, pod.Name, containerToAttach.Name)
}
return nil
}