Help us shape the future of Go! Take the 2026 Go Developer Survey

kubernetes

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package kubernetes brings a devcontainer up against a Kubernetes apiserver: discover-or-create the Pod, exec lifecycle phases through the pod-exec subresource, port-forward, delete on teardown.

The package builds on pkg/projection/kubernetes for the spec → Pod translation and on pkg/host/runner for marker-gated lifecycle dispatch, mirroring the shape of pkg/host/docker against the docker engine. The same `runner.Execer` surface that the docker host adapts onto docker exec is adapted here onto remotecommand.NewSPDYExecutor, so an operator that wraps the kubernetes Exec by hand sees the same upstream-compatible marker shell.

Compatibility with the upstream devcontainer CLI is intentional: container labels (devcontainer.local_folder, devcontainer.config_file) land on the Pod's annotations, marker file paths and lifecycle command semantics mirror microsoft/devcontainers-cli, and the in-pod keepalive is the same shell loop used by pkg/host/docker.

Embedding

Construct a Host with a kubernetes.Interface and the matching *rest.Config (the latter needed for SPDY exec / port-forward, both of which talk a streaming protocol the typed clientset doesn't expose):

cfg, _ := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
cs, _ := kubernetes.NewForConfig(cfg)
h := k8shost.New(cs, cfg, k8shost.Options{Logger: log.Printf})
res, err := h.Up(ctx, k8shost.UpRequest{...})

The package never reads $KUBECONFIG, never writes to stdout/stderr unless explicitly given writers in Options or per-call request, never consults global state. Safe to drop into an HTTP handler, a controller-runtime reconcile loop, or a test.

CLI surface

Deliberately none. The upstream "kubectl + manifest" workflow is the expected UX for the rendering case (covered by pkg/projection/kubernetes); the imperative side belongs in services and operators that already own a kubernetes.Interface, not in a shell-out CLI that re-implements kubeconfig discovery.

Index

Constants

View Source
const (
	// AnnotationLocalFolder mirrors pkg/host/docker.LabelLocalFolder
	// and pkg/projection/kubernetes.AnnotationLocalFolder. Stored as
	// an annotation because path values can't be label values.
	AnnotationLocalFolder = "devcontainer.local_folder"
	// AnnotationConfigFile mirrors pkg/host/docker.LabelConfigFile.
	AnnotationConfigFile = "devcontainer.config_file"
	// LabelLocalFolderHash holds the discovery key. The value is a
	// truncated sha256 of the LocalWorkspace path, so repeated calls
	// with the same workspace land on the same pod even across
	// process restarts.
	LabelLocalFolderHash = "devcontainer.io/local-folder-hash"
)

Discovery surface. The annotations come from pkg/projection/kubernetes (path values aren't valid label values), and the imperative host adds a label whose value is a stable hash of the LocalWorkspace so a List call with a label selector can cheaply rediscover the pod.

Variables

View Source
var ErrPodNotFound = errors.New("kubernetes host: pod not found")

ErrPodNotFound is returned by Inspect / Delete / Stop when no pod matches the supplied selector.

Functions

func LocalFolderHash

func LocalFolderHash(localWorkspace string) string

LocalFolderHash returns the discovery label value for a workspace path. Public so embedders that want to look up pods by the same key (without going through Inspect) can compute the value directly.

The result is the lower-case base32 (RFC 4648, without padding) of the first 20 bytes of sha256(path), giving 32 characters — well inside Kubernetes' 63-char label-value limit and DNS-1123-friendly.

Types

type ExecRequest

type ExecRequest struct {
	Namespace string
	PodName   string
	// ContainerName picks a container in the pod. Empty means the
	// pod's first container — matches `kubectl exec`'s default.
	ContainerName string
	// Cmd is the argv passed to the container. Caller is responsible
	// for shell wrapping.
	Cmd []string
	// User and WorkDir don't have a clean apiserver-exec equivalent
	// (kubectl exec doesn't take -u or --workdir), so they're
	// translated into a `cd <workdir>; exec setpriv ... or su -c
	// ...` shell wrapper. Empty values are no-ops.
	User    string
	WorkDir string
	// Env is layered on top of the container's environment for this
	// exec via a leading `env K=V K=V <cmd>`. Empty is a no-op.
	Env map[string]string
	// TTY allocates a pseudo-terminal.
	TTY bool
	// Stdin / Stdout / Stderr are forwarded to/from the exec session.
	// Any of them may be nil; nil stdin sends EOF immediately, nil
	// stdout/stderr captures into the returned ExecResult.
	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer
}

ExecRequest captures the inputs to a single apiserver pod-exec call. Mirrors pkg/host/docker.ExecRequest so embedders that already use the docker host don't need a separate vocabulary for kubernetes.

type ExecResult

type ExecResult struct {
	// ExitCode is best-effort: the apiserver's exec stream surfaces
	// non-zero exits as a wrapped error rather than a structured
	// status, so a non-nil err from Exec means ExitCode is "unknown
	// non-zero". For matching docker semantics we return ExitCode = 1
	// in that case.
	ExitCode int
	Stdout   []byte
	Stderr   []byte
}

ExecResult reports the outcome.

type ForwardedPort

type ForwardedPort struct {
	Local  uint16
	Remote uint16
}

ForwardedPort is a single mapped port pair.

type Host

type Host struct {
	// contains filtered or unexported fields
}

Host is the per-cluster connection. Safe for concurrent use.

func New

func New(cs kubernetes.Interface, cfg *rest.Config, opts Options) *Host

New wraps an existing clientset and rest.Config. Both lifetimes are the caller's responsibility; New does not copy or mutate either.

cfg may be nil when the embedder only needs Up/Inspect/Delete (no exec, no port-forward); attempts to Exec or PortForward without a cfg return an error rather than panicking.

func (*Host) Clientset

func (h *Host) Clientset() kubernetes.Interface

Clientset returns the underlying clientset for ad-hoc calls.

func (*Host) Delete

func (h *Host) Delete(ctx context.Context, ns, localWorkspace string) error

Delete removes the devcontainer pod for a given workspace. Returns ErrPodNotFound if no pod exists. Pods on Kubernetes don't have a docker-style "stop without remove" semantic — Delete subsumes both.

func (*Host) DeleteByName

func (h *Host) DeleteByName(ctx context.Context, ns, name string) error

DeleteByName removes a pod by exact namespace+name pair. Returns nil if the pod is already gone.

func (*Host) Exec

func (h *Host) Exec(ctx context.Context, req ExecRequest) (*ExecResult, error)

Exec runs a command inside an existing pod and waits for it to finish. When Stdin/Stdout/Stderr are nil, output is captured into the returned ExecResult; when supplied, output streams to the caller's writers and the buffers are empty.

func (*Host) Execer

func (h *Host) Execer(namespace, podName, containerName string) runner.Execer

Execer returns a runner.Execer that runs commands inside the given namespace/pod/container. Embedders that drive lifecycle commands themselves (rather than calling Up) can hand the result to runner.Run — the same pattern pkg/host/docker exposes.

func (*Host) Inspect

func (h *Host) Inspect(ctx context.Context, ns, localWorkspace string) (*PodInfo, error)

Inspect returns the current state of the devcontainer for the given local workspace path, scoped to ns (or the host's default namespace). Returns ErrPodNotFound if no pod matches the workspace label.

func (*Host) InspectByName

func (h *Host) InspectByName(ctx context.Context, ns, name string) (*PodInfo, error)

InspectByName returns the PodInfo for an exact namespace+name pair. Useful when the embedder already has the pod's identity (from Up).

func (*Host) PortForward

func (h *Host) PortForward(ctx context.Context, req PortForwardRequest) (*PortForwardSession, error)

PortForward starts a port-forwarder in a background goroutine and returns a session handle. The forwarder runs until Stop is called or ctx is cancelled. Mirrors `kubectl port-forward pod/<name>`.

func (*Host) RESTConfig

func (h *Host) RESTConfig() *rest.Config

RESTConfig returns the underlying rest.Config (may be nil).

func (*Host) Up

func (h *Host) Up(ctx context.Context, req UpRequest) (*UpResult, error)

Up brings the devcontainer up: find or create the Pod, wait for it to reach Ready, then run the appropriate lifecycle phases gated by marker files. Idempotent: calling Up twice on the same workspace reuses the existing pod and only re-runs the phases whose markers indicate they need to.

type Options

type Options struct {
	// Logger receives informational messages. nil discards.
	Logger func(format string, args ...any)
	// Stdout / Stderr default to nil (silent). Lifecycle command output
	// is written to these unless the per-call request overrides.
	Stdout io.Writer
	Stderr io.Writer
	// Namespace is the default namespace for Up / Inspect / Delete /
	// Exec when the per-call request leaves it empty. Empty here means
	// "default".
	Namespace string
	// ReadyTimeout bounds Up's wait for the pod to reach Ready before
	// running lifecycle commands. Defaults to 5 minutes.
	ReadyTimeout time.Duration
	// DeleteGracePeriod bounds Delete's terminationGracePeriodSeconds
	// override. Defaults to the pod's own
	// TerminationGracePeriodSeconds (typically 30s).
	DeleteGracePeriod time.Duration
}

Options tunes a Host. All fields are optional.

type PodInfo

type PodInfo struct {
	Namespace     string
	Name          string
	UID           string
	NodeName      string
	Phase         corev1.PodPhase
	PodIP         string
	Ready         bool
	StartedAt     string
	CreatedAt     string
	Labels        map[string]string
	Annotations   map[string]string
	ContainerName string
	Image         string
}

PodInfo is a small projection of corev1.Pod's status. Returned by Inspect; sufficient for the most common embedder needs without forcing them to import corev1 just to ask "is it running?".

type PortForwardRequest

type PortForwardRequest struct {
	Namespace string
	PodName   string
	// Ports is one or more "LOCAL:REMOTE" specs (e.g. "0:8080" lets
	// the kernel pick a local port for remote 8080). Same syntax as
	// kubectl port-forward.
	Ports []string
	// Addresses are the local interfaces to bind. Empty defaults to
	// ["localhost"] (matches kubectl's default).
	Addresses []string
	// Out / ErrOut receive port-forward's startup banners
	// ("Forwarding from 127.0.0.1:1234 -> 8080"). nil discards both.
	Out    io.Writer
	ErrOut io.Writer
	// Ready, when non-nil, is closed by the forwarder once it's
	// listening locally. Embedders that need to know when it's safe
	// to dial the local port should pass a channel and wait on it.
	Ready chan struct{}
}

PortForwardRequest captures the inputs to a single port-forward session. Mirrors `kubectl port-forward pod/<name> LOCAL:REMOTE`.

type PortForwardSession

type PortForwardSession struct {
	// LocalPorts is populated once the forwarder is Ready. Each entry
	// is "LOCAL:REMOTE" with the actual local port the kernel
	// assigned (useful for "0:N" specs).
	LocalPorts func() ([]ForwardedPort, error)
	// Stop tears the session down. Safe to call multiple times.
	Stop func()
	// Done is closed when the forwarder goroutine exits, either
	// because Stop was called or because of an unrecoverable error.
	Done <-chan error
}

PortForwardSession is the handle the embedder uses to wait for startup and stop the forwarder.

type UpRequest

type UpRequest struct {
	// Spec is the parsed devcontainer.json. Required.
	Spec *spec.DevContainer
	// SpecPath is the host-side path the spec was loaded from. Stored
	// in the AnnotationConfigFile annotation so downstream tooling
	// can reverse-look-up the source file.
	SpecPath string
	// LocalWorkspace is the host-side workspace folder (absolute).
	// Required: it's the discovery key (the LabelLocalFolderHash label
	// is sha256(LocalWorkspace)) and the default HostPath source for
	// the workspace volume.
	LocalWorkspace string
	// ContainerWorkspace is the path inside the container where the
	// workspace is mounted. When empty, falls back to spec
	// workspaceFolder, then "/workspaces/<basename>".
	ContainerWorkspace string
	// Image is the already-built image reference (use pkg/builder to
	// produce one).
	Image string
	// Features as resolved by pkg/features. Their containerEnv,
	// init/privileged/capAdd/securityOpt, and mounts merge into the
	// pod the same way pkg/projection/kubernetes merges them.
	Features []features.Resolved
	// LocalEnv is the host environment used for ${localEnv:VAR}
	// substitution. Pass an empty map (not nil) for no host env.
	LocalEnv map[string]string
	// IDLabels seeds ${devcontainerId} substitution. The
	// AnnotationLocalFolder + AnnotationConfigFile pair is always
	// included; supply additional entries here if you want them folded
	// into the id hash.
	IDLabels map[string]string

	// Namespace overrides Options.Namespace for this call. Empty falls
	// back to Options.Namespace, then "default".
	Namespace string
	// Name is the metadata.name written onto the projected Pod. Empty
	// is allowed: Up will fall back to "devcontainer-<localFolderHash>"
	// which is stable across reruns and matches the discovery key.
	Name string
	// ContainerName overrides the projected container's name. Empty
	// means pkg/projection/kubernetes.DefaultContainerName.
	ContainerName string
	// Labels and Annotations seed the projected Pod's metadata. The
	// host adds AnnotationLocalFolder, AnnotationConfigFile, and
	// LabelLocalFolderHash on top.
	Labels      map[string]string
	Annotations map[string]string
	// WorkspaceVolumeSource overrides the default HostPath workspace
	// volume. See pkg/projection/kubernetes.ProjectRequest.
	WorkspaceVolumeSource *corev1.VolumeSource
	// ReadyTimeout overrides Options.ReadyTimeout for this call.
	ReadyTimeout time.Duration
}

UpRequest captures everything Up needs. Fields mirror pkg/host/docker.UpRequest where possible, plus the few kubernetes-specific knobs (Namespace, Name, ContainerName, WorkspaceVolumeSource) that pkg/projection/kubernetes already takes.

type UpResult

type UpResult struct {
	Namespace       string
	Name            string
	ContainerName   string
	UID             string
	CreatedThisCall bool
	// PhasesRun lists the lifecycle phases that actually ran (markers
	// may have skipped some).
	PhasesRun []lifecycle.Hook
}

UpResult reports what happened. Symmetrical with the docker host's UpResult so embedders can log "created+started" vs "reused+lifecycle" the same way regardless of host.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL