Skip to main content

Kubernetes Adapter

The Kubernetes adapter enables command execution within existing Kubernetes pods, with port forwarding, log access and file copying built on top of kubectl.

Overview

The Kubernetes adapter (packages/core/src/adapters/kubernetes/index.ts) shells out to kubectl and provides:

  • Pod command execution, including a specific container in a multi-container pod
  • Port forwarding to a pod
  • Log retrieval and streaming
  • File copying to/from a pod (via kubectl cp)
  • Namespace and context targeting

It does not create, patch or delete cluster resources — no pods, Jobs, ConfigMaps, Secrets or Namespaces. Everything here targets a pod that already exists; creating one is out of scope, the same way the SSH adapter doesn't provision the host it connects to.

Basic Usage

import { $ } from '@xec-sh/core';

// Execute in an existing pod — string shorthand or options object
const pod = $.k8s('production/my-app-7d9f8c6b5-x2vjm');
// same as: $.k8s({ pod: 'my-app-7d9f8c6b5-x2vjm', namespace: 'production' })

const files = await pod`ls -la /app`.text();
console.log(files);

// Execute in a specific container of a multi-container pod
const container = $.k8s({
pod: 'multi-container-pod',
container: 'app',
namespace: 'default'
});

await container`cat /etc/config/app.yaml`;

$.k8s(target) returns a K8sExecutionContext with the same chaining API every other target has (.env(), .cd(), .timeout(), .shell(), .retry(), .with(), .pwd(), .which(), .transfer, and so on) — a step written against a K8s target runs the same way it would against SSH, Docker or local.

Pod Configuration

Working with Existing Pods

// Connect to a running pod
const existing = $.k8s({
pod: 'web-server-abc123',
namespace: 'production'
});

// Execute commands
await existing`ps aux`;
for await (const line of existing`tail -f /var/log/app.log`) {
console.log(line);
}

// A specific container
const sidecar = $.k8s({
pod: 'app-pod',
container: 'logging-agent',
namespace: 'monitoring'
});

KubernetesAdapterOptions also accepts context (which cluster context to use — without it, whatever kubectl config current-context happens to be) and kubeconfig (path to a specific kubeconfig file):

const staging = $.k8s({
pod: 'app-pod',
namespace: 'staging',
context: 'staging-cluster'
});

Namespace and Context

There is no namespace-management API (createNamespace, deleteNamespace and similar don't exist) — namespace on a target only selects which namespace kubectl looks in for the pod you name:

// Default namespace
const defaultNs = $.k8s({ pod: 'my-pod' });

// A specific one
const prodNs = $.k8s({ pod: 'app-pod', namespace: 'production' });

Moving Data Between Pods

There's no dedicated cross-namespace copy method; capture output and interpolate it into the next command, the same as you would locally:

const source = $.k8s({ pod: 'source-pod', namespace: 'dev' });
const dest = $.k8s({ pod: 'dest-pod', namespace: 'staging' });

const data = (await source`cat /data/export.json`).stdout;
await dest`cat > /data/import.json <<'XEC_EOF'
${data}
XEC_EOF`;

Port Forwarding

Port forwarding is a method on a pod, reached through .pod(name) — there is no $.k8s.portForward(...); k8s is a method, not a namespace, and forwarding isn't exposed on the context directly:

const pod = $.k8s('production').pod('web-service-7d9f8c6b5-x2vjm');

// Forward a fixed local port to a pod port
const forward = await pod.portForward(8080, 80);

// Access the pod locally
const response = await fetch('http://localhost:8080');

// Close forwarding
await forward.close();
// Let the OS pick a free local port
const dbForward = await $.k8s('data').pod('database-pod').portForwardDynamic(5432);
console.log('Forwarded on port', dbForward.localPort);

const db = await connectDB(`localhost:${dbForward.localPort}`);

K8sPortForward has .localPort, .remotePort, .isOpen and .close(). For more than one port on the same pod, open more than one forward — there is no batch/multi-port form.

Logs

const pod = $.k8s('production').pod('app-pod');

// One-shot retrieval
const logs = await pod.logs({ tail: 100, timestamps: true });

// A specific container's logs
const nginxLogs = await pod.logs({ container: 'nginx' });

// The previous instance of a crashed/restarted container
const crashLogs = await pod.logs({ previous: true, container: 'app' });

Streaming Logs

// Callback form
const stream = await pod.streamLogs((line) => {
console.log('LOG:', line);
}, { follow: true, tail: 100 });

// later
stream.stop();

// Or, equivalently, .follow() — the same thing with follow: true implied
await pod.follow((line) => console.log('LOG:', line));

Logs Across Several Pods

There's no label-based log aggregation call. List matching pods with kubectl directly, then read each one:

const names = (await $`kubectl get pods -n production -l app=web-server -o jsonpath='{.items[*].metadata.name}'`.text())
.split(/\s+/);

for (const name of names) {
const logs = await $.k8s('production').pod(name).logs({ tail: 50 });
console.log(`${name}:\n${logs}`);
}

KubernetesAdapter.getPodFromSelector(selector, namespace?) and .isPodReady(pod, namespace?) also exist and do a single-pod version of this (first match for a label selector, and a readiness check), reachable via ($.getAdapter('kubernetes')) with a cast to KubernetesAdapter, but for anything beyond one pod, kubectl get pods is simpler.

File Operations

const pod = $.k8s('default').pod('app-pod');

// Copy a file (or a directory — kubectl cp handles both) to the pod
await pod.copyTo('/local/config.yaml', '/app/config.yaml');

// Copy from the pod
await pod.copyFrom('/app/logs/error.log', '/local/logs/error.log');

// With a specific container
await pod.copyTo('/local/nginx.conf', '/etc/nginx/nginx.conf', 'nginx');

There is no compression option — copyTo/copyFrom run kubectl cp exactly as it behaves on the command line. These are the pod-native calls; the uniform cross-target surface reaches the same kubectl cp through $.k8s(pod).transfer.upload(local, remote) and .download(remote, local), which carry the target's namespace, container and cluster context. Only the URL grammar of $.transfer.copy() has no Kubernetes leg — it handles local/SSH/Docker paths.

Multi-Container Pods

A sidecar container — a service mesh proxy like Istio's, a logging agent, anything running alongside the main container in the same pod — is reached the same way as any other container: name it explicitly.

// The main container
const app = $.k8s({ pod: 'app-pod', container: 'app', namespace: 'default' });
await app`npm run migrate`;

// A sidecar
const proxy = $.k8s({ pod: 'app-pod', container: 'istio-proxy', namespace: 'default' });
await proxy`curl -s localhost:15000/clusters`;

There is no dedicated "list containers in this pod" call — read it off kubectl get pod directly: await $`kubectl get pod app-pod -n default -o jsonpath='{.spec.containers[*].name}'`.

Error Handling

Failures throw KubernetesError or CommandError, both extending ExecutionError with a kind to branch on rather than a specific error code:

import { $, ExecutionError } from '@xec-sh/core';

const pod = $.k8s({ pod: 'app-pod', namespace: 'default' });

try {
await pod`command`;
} catch (error) {
if (error instanceof ExecutionError) {
if (error.kind === 'not-found') {
console.error('Pod or container does not exist');
} else if (error.kind === 'connection-refused' || error.kind === 'connection-lost') {
console.error('Cluster unreachable — check kubeconfig and context');
}
}
}

Implementation Details

The Kubernetes adapter is implemented in:

  • packages/core/src/adapters/kubernetes/index.ts — main adapter, kubectl invocation, port forwarding, log streaming, file copy
  • packages/core/src/adapters/kubernetes/kubernetes-api.tsK8sExecutionContext/K8sPod, the fluent surface shown throughout this page
  • packages/core/src/adapters/kubernetes/kubernetes-utils.ts — target parsing and helpers

See Also