Add --detach-keys for kubectl attach command

This commit is contained in:
杨军10092085
2025-10-31 14:57:46 +08:00
parent b55862975c
commit ee02a8ab32
5 changed files with 177 additions and 6 deletions

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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
}

View 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)
}
}
})
}
}