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
- Variables
- func LocalFolderHash(localWorkspace string) string
- type ExecRequest
- type ExecResult
- type ForwardedPort
- type Host
- func (h *Host) Clientset() kubernetes.Interface
- func (h *Host) Delete(ctx context.Context, ns, localWorkspace string) error
- func (h *Host) DeleteByName(ctx context.Context, ns, name string) error
- func (h *Host) Exec(ctx context.Context, req ExecRequest) (*ExecResult, error)
- func (h *Host) Execer(namespace, podName, containerName string) runner.Execer
- func (h *Host) Inspect(ctx context.Context, ns, localWorkspace string) (*PodInfo, error)
- func (h *Host) InspectByName(ctx context.Context, ns, name string) (*PodInfo, error)
- func (h *Host) PortForward(ctx context.Context, req PortForwardRequest) (*PortForwardSession, error)
- func (h *Host) RESTConfig() *rest.Config
- func (h *Host) Up(ctx context.Context, req UpRequest) (*UpResult, error)
- type Options
- type PodInfo
- type PortForwardRequest
- type PortForwardSession
- type UpRequest
- type UpResult
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
RESTConfig returns the underlying rest.Config (may be nil).
func (*Host) Up ¶
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.