cocoon-common

Metadata contract

meta owns the shared identifiers cocoon components stamp on Kubernetes objects, plus the typed wrappers that read and write them. Components may also define private keys for their own reconciliation state.

Import path: github.com/cocoonstack/cocoon-common/meta.

Identifier namespaces

All identifiers live under three cocoonstack.io prefixes. The prefix tells you who owns the value.

Prefix Owner and use Keys
cocoonset.cocoonstack.io/ CocoonSet CRD group, pod selector labels, and CocoonSet-level fields the operator mirrors onto a managed pod name, role, slot, mode, image, os, storage, snapshot-policy, network, managed, force-pull, generation, hibernated-on-node
vm.cocoonstack.io/ VM-instance metadata — observed runtime state plus the per-VM spec the operator hands to vk-cocoon id, name, ip, vnc-port, hibernate, restore-from-hibernate, keep-snapshot-on-delete, fork-from, clone-from-dir, conn-type, backend, no-direct-io, probe-port, lifecycle-state, lifecycle-observed-generation, lifecycle-state-message, snapshot-annotations, snapshot, snapshot-status
cocoonstack.io/ Node labels vk-cocoon stamps on its virtual node and the operator selects on pool, snapshot-cpu-class

meta.KindCocoonSet and meta.TolerationKey (virtual-kubelet.io/provider) complete the set.

Typed annotation wrappers

Prefer these over raw map manipulation — they are the only place the key/value encoding lives.

// Managed=true: vk-cocoon owns lifecycle; false: adopt a pre-assigned VM.
spec := meta.VMSpec{
    VMName:         "vk-prod-demo-0-71ea62",
    Image:          "ghcr.io/cocoonstack/cocoon/ubuntu:24.04",
    Mode:           string(v1.AgentModeRun),
    OS:             string(v1.OSLinux),
    Backend:        string(v1.BackendFirecracker),
    SnapshotPolicy: string(v1.SnapshotPolicyAlways),
    Managed:        true,
    ForcePull:      true,
    ProbePort:      "22",
}
spec.Apply(pod)

// vk-cocoon side: write runtime state back onto the pod.
meta.VMRuntime{VMID: vmID, IP: ip}.Apply(pod)

// hibernate / wake
meta.HibernateState(true).Apply(pod)

VMSpec.Apply and VMRuntime.Apply skip empty string fields, and runtime ports are emitted only when positive. Spec booleans are written only when true; false does not clear an existing annotation. Clearing runtime values requires an explicit annotation patch. HibernateState(false).Apply removes its key. LifecycleStatus.Apply always writes state and observed generation, and removes the message key when Message is empty. ParseVMSpec / ParseVMRuntime / ReadHibernateState read them back.

meta.FromAgentSpec and meta.FromToolboxSpec build a VMSpec straight from the CRD types, resolving each enum that declares a Default() through it; ConnType has none and is passed through as-is.

VM naming and roles

meta.VMNameForDeployment(ns, cocoonSet, slot)  // "vk-<ns>-<set>-<slot>-<hash>"
meta.ToolboxPodName(cocoonSet, toolbox)        // "<set>-<toolbox>"
meta.VMNameForPod(ns, podName)                 // "vk-<ns>-<pod>-<hash>"
meta.AgentVMNamePrefix(ns, cocoonSet)          // "vk-<ns>-<set>-"
meta.ExtractAgentSlot(ns, cocoonSet, vmName)   // slot, or -1 for a toolbox
meta.InferRoleFromAgentSlot(slot)              // main / sub-agent / toolbox
meta.RoleForPod(pod, vmName)                   // owner ref + name → role

<hash> is the first 6 hex digits of sha256(<ns>/<pod>). A namespace never contains a slash, so two pods never share a VM name even when their dash-joined forms match (team-a/dev-0 is vk-team-a-dev-0-cc642f, team/a-dev-0 is vk-team-a-dev-0-7ab660), and the name stays recomputable from the namespace and pod name alone: the operator re-derives the main agent’s name to find its snapshots after the pod is gone. The name carries no dot because cocoon uses it as the guest hostname, and cloud-init keeps only the part before the first dot: dots in the pod name become dashes, while the hash still covers the original name, so demo.v1-0 and demo-v1-0 stay distinct. ExtractAgentSlot accepts a name only when VMNameForDeployment reproduces it, so no toolbox (app-0, or a toolbox of a set whose name extends this one) is ever misread as an agent slot. A toolbox VM is VMNameForPod(ns, ToolboxPodName(set, toolbox)).

A pulled hibernate snapshot is imported as <vm> + meta.HibernateImportSuffix (-hibernate-import) so it never collides with the live VM; the webhook caps non-macOS VM names at 63 minus that suffix so the import name still fits cocoon’s 63-character limit.

When a Pod is available, use RoleForPod to combine its CocoonSet ownership with the VM name. Cleanup paths that no longer have a Pod can use ExtractAgentSlot with the owning CocoonSet’s namespace and name.

Snapshot contract

Two tag constants anchor the cross-component contract:

meta.ShouldSnapshotVM(spec, role) returns true for always, true only for the main role under main-only, and false for never. It checks the policy and role, not Managed; vk-cocoon checks lifecycle ownership separately.

After all owned Pods are gone, cocoon-operator deletes their hibernate tags. It keeps latest tags selected by the snapshot policy for later reuse: always keeps every role, main-only keeps only the main agent, and never keeps none. Its cleanup derives the role from the stored VM name because the Pods have already been deleted.

meta.LabelSnapshotCompatibilityClass (cocoonstack.io/snapshot-cpu-class) closes the same loop for placement: CocoonSetSpec.SnapshotCompatibilityClass names a certified guest-visible CPU ABI, cocoon-operator renders it as a hard node selector on every managed pod, and vk-cocoon publishes the label on nodes configured with that class and refuses any classified pod it cannot serve. It is independent from LabelNodePool, so several workload pools can share one snapshot class.

meta.AnnotationKeepSnapshotOnDelete marks a pod delete as a scheduling-seat release rather than a teardown: vk-cocoon keeps the node-local snapshot as a warm-wake cache instead of dropping it with the VM. It is best-effort by contract — a lost flag costs the wake a registry pull, never correctness.

meta.AnnotationHibernatedOnNode records the main agent’s node at release-policy suspend; wake uses it as a preferred-affinity hint for a warm restore.

meta.AnnotationSnapshotAnnotations carries a JSON object of OCI annotations that vk-cocoon stamps on every snapshot it pushes for the pod, hibernate and latest alike. The caller owns the keys and values and reads them back from the registry; the engine never interprets them. meta.ReadSnapshotAnnotations rejects a malformed value and any key under the cocoonstack. prefix, which the snapshot wire format reserves for its own annotations.

meta.AnnotationSnapshotRequest (vm.cocoonstack.io/snapshot) asks vk-cocoon to push a live snapshot of the pod’s running VM to a registry-relative repo[:tag] (the tag defaults to latest). The VM pauses only for the save and keeps running; the snapshot keeps its NICs, so it can serve as a clone source. vk-cocoon serves a request once the VM is Ready and no hibernate is asked for, and answers in meta.AnnotationSnapshotStatus (vm.cocoonstack.io/snapshot-status), a JSON meta.SnapshotStatus whose ref echoes the request and whose state is pushed or failed; a failed answer carries a message. A ref is answered once, pushed or failed, and re-patching an answered ref does nothing: every push, a retry included, uses a new tag, and the client waits for a status carrying that ref.

Lifecycle status

meta.LifecycleStatus is the typed contract for the annotation triple vk-cocoon writes (lifecycle-state, lifecycle-observed-generation, lifecycle-state-message):

status := meta.LifecycleStatus{
    State:              meta.LifecycleStateReady,
    ObservedGeneration: meta.ReadCocoonSetGeneration(pod),
}

// In-memory: mutate the pod we already hold.
status.Apply(pod)

// Wire: Annotations() returns the key/value map (a nil value deletes the key);
// wrap it with k8s.AnnotationsMergePatch for an apiserver merge-patch body.
patch, _ := k8s.AnnotationsMergePatch(status.Annotations())

States are creating, ready, hibernating, hibernated, failed; a client waits for one of ready, hibernated or failed. An empty message clears the annotation, so a stale failure reason cannot tail into the next lifecycle. LifecycleStatus.Snapshot() returns a NUL-separated comparison key for change detection.

cocoon-operator stamps the owning CocoonSet’s metadata.generation onto the pod via meta.StampCocoonSetGeneration so vk-cocoon can echo it back as lifecycle-observed-generation. Counter-based completion lets a client tell “the operation I asked for finished” from “an older completion is still being reported”, with no dependence on wall-clock skew.

Pod helpers

Helper Meaning
IsPodReady(pod) A PodReady=True condition exists
IsPodTerminal(pod) Phase is PodFailed; PodSucceeded is not included
IsContainerRunning(pod) Any container status reports Running
VMLive(pod) A container reports Running and the VMID annotation is non-empty; this does not test readiness or contact the VM
PodKey(ns, name) namespace/name
PodNodePool(pod) First non-empty pool in nodeSelector, labels, annotations, then DefaultNodePool

HasCocoonTolerationKey and IsOwnedByCocoonSet / CocoonSetOwnerName cover the admission-side ownership checks. The toleration check matches on key alone; operator, value, and effect are ignored so every toleration bearing the key is included in the gate. The owner helpers match the owner reference’s Kind and return its name; they do not authenticate who created the Pod.

ConnectionType(osType, hasVNCPort, override) resolves the connection protocol: an explicit override wins, then a VNC port, then the OS family (android→adb, windows→rdp, otherwise ssh).