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 frame; returns Status | None |
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.