mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-18 04:54:54 +00:00
Add --detach-keys for kubectl attach command
This commit is contained in:
@@ -66,6 +66,8 @@ var (
|
||||
const (
|
||||
defaultPodAttachTimeout = 60 * time.Second
|
||||
defaultPodLogsTimeout = 20 * time.Second
|
||||
|
||||
defaultDetachSequence = "ctrl-p,ctrl-q"
|
||||
)
|
||||
|
||||
// AttachOptions declare the arguments accepted by the Attach command
|
||||
@@ -75,6 +77,8 @@ type AttachOptions struct {
|
||||
// whether to disable use of standard error when streaming output from tty
|
||||
DisableStderr bool
|
||||
|
||||
DetachKeys string
|
||||
|
||||
CommandName string
|
||||
|
||||
Pod *corev1.Pod
|
||||
@@ -122,6 +126,7 @@ func NewCmdAttach(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.
|
||||
cmd.Flags().BoolVarP(&o.Stdin, "stdin", "i", o.Stdin, "Pass stdin to the container")
|
||||
cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, "Stdin is a TTY")
|
||||
cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, "Only print output from the remote session")
|
||||
cmd.Flags().StringVar(&o.DetachKeys, "detach-keys", defaultDetachSequence, "Override the key sequence for detaching a container")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -149,8 +154,17 @@ func DefaultAttachFunc(o *AttachOptions, containerToAttach *corev1.Container, ra
|
||||
Stderr: !o.DisableStderr,
|
||||
TTY: raw,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
return o.Attach.Attach(req.URL(), o.Config, o.In, o.Out, o.ErrOut, raw, sizeQueue)
|
||||
stdin := o.In
|
||||
if o.Stdin && raw {
|
||||
if o.DetachKeys == "" {
|
||||
o.DetachKeys = defaultDetachSequence
|
||||
}
|
||||
stdin, err = term.NewDetachableReader(stdin, o.DetachKeys)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not bind detach keys: %w", err)
|
||||
}
|
||||
}
|
||||
return o.Attach.Attach(req.URL(), o.Config, stdin, o.Out, o.ErrOut, raw, sizeQueue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,13 +366,13 @@ func (o *AttachOptions) GetContainerName(pod *corev1.Pod) (string, error) {
|
||||
// reattachMessage returns a message to print after attach has completed, or
|
||||
// the empty string if no message should be printed.
|
||||
func (o *AttachOptions) reattachMessage(containerName string, rawTTY bool) string {
|
||||
if o.Quiet || !o.Stdin || !rawTTY || o.Pod.Spec.RestartPolicy != corev1.RestartPolicyAlways {
|
||||
if o.Quiet || !o.Stdin || !rawTTY {
|
||||
return ""
|
||||
}
|
||||
if _, path := podcmd.FindContainerByName(o.Pod, containerName); strings.HasPrefix(path, "spec.ephemeralContainers") {
|
||||
return fmt.Sprintf("Session ended, the ephemeral container will not be restarted but may be reattached using '%s %s -c %s -n %s -i -t' if it is still running", o.CommandName, o.Pod.Name, containerName, o.Pod.Namespace)
|
||||
}
|
||||
return fmt.Sprintf("Session ended, resume using '%s %s -c %s -n %s -i -t' command when the pod is running", o.CommandName, o.Pod.Name, containerName, o.Pod.Namespace)
|
||||
return fmt.Sprintf("Session ended, resume using '%s %s -c %s -n %s -i -t' command", o.CommandName, o.Pod.Name, containerName, o.Pod.Namespace)
|
||||
}
|
||||
|
||||
type terminalSizeQueueAdapter struct {
|
||||
|
||||
@@ -510,7 +510,7 @@ func TestReattachMessage(t *testing.T) {
|
||||
container: "bar",
|
||||
rawTTY: true,
|
||||
stdin: true,
|
||||
expected: "Session ended, resume using 'kubectl foo -c bar -n test -i -t' command when the pod is running",
|
||||
expected: "Session ended, resume using 'kubectl foo -c bar -n test -i -t' command",
|
||||
},
|
||||
{
|
||||
name: "no stdin",
|
||||
@@ -541,7 +541,7 @@ func TestReattachMessage(t *testing.T) {
|
||||
container: "bar",
|
||||
rawTTY: true,
|
||||
stdin: true,
|
||||
expected: "",
|
||||
expected: "Session ended, resume using 'kubectl foo -c bar -n test -i -t' command",
|
||||
},
|
||||
{
|
||||
name: "ephemeral container",
|
||||
|
||||
@@ -92,6 +92,8 @@ var (
|
||||
|
||||
const (
|
||||
defaultPodAttachTimeout = 60 * time.Second
|
||||
|
||||
defaultDetachSequence = "ctrl-p,ctrl-q"
|
||||
)
|
||||
|
||||
var metadataAccessor = meta.NewAccessor()
|
||||
@@ -117,6 +119,7 @@ type RunOptions struct {
|
||||
|
||||
ArgsLenAtDash int
|
||||
Attach bool
|
||||
DetachKeys string
|
||||
Expose bool
|
||||
Image string
|
||||
Interactive bool
|
||||
@@ -190,6 +193,7 @@ func addRunFlags(cmd *cobra.Command, opt *RunOptions) {
|
||||
cmd.Flags().BoolVar(&opt.Expose, "expose", opt.Expose, "If true, create a ClusterIP service associated with the pod. Requires `--port`.")
|
||||
cmd.Flags().BoolVarP(&opt.Quiet, "quiet", "q", opt.Quiet, "If true, suppress prompt messages.")
|
||||
cmd.Flags().BoolVar(&opt.Privileged, "privileged", opt.Privileged, i18n.T("If true, run the container in privileged mode."))
|
||||
cmd.Flags().StringVar(&opt.DetachKeys, "detach-keys", defaultDetachSequence, "Override the key sequence for detaching a container.")
|
||||
cmdutil.AddFieldManagerFlagVar(cmd, &opt.fieldManager, "kubectl-run")
|
||||
opt.AddOverrideFlags(cmd)
|
||||
}
|
||||
@@ -340,6 +344,8 @@ func (o *RunOptions) Run(f cmdutil.Factory, cmd *cobra.Command, args []string) e
|
||||
CommandName: cmd.Parent().CommandPath() + " attach",
|
||||
|
||||
Attach: &attach.DefaultRemoteAttach{},
|
||||
|
||||
DetachKeys: o.DetachKeys,
|
||||
}
|
||||
config, err := f.ToRESTConfig()
|
||||
if err != nil {
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
package term
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
@@ -113,3 +114,27 @@ func (t TTY) Safe(fn SafeFunc) error {
|
||||
term.RestoreTerminal(inFd, state)
|
||||
}).Run(fn)
|
||||
}
|
||||
|
||||
type detachableReader struct {
|
||||
escapeProxy io.Reader
|
||||
}
|
||||
|
||||
func NewDetachableReader(r io.Reader, detachKeys string) (io.Reader, error) {
|
||||
detachKeyBytes, err := term.ToBytes(detachKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &detachableReader{
|
||||
escapeProxy: term.NewEscapeProxy(r, detachKeyBytes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *detachableReader) Read(p []byte) (n int, err error) {
|
||||
n, err = r.escapeProxy.Read(p)
|
||||
if errors.Is(err, term.EscapeError{}) {
|
||||
// EscapeError is expected when the user types the detach key sequence, so we should not treat it as an error.
|
||||
// Instead, we return io.EOF so that the attach session will end gracefully.
|
||||
err = io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
126
staging/src/k8s.io/kubectl/pkg/util/term/term_test.go
Normal file
126
staging/src/k8s.io/kubectl/pkg/util/term/term_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package term
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// errorReader is a helper that always returns a fixed error.
|
||||
type errorReader struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *errorReader) Read(p []byte) (n int, err error) {
|
||||
return 0, e.err
|
||||
}
|
||||
|
||||
func TestDetachableReader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
detachKeys string
|
||||
input string
|
||||
bufSize int
|
||||
wantN int
|
||||
wantErr error
|
||||
wantData string
|
||||
}{
|
||||
{
|
||||
name: "normal read without detach sequence",
|
||||
detachKeys: "ctrl-p,ctrl-q",
|
||||
input: "hello world this is a test",
|
||||
bufSize: 20,
|
||||
wantN: 20,
|
||||
wantErr: nil,
|
||||
wantData: "hello world this is ",
|
||||
},
|
||||
{
|
||||
name: "detach sequence triggers EOF",
|
||||
detachKeys: "ctrl-p,ctrl-q",
|
||||
input: "data before\x10\x11data after",
|
||||
bufSize: 30,
|
||||
wantN: 11,
|
||||
wantErr: io.EOF,
|
||||
wantData: "data before",
|
||||
},
|
||||
{
|
||||
name: "partial detach sequence is treated as normal data",
|
||||
detachKeys: "ctrl-p,ctrl-q",
|
||||
input: "normal text\x10only ctrl-p no q",
|
||||
bufSize: 30,
|
||||
wantN: 28,
|
||||
wantErr: nil,
|
||||
wantData: "normal text\x10only ctrl-p no q",
|
||||
},
|
||||
{
|
||||
name: "underlying error is passed through unchanged",
|
||||
detachKeys: "ctrl-a",
|
||||
input: "", // dummy, error comes from mock
|
||||
bufSize: 10,
|
||||
wantN: 0,
|
||||
wantErr: errors.New("simulated io error"),
|
||||
wantData: "",
|
||||
},
|
||||
{
|
||||
name: "detach sequence exactly at buffer boundary",
|
||||
detachKeys: "ctrl-a",
|
||||
input: "short\x01rest",
|
||||
bufSize: 6,
|
||||
wantN: 5,
|
||||
wantErr: io.EOF,
|
||||
wantData: "short",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var r io.Reader
|
||||
var err error
|
||||
|
||||
if tt.wantErr != nil && !errors.Is(tt.wantErr, io.EOF) {
|
||||
// Special case: simulate underlying read error
|
||||
r = &errorReader{err: tt.wantErr}
|
||||
} else {
|
||||
r, err = NewDetachableReader(strings.NewReader(tt.input), tt.detachKeys)
|
||||
if err != nil {
|
||||
t.Fatalf("NewDetachableReader failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
buf := make([]byte, tt.bufSize)
|
||||
n, readErr := r.Read(buf)
|
||||
|
||||
if n != tt.wantN {
|
||||
t.Errorf("Read() got n = %d, want %d", n, tt.wantN)
|
||||
}
|
||||
|
||||
if !errors.Is(readErr, tt.wantErr) {
|
||||
t.Errorf("Read() got err = %v, want %v", readErr, tt.wantErr)
|
||||
}
|
||||
|
||||
if tt.wantN > 0 {
|
||||
got := string(buf[:tt.wantN])
|
||||
if got != tt.wantData {
|
||||
t.Errorf("Read() got data = %q, want %q", got, tt.wantData)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user