vnode

package
v0.0.0-...-c07e1af Latest Latest
Warning

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

Go to latest
Published: Aug 22, 2026 License: Apache-2.0 Imports: 47 Imported by: 0

Documentation

Overview

Package vnode implements the Virtual Kubelet integration: one static virtual Node per provider, whose PodLifecycleHandler provisions/terminates external instances through the provider seam. See docs/architecture.md §3.

Ownership model ("VK owns provisioning"): the pod controller's CreatePod calls provider.Provision and DeletePod calls provider.Terminate directly. The Pod is the single source of truth for the workload; the only provisioning input that is not on the Pod — the optimizer's capacity tier — rides on the CapacityTypeAnnotation, written by the placement controller when it ungates the Pod. Instance identity is derived deterministically from the Pod (ClaimName), so a provider whose List reports the claim tag can recover and reclaim an instance across a controller restart without a durable ledger.

Index

Constants

View Source
const DefaultKubeletAddr = ":10250"

DefaultKubeletAddr is where the kubelet API listens. Nothing requires 10250 (the API server dials whatever the Node advertises), but matching a real kubelet keeps existing NetworkPolicies and operator intuition intact.

Variables

This section is empty.

Functions

func ConnectSecretName

func ConnectSecretName(podName string) string

ConnectSecretName is the Secret holding a Pod's connect credential.

func NodeName

func NodeName(providerName string) string

NodeName returns the virtual node name for a provider: "nebula-<provider>". One static node per provider (see docs/architecture.md §3); the scheduler routes an ungated Pod to it via the ProviderLabel nodeSelector.

func PodIPFromEnv

func PodIPFromEnv() string

PodIPFromEnv reads the manager's own Pod IP, projected by config/manager via a fieldRef. Empty means no endpoint can be advertised, so the caller degrades to logs-unsupported rather than publishing an unreachable address.

Types

type Blocklister

type Blocklister interface {
	Record(prov string, scope provider.BlockScope, ttl time.Duration)
}

Blocklister records a failed placement so the placement controller fails over to the next candidate instead of hot-looping on a provider that just said no. The write half of pkg/failover.Blocklist; a nil blocklist is a no-op.

type Handler

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

Handler bridges one provider into the virtual kubelet: CreatePod provisions an external instance, DeletePod terminates it. This is the "VK owns provisioning" model — no separate controller issues Provision/Terminate.

Leak-safety: CreatePod records the instance id before returning success, so a paid instance is always reachable for teardown, and Provision is idempotent on ClaimName, so a retry after a crash adopts the existing instance instead of creating a second.

func NewHandler

func NewHandler(prov provider.Provider, client kubernetes.Interface, blocklist Blocklister) *Handler

NewHandler builds a Handler for one provider backend. The poll cadence comes from Capabilities.PollInterval, falling back to defaultPollInterval. blocklist (failover recording) and client (the endpoint patch) may both be nil.

func (*Handler) AttachToContainer

func (h *Handler) AttachToContainer(context.Context, string, string, string, vkapi.AttachIO) error

func (*Handler) CreatePod

func (h *Handler) CreatePod(ctx context.Context, pod *corev1.Pod) error

CreatePod provisions an external instance for the Pod through the provider. The Pod carries the whole workload shape; the only out-of-band input, the optimizer's capacity tier, rides on CapacityTypeAnnotation.

func (*Handler) DeletePod

func (h *Handler) DeletePod(ctx context.Context, pod *corev1.Pod) error

DeletePod terminates the external instance and drops the pod from tracking. Terminate is idempotent, so a repeated DeletePod (VK may call it more than once) is safe.

func (*Handler) GetContainerLogs

func (h *Handler) GetContainerLogs(
	ctx context.Context, namespace, podName, containerName string, opts vkapi.ContainerLogOpts,
) (io.ReadCloser, error)

GetContainerLogs serves `kubectl logs` for a Pod on this virtual node (kubelet.go carries the endpoint). Three things must line up, each failing as NotFound: the Pod is TRACKED here, it has an instance id (one still inside Provision has nothing to read), and the provider implements provider.LogStreamer (logs are optional).

containerName is IGNORED: a Nebula Pod is one external instance with one console, so there is no per-container stream to select.

func (*Handler) GetMetricsResource

func (h *Handler) GetMetricsResource(context.Context) ([]*dto.MetricFamily, error)

func (*Handler) GetPod

func (h *Handler) GetPod(ctx context.Context, namespace, name string) (*corev1.Pod, error)

GetPod returns the tracked pod, or a NotFound error the pod controller understands.

Tracking is in-memory, so a restart (redeploy, crash, leader handoff) starts with an empty map while the instances are still running. VK's createOrUpdatePod treats a nil result as "create", which would re-drive provisioning. So on a cold map we RE-ADOPT: ask the provider whether an instance with this claim is live, and rebuild the entry from it. VK then takes the adopt branch and the next poll tick advances the pod.

The three outcomes stay DISTINCT, because "no such instance" and "could not ask" have opposite consequences. Reporting the latter as NotFound is what let one failed List destroy a healthy workload: VK discards this error and branches on nil-ness alone, so a nil pod re-issues CreatePod against a running instance — and with the provider still unreachable that Provision fails too, marking the Pod Failed for a reap while the real instance keeps billing behind a zero id. So an unreachable provider returns a non-nil pod WITH an error: the pod suppresses the create, and the non-NotFound error makes the delete path requeue instead of terminating. Both wait for the next sync, the only correct move while ownership is unknown.

func (*Handler) GetPodStatus

func (h *Handler) GetPodStatus(_ context.Context, namespace, name string) (*corev1.PodStatus, error)

GetPodStatus returns the tracked pod's status.

func (*Handler) GetPods

func (h *Handler) GetPods(_ context.Context) ([]*corev1.Pod, error)

GetPods returns every pod this virtual node is tracking.

func (*Handler) GetStatsSummary

func (h *Handler) GetStatsSummary(context.Context) (*statsv1alpha1.Summary, error)

func (*Handler) NotifyPods

func (h *Handler) NotifyPods(ctx context.Context, cb func(*corev1.Pod))

NotifyPods registers the async status callback and starts the poll loop. VK calls it once at startup; the loop runs until ctx is cancelled.

We WRAP VK's callback: its status path writes only the /status subresource and silently drops metadata changes on the same object, but the endpoint has to live on metadata (PodIP cannot hold a DNS name — see applyState) and so does the instance id. So the wrapper writes that metadata first, then hands the same Pod to VK. Every status push therefore also reconciles how the workload is reached and which instance backs it.

func (*Handler) PortForward

func (*Handler) RunInContainer

func (h *Handler) RunInContainer(
	ctx context.Context, namespace, podName, containerName string, cmd []string, attach vkapi.AttachIO,
) error

RunInContainer serves `kubectl exec` for a Pod on this virtual node: it starts cmd in the external instance and hands the streams to runExec, which owns the pumping.

Same three preconditions as logs, each a NotFound: the Pod is tracked here, it has an instance, and the provider implements provider.Executor (exec is optional — a backend with no way into the box simply cannot serve it).

containerName is IGNORED, as for logs: a Nebula Pod is one external instance, so there is no second container to pick.

func (*Handler) Tracks

func (h *Handler) Tracks(namespace, podName string) bool

Tracks reports whether this virtual node is the one running the Pod. The kubelet API has one listener for every provider and its routes carry no node name, so this is how a request finds its handler; see kubelet.go.

func (*Handler) UpdatePod

func (h *Handler) UpdatePod(_ context.Context, pod *corev1.Pod) error

UpdatePod is a no-op: an instance's shape is immutable once provisioned (recovery from any change is delete-and-recreate). We still refresh the tracked copy so GetPod reflects the latest metadata.

type KubeletServer

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

KubeletServer serves the kubelet API routes Nebula implements — container logs and exec — for every provider's virtual node.

Why it exists: `kubectl logs` and `kubectl exec` are not control-plane reads. The API server proxies them to the kubelet of the Pod's node, at that Node's addresses and daemonEndpoints. A virtual node has no kubelet, so without a listener both fail whatever the provider can serve.

One listener for all nodes: the routes carry only namespace/pod/container, so a request is resolved by asking each registered Handler whether it tracks that Pod — at most one can. Cheaper than a port per provider, and than reading the Pod to learn its node.

TLS uses a self-signed in-memory cert, which is what the API server expects: it does not verify a kubelet's serving cert unless --kubelet-certificate-authority is set. The webhook cert rotator cannot help, since it mints for a Service DNS name and this endpoint is dialed by Pod IP.

Client certs are verified only when ClientCAPath is set. Off by default because which CA signs the API server's kubelet client cert is not portable (kubeadm uses the cluster CA, EKS/GKE their own), so requiring it would break `kubectl logs` on managed control planes. The cost is now larger than logs: anything that can reach this port can also RUN COMMANDS in these Pods' instances, without passing through the API server's RBAC. Set the CA if you can name it, else close the port with a NetworkPolicy.

func NewKubeletServer

func NewKubeletServer(nodeIP, addr, clientCAPath string) (*KubeletServer, error)

NewKubeletServer builds the shared kubelet endpoint. nodeIP is the manager's Pod IP, addr defaults to DefaultKubeletAddr, clientCAPath may be empty.

A bad address or IP is an error here, so a misconfiguration fails at startup instead of producing nodes that advertise an endpoint nothing serves.

func (*KubeletServer) Register

func (s *KubeletServer) Register(nodeName string, h *Handler)

Register wires one provider's Handler in. Called by Runner before it starts; the node name is only a key, since lookup is by Pod.

func (*KubeletServer) Start

func (s *KubeletServer) Start(ctx context.Context) error

Start serves until ctx is cancelled. As a manager.Runnable it is leader-scoped like the Runners, which is correct: only the leader holds tracked Pods, and only its IP is advertised on the Nodes.

type Runner

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

Runner is a manager.Runnable owning one provider's virtual node. It starts the VK node/pod controllers and blocks until the manager's context is cancelled, so it follows the manager's lifecycle and leader election like any other runnable.

It wires virtual-kubelet's lower-level `node` package directly rather than the `nodeutil` wrapper, which pulls in a kubelet HTTP/auth stack whose apiserver dependency does not compile against the k8s 0.33 line we pin. The one kubelet route we serve (logs) is hosted by KubeletServer, which needs none of it.

func NewRunner

func NewRunner(
	prov provider.Provider, client kubernetes.Interface, blocklist Blocklister, kubelet *KubeletServer,
) *Runner

NewRunner builds the virtual-node runner for one provider. blocklist (Provision failures) and kubelet (the log endpoint) are both shared, and both may be nil.

func (*Runner) Start

func (r *Runner) Start(ctx context.Context) error

Start builds and runs the virtual node until ctx is cancelled. It is invoked by the controller-runtime manager.

Jump to

Keyboard shortcuts

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