Attach¶
The attach subresource opens a WebSocket connection to the kubelet and attaches to a running container process — without starting a new command.
Beta / experimental
The attach WebSocket implementation is functional and tested against K3S, but the underlying channel-protocol layer is relatively new. The API may change between minor releases. Requires Kubernetes ≥ 1.30 (v5 channel protocol).
Exec vs Attach¶
exec |
attach |
|
|---|---|---|
| Starts a new command | yes | no |
| Connects to | new process | existing container entrypoint |
run() (one-shot) |
yes | no |
stream() (interactive) |
yes | yes |
Use attach when you want to connect to a container that was started with stdin: true in its spec — for example, an interactive REPL, a legacy daemon that reads from stdin, or a container designed for interactive debugging. Use exec to run a one-off command.
Installation requirement¶
Attach uses a WebSocket upgrade. You need one of:
kubex[httpx-ws]— httpx client with thehttpx-wsWebSocket extensionkubex[aiohttp]— aiohttp client with built-in WebSocket support
Availability¶
Only resources that implement the HasAttach marker interface expose api.attach. In practice this means Pod. Accessing api.attach on any other resource type raises NotImplementedError at runtime and resolves to SubresourceNotAvailable for the type-checker.
Container requirement¶
The target container must have been created with stdin: true in its spec. If the container was not configured to accept stdin, attaching will succeed at the protocol level but your writes will be discarded.
Attaching to a container¶
api.attach.stream() is an async context manager that returns a StreamSession — the same type used by api.exec.stream().
from typing import cast
import anyio
from kubex.api import Api
from kubex.client import create_client
from kubex.k8s.v1_35.core.v1.container import Container
from kubex.k8s.v1_35.core.v1.pod import Pod
from kubex.k8s.v1_35.core.v1.pod_spec import PodSpec
from kubex_core.models.metadata import ObjectMetadata
async def main() -> None:
client = await create_client()
async with client:
api: Api[Pod] = Api(Pod, client=client, namespace="default")
pod = await api.create(
Pod(
metadata=ObjectMetadata(generate_name="example-attach-"),
spec=PodSpec(
containers=[
Container(
name="main",
image="busybox:1.36",
command=[
"sh",
"-c",
'while IFS= read -r line; do printf "echo: %s\\n" "$line"; done',
],
stdin=True,
)
]
),
),
)
pod_name = cast(str, pod.metadata.name)
try:
async with api.attach.stream(
pod_name,
stdin=True,
stdout=True,
) as session:
await session.stdin.write(b"hello\n")
buf = bytearray()
with anyio.fail_after(10):
async for chunk in session.stdout:
buf.extend(chunk)
if b"echo: hello" in buf:
break
print("attach output:", bytes(buf).decode(errors="replace"))
await session.close_stdin()
finally:
await api.delete(pod_name)
StreamSession API¶
api.attach.stream() returns the same StreamSession as api.exec.stream():
| Member | Type | Description |
|---|---|---|
stdin |
writer | Call await session.stdin.write(data) to send bytes to the container |
stdout |
MemoryObjectReceiveStream[bytes] |
Async iterable yielding stdout chunks |
stderr |
MemoryObjectReceiveStream[bytes] |
Async iterable yielding stderr chunks |
resize(width, height) |
coroutine | Send a terminal resize event |
close_stdin() |
coroutine | Half-close the stdin channel (idempotent) |
wait_for_status() |
coroutine | Await the final status; returns Status | None (None if the server closed without one) |
Output is never dropped: stdout and stderr each queue up to 128 frames, and when a queue is
full kubex stops reading the WebSocket until you consume more. Since both channels share one
connection, a channel you don't read stalls the other — read every enabled channel
concurrently, disable it (stderr=False), or discard it with session.stderr.close(). See
exec: reading output for details. Large stdin.write()
payloads are sent in frames of at most 32 KiB, and writing after the session has ended raises
KubexClientException. stdout_truncated/stderr_truncated are deprecated and always False.
TTY mode and stderr¶
When tty=True, the kubelet merges stderr into stdout. session.stderr closes immediately. Read only session.stdout when tty=True.
stream() options¶
| Option | Type | Description |
|---|---|---|
stdin |
bool |
Whether to attach to the stdin channel |
stdout |
bool |
Whether to attach to the stdout channel (default True) |
stderr |
bool |
Whether to attach to the stderr channel |
tty |
bool |
Whether the container stdin is a TTY |
container |
str | None |
Container name — required for multi-container pods |
namespace |
str | None | ... |
Override the Api instance namespace |
request_timeout |
Timeout | float | None | ... |
Override the client-level timeout |
Exiting early¶
Exiting the async with block cancels the read loop before the WebSocket closes. You can break out of the attach session at any point without deadlocking.