Arquitectura — Delonix Engine
Modelo C4 (Contexto → Contentores → Componentes) e system design funcional do
Delonix Engine: motor de containers e microVMs daemonless, rootless-first,
kernel-native, em Rust (28 crates, workspace crates/). Este documento é canónico
e mantido contra o código — cada afirmação estrutural tem a referência do
crate/ficheiro onde foi confirmada. Onde há limites, eles aparecem nos diagramas,
não escondidos em rodapés.
Convenção de nomes: delonix é o binário CLI (crate delonix-runtime-bin);
delonix-cri é o binário servidor CRI (crate delonix-cri); "holder" é o processo
que detém o network namespace de infra-estrutura do ingress rootless
(crates/adapters/delonix-sdn/src/infra.rs).
C4 — Nível 1: Contexto
O Delonix Engine é usado por um operador humano (via CLI), por um kubelet (via CRI), e fala com sistemas externos: registos OCI, hosts SSH remotos e os hipervisores locais.
graph TB
OP["Operador / utilizador<br>terminal"]
KUBELET["kubelet<br>nó Kubernetes"]
REG["Registos OCI<br>Docker Hub, ghcr.io, registos privados"]
SSHHOSTS["Hosts SSH remotos<br>cluster apply via ssh e scp"]
HV["Hipervisores locais<br>Cloud Hypervisor sobre KVM ou libvirt"]
UBU["cloud-images.ubuntu.com<br>imagens base p/ imagem VM dourada"]
DLX["Delonix Engine<br>motor de containers e microVMs<br>daemonless, rootless-first"]
OP -- "delonix container / image / build / vm / volumes / network / stack / cluster" --> DLX
KUBELET -- "CRI runtime.v1 gRPC<br>socket unix" --> DLX
DLX -- "pull e push de imagens<br>HTTP, verificação de digest" --> REG
DLX -- "kubeadm init e join idempotentes<br>sudo -n remoto" --> SSHHOSTS
DLX -- "arranque de microVMs<br>VmBackend" --> HV
DLX -- "download validado por SHA256SUMS" --> UBU
- CLI:
bins/delonix-runtime-bin/src/main.rs(gruposContainer/Image/Build/Vm/Volumes/Network/Stack/Cluster, um módulo por grupo emsrc/cmd/). - CRI:
crates/interfaces/delonix-cri/src/bin/delonix-cri.rs→serve_blockingnum socket unix (DELONIX_CRI_ADDR, defaultunix:///run/delonix-cri.sock), o endpoint do--container-runtime-endpointdo kubelet. - Registos OCI:
crates/adapters/delonix-oci/src/registry.rs(pull_from_registry_with_creds,push_to_registry,push_oci_artifact/pull_oci_artifact— este par com verificação de digest do blob contra o manifesto). - Hosts SSH:
bins/delonix-runtime-bin/src/cmd/remote.rs(shell-out assh/scpdo sistema,sudo -nremoto). - Hipervisores:
crates/adapters/delonix-vm/src/lib.rs(traitVmBackend, implementaçõesCloudHypervisorBackendeLibvirtBackend). - Fronteira pública: este repositório não conhece tenant, licença, billing nem consola — qualquer consumidor desse género é "externo" (ver ADR-7).
C4 — Nível 2: Contentores (unidades executáveis)
Não há daemon. Não existe nenhum processo residente obrigatório: a CLI é
efémera, e os processos de longa duração que existem são infra-estrutura opt-in
(rede rootless, log shim, watcher --rm, VMs) — cada um arranca quando é preciso e
morre com o recurso que serve. O estado partilhado é um directório de ficheiros
JSON, não um socket de daemon.
graph TB
subgraph HOST["Host Linux"]
CLI["delonix — CLI efémera<br>clone, pivot_root, cgroups, nft<br>termina após cada comando"]
CRIBIN["delonix-cri — servidor gRPC<br>socket unix p/ o kubelet<br>o único processo-servidor, e é opcional"]
STATE["Estado em JSON — DELONIX_ROOT<br>containers, vms, volumes, images CAS,<br>networks, ingress, cri, clusters"]
subgraph INFRA["Infra rootless do ingress — só quando ha rede custom"]
HOLDER["holder netns<br>unshare --user --map-auto --map-root-user --net --mount<br>re-exec delonix netns holder<br>bridge delonix0 + nft + DHCP + DNS dentro do netns"]
SLIRP1["slirp4netns unico<br>tap0 host para infra, api-socket p/ hostfwd"]
end
subgraph PERCONT["Por container — quando aplicavel"]
CONT["processo do container<br>PID 1 proprio apos clone + exec"]
SLIRPN["slirp4netns por container<br>so em --net host com -p"]
SHIM["log shim<br>fork; escreve o log com rotacao<br>formato cru ou CRI"]
WATCH["watcher --rm<br>fork; espera o fim e limpa"]
end
VM["microVM<br>cloud-hypervisor ou qemu via libvirt"]
end
KUBELET2["kubelet"]
CLI -- "lê e escreve" --> STATE
CRIBIN -- "lê e escreve" --> STATE
KUBELET2 -- "gRPC runtime.v1" --> CRIBIN
CLI -- "arranca e configura" --> CONT
CLI -- "arranca sob procura" --> HOLDER
HOLDER --- SLIRP1
CLI -- "hook on_started" --> SLIRPN
CLI -- "fork" --> SHIM
CLI -- "fork" --> WATCH
CLI -- "VmBackend" --> VM
Confirmação no código, peça a peça:
| Processo | Onde nasce | Tempo de vida |
|---|---|---|
delonix (CLI) |
bins/delonix-runtime-bin/src/main.rs |
um comando; em foreground faz waitpid do container e sai |
delonix-cri |
crates/interfaces/delonix-cri/src/bin/delonix-cri.rs |
serviço (ex.: unit systemd dist/delonix-cri.service dentro da imagem VM dourada) |
| holder netns | infra::start_holder (crates/adapters/delonix-sdn/src/infra.rs): unshare --user --map-auto --map-root-user --net --mount -- <self> netns holder; o re-exec é apanhado em main.rs antes do parser clap |
vida da infra do ingress, gerida por ref-count em ficheiro (infra::acquire/release) |
| slirp4netns único | infra::start_slirp — liga o tap0 ao netns do holder, com api-socket para add_hostfwd |
acompanha o holder |
| slirp4netns por container | delonix_sdn::slirp_attach (crates/adapters/delonix-sdn/src/lib.rs) — chamado como hook on_started do RunSpec |
vida do container (morre com o netns); órfãos limpos por reap_orphan_slirp |
| log shim | fork no pai em spawn (crates/adapters/delonix-linux/src/lib.rs), corre log_shim — lê o pipe (ou o master do pty em modo console) e escreve o log com rotação |
vida do container; destaca-se do stdio com setsid + /dev/null |
watcher --rm |
spawn_rm_watcher (bins/delonix-runtime-bin/src/cmd/container.rs) — só em run -d --rm |
até o container terminar; faz a mesma limpeza do rm -f |
| microVM | delonix_vm::create — processo cloud-hypervisor (dentro do netns de infra, tap via infra::vm_attach) ou domínio libvirt |
vida da VM |
O estado vive em $DELONIX_ROOT (default /var/lib/delonix para root,
~/.local/share/delonix para rootless — infra::base_root e
ImageStore::default_root): Store/JsonStore
(crates/adapters/delonix-state/src/store.rs) persistem Container/Vm como um
JSON por registo; o CRI guarda os seus registos próprios em <root>/cri/
(crates/interfaces/delonix-cri/src/runtime_svc/lifecycle.rs).
Consequência directa do daemonless: como não há monitor residente, o estado
Running de um registo JSON pode divergir do kernel. A verdade é reconciliada na
leitura — reconcile_status (crates/adapters/delonix-linux/src/lib.rs) usa
safe_to_signal (PID + starttime de /proc, para fechar a janela de reutilização
de PID) e reclassifica Running→Crashed/Paused. O CRI chama-o em
load_reconciled antes de responder ao kubelet.
C4 — Nível 3: Componentes (os 28 crates)
Setas = dependências reais, confirmadas nos Cargo.toml de crates/*/ e nos
use delonix_* dos src/. Não há ciclos; delonix-model é a raiz comum.
graph TB
BIN["delonix-runtime-bin<br>CLI delonix — um modulo por grupo em src/cmd<br>manifesto e apply, cluster kubeadm, imagens VM douradas"]
CRI["delonix-cri<br>servidor CRI runtime.v1 — ImageService e RuntimeService<br>modulos: runtime_svc, lifecycle, streaming, spdy"]
RT["delonix-linux<br>motor de containers: clone e namespaces, setup_rootfs,<br>cgroups v2, seccomp, caps, exec, log shim, reconcile_status"]
NET["delonix-sdn<br>SDN rootless: modulo infra — holder netns, slirp unico,<br>publish e DNAT, DNS e DHCP; cni, wg WireGuard, discover"]
IMG["delonix-oci<br>imagens OCI: registry pull e push, cas, overlay,<br>build Dockerfile, buildpack CNB, sign, internal_registry"]
VM2["delonix-vm<br>raiz de composicao das VMs ate a P5:<br>semeia o registo e monta o VmEngine"]
VOL["delonix-volume<br>volumes nomeados e bind mounts, sintaxe -v Docker,<br>driver local ou nfs"]
NODECTX["delonix-node<br>contexto do no: eventos, verificacoes do host,<br>peer_cred, dispatch e as perguntas ao host e aos processos"]
STATECRATE["delonix-state<br>estado persistido: Store e JsonStore com flock,<br>escrita atomica, SecretStore e CredVault cifrados"]
MGMT["delonix-mgmt<br>API de gestao LOCAL (HTTP+JSON num socket unix, so o proprio uid)<br>expoe as metricas partilhadas em /metrics"]
TEL["delonix-telemetry<br>observabilidade: logging estruturado, spans OpenTelemetry/OTLP<br>e o registo Prometheus partilhado (saiu do core na P3)"]
SCAN["delonix-scanner<br>SBOM e varredura de CVE — image scan<br>e a imposicao de scan-on-pull"]
SEC["delonix-security-runtime<br>decisoes de seguranca do no: politica, admissao<br>unica (container E VM), evento, score, redaccao<br>(ADR-0026) — puro, sem sensores e sem inquilino"]
RULES["delonix-net-rules<br>regras de rede PURAS, ZERO dependencias — Cidr, nome de bridge,<br>IPAM dentro de um prefixo, leitura de taxas, o PolicyIr (ADR-0059 D6); partilhado com o PaaS"]
CHV["delonix-provider-cloud-hypervisor<br>backend VmBackend Cloud Hypervisor (microVM por processo, api-socket)<br>(ADR-0044 P4b.4c) — saiu do delonix-vm, que o regista"]
LVIRT["delonix-provider-libvirt<br>backend VmBackend libvirt/KVM (QEMU via virsh)<br>(ADR-0044 P4b.4b) — saiu do delonix-vm, que o regista"]
PVE["delonix-proxmox<br>backend VmBackend REMOTO contra a API de UM no Proxmox VE<br>(ADR-0008) — fora do delonix-vm por trazer cliente HTTP"]
NAS["delonix-truenas<br>provisiona dataset, quota, permissoes e export numa NAS<br>pela API do TrueNAS (ADR-0009) — mesma razao de crate a parte"]
OPN["delonix-opnsense<br>GatewayProvider REMOTO contra a API REST de UMA appliance OPNsense<br>(ADR-0051) — fora do delonix-sdn por trazer cliente HTTP"]
MODEL["delonix-model<br>modelo partilhado PURO (foundation, ADR-0040) —<br>o Error e o dicionario DX-CDNN, Status, ContainerFw,<br>typestate, o modelo do segredo e os nomes gerados"]
STACK["delonix-stack<br>contexto Stack (ADR-0040): tabela de Kinds,<br>reconciliador de 3 vias, Condition, revisões"]
COMPUTE["delonix-compute<br>contexto Compute (ADR-0040): a especificacao<br>de execucao unica (RunOpts) que as entradas traduzem"]
NETCTX["delonix-networking<br>contexto Networking (ADR-0059 D7): as portas de rede por papel<br>(GatewayProvider, NetworkZoneProvider), os registos e as marcas de posse"]
MGMTBIN["delonix-mgmt-bin<br>o executavel delonix-mgmt, que `delonix serve api` executa (P3m)"]
MCPBIN["delonix-mcp-bin<br>o executavel delonix-mcp, que `delonix mcp` executa (P3l)"]
NODEAPI["delonix-node-api<br>o contrato de no delonix.node.v1 SERVIDO: gRPC e HTTP/JSON<br>dos mesmos .proto num socket unix, so o proprio uid;<br>hoje ListProviders (ADR-0050 D5), o resto UNIMPLEMENTED"]
NODEAPIBIN["delonix-node-api-bin<br>o executavel delonix-node-api, que `delonix serve node-api` executa"]
MCP["delonix-mcp<br>servidor MCP (ADR-0025) — superficie de IA LOCAL, sem inquilino<br>stdio-only; tools chamam Store/dominio, nunca shell arbitrario"]
BIN --> RT
BIN --> IMG
BIN --> VM2
BIN --> VOL
BIN --> NET
BIN --> CRI
BIN --> MGMT
BIN --> SCAN
BIN --> SEC
BIN --> PVE
BIN --> NAS
BIN --> OPN
MCPBIN --> MCP
MGMTBIN --> MGMT
NODEAPIBIN --> NODEAPI
NODEAPIBIN --> NODECTX
NODEAPIBIN --> TEL
NODEAPI --> VM2
NODEAPI --> PVE
NODEAPI --> RT
NODEAPI --> NET
NODEAPI --> VOL
NODEAPI --> COMPUTE
NODEAPI --> NODECTX
NODEAPI --> MODEL
BIN --> MODEL
BIN --> STACK
BIN --> COMPUTE
BIN --> TEL
MGMT --> RT
MGMT --> IMG
MGMT --> VM2
MGMT --> VOL
MGMT --> NET
MGMT --> SCAN
MGMT --> TEL
SCAN --> IMG
CRI --> RT
CRI --> IMG
CRI --> NET
CRI --> TEL
VM2 --> COMPUTE
NET --> RULES
PVE --> NETCTX
VM2 --> LVIRT
VM2 --> CHV
CHV --> COMPUTE
CHV --> MODEL
CHV --> NODECTX
LVIRT --> COMPUTE
LVIRT --> MODEL
LVIRT --> NODECTX
OPN --> NETCTX
NET --> NETCTX
NETCTX --> MODEL
NETCTX --> COMPUTE
NETCTX --> RULES
BIN --> NETCTX
STATECRATE --> MODEL
COMPUTE --> NODECTX
COMPUTE --> MODEL
CRI --> NODECTX
CRI --> COMPUTE
CRI --> MODEL
RT --> NODECTX
RT --> COMPUTE
RT --> MODEL
MCP --> NODECTX
MCP --> COMPUTE
MCP --> MODEL
MCPBIN --> NODECTX
MGMT --> NODECTX
MGMT --> COMPUTE
MGMT --> MODEL
MGMTBIN --> NODECTX
NODECTX --> MODEL
IMG --> NODECTX
IMG --> COMPUTE
IMG --> MODEL
PVE --> COMPUTE
PVE --> MODEL
BIN --> NODECTX
SCAN --> MODEL
NET --> NODECTX
NET --> COMPUTE
NET --> MODEL
SEC --> NODECTX
SEC --> MODEL
STACK --> MODEL
STATECRATE --> NODECTX
STATECRATE --> COMPUTE
NAS --> MODEL
OPN --> MODEL
VM2 --> NODECTX
VM2 --> MODEL
VOL --> NODECTX
VOL --> COMPUTE
VOL --> MODEL
RT --> STATECRATE
NET --> STATECRATE
IMG --> STATECRATE
VOL --> STATECRATE
VM2 --> STATECRATE
BIN --> STATECRATE
CRI --> STATECRATE
MGMT --> STATECRATE
MCP --> STATECRATE
MCP --> VM2
MCP --> VOL
MCP --> NET
MCP --> MGMT
Notas de leitura do grafo (todas verificadas):
delonix-runtime-binDEPENDE dedelonix-cri— uma versão anterior deste documento afirmava o contrário («são dois binários independentes»), e isso deixou de ser verdade quando odelonix serve cripassou a servir o CRI a partir da própria CLI. Confirmado embins/delonix-runtime-bin/Cargo.toml:21. Odelonix-cricontinua a ter o seu[[bin]]próprio, e é a razão de ohyper/tonic/tokio-rustlsjá estarem na árvore do-binsem custo novo de supply-chain (ver a secção do proxy L7 noAGENTS.md). O CRI, por sua vez, não depende dedelonix-vmnem dedelonix-volume(sóruntime/image/net/core—crates/interfaces/delonix-cri/Cargo.toml).delonix-mgmté o crate com mais dependências internas (sete:runtime,image,vm,volume,net,scan,core) e é isso que a sua função exige — responde por todo o motor a um control-plane local. Nada depende dele a não ser o-bin, e é local por desenho: socket unix,SO_PEERCRED, só o próprio uid (ADR-0010).delonix-linuxsó depende decore— o motor de containers não conhece rede: a integração faz-se por inversão de controlo, com o hookon_starteddoRunSpec(a CLI passa closures que chamamdelonix-sdn).delonix-vmjá não depende dedelonix-sdn(P3i): o backend Cloud Hypervisor liga otapda VM à bridge do ingress pela portaVmNetworkdodelonix-compute, que odelonix-sdnimplementa (vm_network::HostVmNetwork) e o binário regista.delonix-telemetry(P3j) tem o logging estruturado, os spans OTLP e o registo Prometheus partilhado, que saíram dodelonix-runtime-core: a fundação deixou de carregar um exportador que todo o crate compilava só por precisar de umContainer.delonix-net-rulesnão tem UMA dependência — nem interna nem externa, e é isso que o torna atravessável: é o que odelonix-sdne o control-plane dodelonix-paascompilam os dois para responderem o MESMO nome de bridge, o mesmo IP dentro de um prefixo, a mesma leitura de10mbit. Pôr isso atrás de um endpoint HTTP pagaria um salto de rede — e um modo de falha novo — para calcular o que os dois lados já sabem calcular; e duas implementações que TÊM de concordar são duas que um dia não concordam. Odelonix-sdnre-exporta tudo, portanto nenhum consumidor mudou.delonix-proxmox,delonix-truenasedelonix-opnsenseestão fora dos crates de motor de propósito, e pela mesma razão escrita nos trêsCargo.toml: trazem um cliente HTTP (reqwest), e um crate de motor não cresce um. Odelonix-vmtem quatro dependências, odelonix-volumetrês e odelonix-sdncinco — meter lá o cliente para falar com UM alvo remoto trocaria isso por uma árvore que todo o motor passa a arrastar. Por isso o backend Proxmox (ADR-0008) implementa oVmBackende oGatewayProviderdo OPNsense (ADR-0051) implementa a porta do contextodelonix-networking(ADR-0059 F2a; até lá vivia nodelonix-sdn), os dois a partir de fora, e registam-se (register_backend/register_gateway_provider), com o alvo conhecido só pelo-bin, não pelo motor.- Nada depende de
delonix-proxmox,delonix-truenasnemdelonix-opnsensea não ser o-bine, no caso dodelonix-proxmox, odelonix-node-api(que o lista entre os providers do nó, como odelonix provider lsfaz) — são folhas do grafo, e é o que permite que um alvo remoto mal configurado avise e siga em vez de parar umcontainer ls. delonix-mcpfica fora dos oito crates de motor dependency-clean, pela MESMA razão dodelonix-mgmt(ADR-0025): traz o SDK MCP oficial (rmcp) e o seu próprio runtimetokio, e nenhum crate de motor depende dele. Depende dedelonix-mgmtpara reaproveitar odashstats::collectjá existente (runtime.info/metrics.querynão derivam os mesmos números uma segunda vez) — é o único crate novo que depende de outro crate "fora do motor" em vez de só dos oito. Stdio-only nesta fase: um processo filho por sessão do cliente de IA, nunca um daemon nem um socket loopback — a variante remota/multi-tenant do desenho original ficou recusada por ADR-0010 e pertence aodelonix-paas, não aqui.- Módulos internos que importam ao desenho:
delonix-sdn::infra— todo o plano rootless (holder, slirp único, publish/DNAT, DNS/DHCP/RA, firewall por container, egress policy,attach_container);delonix-sdn::wg— WireGuard entre nós (cifra o overlay; o holder cria a interface no netns de infra);delonix-sdn::cni— compatibilidade com plugins CNI reais (opt-inDELONIX_CNI=1,cni::enabled_conf);delonix-oci::overlay—mount_rootfs(overlayfs) eexport_rootfs(achatamento, o caminho rootless);delonix-oci::registry— cliente HTTP de registo, com verificação de digest;delonix-oci::buildpack— Cloud Native Buildpacks (CnbPlan);delonix-state::{secret,cred_vault}— Secret Manager do runtime (--secret/--secret-files; os valores decifrados só tocam um tmpfs dentro do namespace do container —write_secret_filesemcrates/adapters/delonix-linux/src/lib.rs);delonix-runtime-bin::cmd::{cluster,k8s_recipes,vmimage,remote}— o plano de cluster kubeadm (fluxo d, abaixo).
System design — fluxos principais
(a) container run rootless com -p, em --net host (default)
Com -p e sem rede custom, o container deixa de partilhar a rede do host e ganha um
netns próprio servido por um slirp4netns dedicado — o comportamento do
docker run -p no modelo rootless do Podman (cmd_run em
bins/delonix-runtime-bin/src/cmd/container.rs; spawn em
crates/adapters/delonix-linux/src/lib.rs; slirp_attach em crates/adapters/delonix-sdn/src/lib.rs).
sequenceDiagram
participant U as operador
participant CLI as delonix run
participant K as kernel
participant INIT as container_init filho
participant S as slirp4netns dedicado
U->>CLI: delonix container run -p 8080:80 imagem
CLI->>CLI: parse_publish valida os -p antes de criar seja o que for
CLI->>CLI: resolve_or_pull + prepare_rootfs overlay ou export achatado
CLI->>K: clone com NEWNS NEWUTS NEWPID NEWIPC NEWNET NEWUSER
K-->>CLI: pid do filho
Note over CLI,INIT: filho bloqueia num pipe a espera do GO do userns
CLI->>K: write_userns_maps — newuidmap e newgidmap com gama subuid se ha helpers
CLI->>INIT: byte GO pelo pipe de sincronizacao
INIT->>INIT: setup_rootfs — mount privado, bind do rootfs, volumes, pivot_root, proc, sys RO
INIT->>INIT: caps, seccomp, no_new_privs, sysctls, ulimits, secrets em tmpfs
CLI->>CLI: setup_cgroup + store.save Running
CLI->>S: hook on_started — slirp_attach ao pid
S-->>CLI: byte ready pelo ready-fd
CLI->>S: add_hostfwd 8080 para 80 via api-socket
INIT->>INIT: execve do comando da imagem
alt foreground
CLI->>K: waitpid — Status final com exit code
else detach
CLI-->>U: imprime o id e termina — sem monitor residente
end
Pontos estruturais: a ordem userns-GO → console/log é crítica e está documentada em
spawn (deadlock caso contrário); o log shim em detach faz setsid e larga o stdio
para o run -d não pendurar quem lhe capture o stdout; o slirp morre com o netns do
container e há um reaper de órfãos (reap_orphan_slirp) porque, sem daemon, um
crash do container não tem quem limpe na hora.
(b) run --net <rede> -p — publicação pelo ingress partilhado
Com rede custom, não há slirp por container: o container junta-se a uma netns criada
pelo holder e a porta publica-se com um add_hostfwd no slirp único + um DNAT na
tabela nft dentro do netns de infra (infra::attach_container,
infra::publish_port, do_publish — crates/adapters/delonix-sdn/src/infra.rs).
sequenceDiagram
participant CLI as delonix run --net web -p 8080:80
participant H as holder netns
participant SL as slirp4netns unico
participant C as container
CLI->>H: ensure_up — arranca holder e slirp se nao existirem, refcount
CLI->>H: attach_container pelo control socket SO_PEERCRED
H->>H: ip netns add + veth para a bridge da rede + IP deterministico + anti-spoofing
H-->>CLI: nome da netns e IP do container
CLI->>SL: publish_port — add_hostfwd 8080 para tap0 8080 via api-socket
CLI->>H: publish 8080 para IP 80 — DNAT nft na chain pre do netns de infra
Note over CLI,H: regras criadas ANTES do arranque — o IP ja e conhecido
CLI->>C: clone com join_netns = /run/netns/nome — setns em vez de NEWNET
Note over C: LIMITACAO rootless: o setns a netns do holder falha p/ um run normal — ver Limitacoes
Note over H,SL: publish e unpublish sao estado do dataplane, nao do processo:<br>reconfiguracao a quente sem parar o container
CLI->>SL: stop ou rm — unpublish_port remove hostfwd
CLI->>H: unpublish — remove o DNAT; detach_container desfaz o veth
O que torna o hot reconfig possível: a publicação vive no slirp (hostfwd) e no nft
do holder — nenhum dos dois pertence ao processo do container. unpublish_ports
corre no stop/rm (cmd/container.rs), e reap_orphan_hostfwds reconcilia o
slirp contra os containers vivos quando algo morreu sem limpar. O mesmo dataplane
suporta firewall por container (fw_chain_body, despachada em O(1) pelo verdict map
fwmap na chain fwcont), egress policy e rate-limit (do_netrate).
Restringir uma porta publicada por CIDR de origem funciona (net ingress allow <c>
<porta> --from <cidr>): o endereço do cliente sobrevive ao hostfwd para qualquer origem
roteável. A excepção é o cliente no loopback do próprio host, que chega como o gateway
do slirp (delonix_sdn::SLIRP_GW) porque a libslirp não tem rota de volta para
127.0.0.1 — testar uma regra dessas com curl localhost falha por essa razão, não
por causa da regra.
(c) Pod CRI — sandbox e join_netns
O CRI (crates/interfaces/delonix-cri/src/runtime_svc/lifecycle.rs) guarda o estado
CRI em JSON próprio e delega no binário delonix as operações que usam clone
— o servidor é multi-thread (tokio) e clone só é seguro single-threaded (decisão
documentada no cabeçalho de lifecycle.rs).
sequenceDiagram
participant KL as kubelet
participant CRI as delonix-cri
participant D as binario delonix
participant H as holder netns
KL->>CRI: RunPodSandbox
alt host network — namespace_options NODE
CRI->>CRI: sem netns proprio — regista host_network true
else rootless com CNI opt-in DELONIX_CNI=1
CRI->>H: cni_attach_container — plugins CNI correm no holder, IP do IPAM
else rootless SDN nativo
CRI->>D: netns attach cri-id — netns partilhada do ingress
else root
CRI->>D: pod create pod-cri-id --network — infra container estilo pause
end
CRI-->>KL: pod_sandbox_id
KL->>CRI: CreateContainer + StartContainer
CRI->>D: run detached com --pod — RunSpec.join_netns p/ a netns do sandbox
Note over D: log em formato CRI — RunSpec.log_cri: rfc3339nano stdout F linha
KL->>CRI: ContainerStatus
CRI->>CRI: load_reconciled — reconcile_status contra o kernel antes de responder
Note over CRI: crash detectado vira Crashed 137 — o kubelet com OnFailure reage
KL->>CRI: StopPodSandbox e RemovePodSandbox
CRI->>D: stop e rm -f de cada container, depois desfaz o netns ou o infra container
Os logs no formato CRI são escritos pelo mesmo log shim do fluxo (a) —
RunSpec.log_cri (crates/adapters/delonix-linux/src/lib.rs) — para o kubectl logs/
crictl logs lerem directamente o ficheiro. exec/attach/port-forward do
kubectl passam por streaming.rs/spdy.rs (servidor de streaming próprio, SPDY).
(d) cluster kubeadm — da imagem dourada ao cluster
Duas camadas com a mesma fundação (bins/delonix-runtime-bin/src/cmd/cluster.rs):
cluster apply -f cloud.yaml faz o bootstrap kubeadm em hosts já vivos;
cluster kubeadm --name N --control-plane 1 --workers M provisiona primeiro as VMs
(imagem dourada) e depois chama a mesma apply_one (provision_and_apply —
zero duplicação da lógica kubeadm/SSH/validação).
sequenceDiagram
participant U as operador
participant CL as delonix cluster kubeadm
participant VS as VmImageStore
participant VM as delonix vm create
participant R as hosts remotos via ssh
U->>CL: cluster kubeadm --name demo --control-plane 1 --workers 2
CL->>VS: resolve_vm_image — a explicita, ou a UNICA existente; erro claro com 0 ou N
Note over VS: imagem dourada = Ubuntu cloud image validada por SHA256SUMS +<br>virt-customize com k8s_customization_steps: kubeadm kubelet kubectl,<br>swap off, modulos e sysctls, delonix-cri + unit systemd — cmd/vmimage.rs
CL->>CL: gera ou carrega chave SSH ed25519 em root/clusters/nome
loop cada VM cp1..N w1..M
CL->>VM: delonix_vm create — disco = imagem dourada + seed ISO cloud-init por instancia
CL->>R: wait_for_vm_ssh_ready — IP via vm status, depois ssh_check real, timeout
end
CL->>CL: constroi ClusterSpec em memoria + validate — whitelists valid_endpoint valid_cidr valid_version
Note over CL: validate corre ANTES de qualquer interpolacao em comando remoto — anti RCE por manifesto
loop cada host — sequencial
CL->>R: prepare_host — k8s_recipes: cada passo tem check e apply, idempotente sem ficheiro de estado
end
CL->>R: kubeadm_init no 1.o control-plane — salta se admin.conf ja existe
R-->>CL: JoinInfo — token e hashes
loop restantes control-planes e workers
CL->>R: kubeadm_join — salta se kubelet.conf ja existe
end
CL->>R: traz o kubeconfig p/ root/clusters/nome-kubeconfig.yaml — copia p/ ~/.kube/config se nao existir
Note over CL: LIMITACAO: cluster kubeadm recusa mais de 1 control-plane —<br>HA exige endpoint estavel LB ou VIP, que este comando ainda nao provisiona
k8s_recipes.rs é deliberadamente o catálogo partilhado entre o build da imagem
dourada (vmimage::build, offline via virt-customize) e a preparação ao vivo por
SSH (cluster apply) — um host preparado por qualquer dos caminhos fica idêntico.
Limitações conhecidas
Cada uma confirmada no código na data deste documento; várias já estão anotadas nos diagramas acima.
--net <rede>custom em rootless falha nosetnspara umrunnormal. A netns nomeada é criada pelo holder, dentro do userns mapeado do holder (infra::start_holder); o processo do container, sem privilégio nesse userns, não a consegue abrir. O mecanismo certo já existe no motor — re-exec viansenter … ip netns exec+RunSpec.inherit_userns(crates/adapters/delonix-linux/src/lib.rs) — mas hoje só é usado pelo próprio holder; não há nenhum caminho emdelonix-linux/delonix-runtime-binque o faça para umcontainer runnormal. Fechar isto é trabalho do motor, não da CLI.delonix buildé single-stage. Um Dockerfile/Delonixfile comFROM … AS xseguido doutroFROMé recusado com erro claro (cmd/build.rs); multi-stage precisa de desenhar a passagem de rootfs entre estágios.macvlan/ipvlan/overlaynão têm attach de containers.network createregista-os noNetworkStoredeclarativo (crates/adapters/delonix-sdn/src/lib.rs), mas o plano físico do holder (infra::network_create_with) só é orquestrado para o driverbridge— o único a que containers se ligam hoje.Container.restart_policynão é consumida. O campo existe emdelonix_compute::Container, mas nada no motor nem na CLI o lê para containers (grep confirmado); sem daemon, não há quem reinicie. Nas VMs a política é materializada apenas pelo backend libvirt (on_crash); no Cloud Hypervisor fica não-supervisionada e o código avisa (restart_policy_unsupervised,crates/adapters/delonix-vm/src/lib.rs).cluster kubeadmprovisiona só 1 control-plane.--control-plane > 1é recusado — HA exige um endpoint estável (LB/VIP) que o comando ainda não cria.cluster applyjá suporta HA comcontrolPlaneEndpointexterno; só etcdstacked(o modoexternalé reconhecido e recusado); execução sequencial entre hosts (cmd/cluster.rs, cabeçalho).- Exit code real perde-se em detach. Em foreground o
waitpidproduzStopped/Failed(n)/Crashed(Status::from_wait,crates/contexts/delonix-compute/src/record.rs); em detach não há monitor, e a reconciliação posterior só consegue classificar um processo desaparecido comoCrashed(reportado como 137) — o código de saída verdadeiro já não é observável. Trade-off directo do daemonless (ver ADR-1). - A delegação interna do
delonix-criassume superfícies de CLI que o bináriodelonixdeste repo não expõe.lifecycle.rs/streaming.rsinvocamcurrent_exe/self_bincom formas de topo (run,stop,rm,netns attach,pod create, sobDELONIX_INTERNAL=1) herdadas do binário do monorepo de origem, onde CLI e CRI viviam no mesmo executável; odelonixopensource só tem os grupos (container run, …) e o re-exec ocultonetns holder. Empacotar o CRI standalone (o próximo milestone do README) implica fechar este contrato interno. --privilegednão arranca imagens systemd-aninhado tipokindest/node— crash cedo na deteção de cgroup do entrypoint, causa-raiz por isolar; existe já lógica dedicada de delegação cgroup2 para nodes Kind (setup_node_cgroup_ns, activada por--privileged+ labelio.x-k8s.kind.*), mas não é suficiente. Detalhe da investigação noAGENTS.md(secção "Cluster modo Kind sem Docker").
Decisões de arquitectura (mini-ADRs)
ADR-1 — Daemonless: nenhum processo residente obrigatório
Decisão. O runtime não tem daemon: a CLI faz clone directo e sai; o estado é
partilhado por ficheiros JSON; processos auxiliares (log shim, watcher --rm,
slirp, holder) nascem por necessidade e morrem com o recurso.
Porquê. Menos superfície de ataque e de falha (não há um dockerd cuja queda
leve os containers); arranque instantâneo; auditabilidade — cada processo tem um
dono e um ciclo de vida óbvios.
Trade-offs assumidos. Sem monitor por container: exit codes reais perdem-se em
detach (limitação 6); restart_policy de containers não tem executor (limitação 4);
a verdade sobre "está vivo?" é reconstruída na leitura (reconcile_status +
safe_to_signal contra reutilização de PID) em vez de mantida por eventos; e a
limpeza de órfãos precisa de reapers explícitos (reap_orphan_slirp,
reap_orphan_hostfwds). Um delonixd opcional é uma evolução discutida, não uma
peça existente.
ADR-2 — Rootless-first com subuid/newuidmap
Decisão. O caminho rootless é o desenho principal, não um modo degradado:
CLONE_NEWUSER + write_userns_maps, que usa a gama subuid/subgid completa via
newuidmap/newgidmap quando os helpers existem (have_subid_helpers,
crates/adapters/delonix-linux/src/lib.rs), com fallback para mapa de um só uid.
Porquê. Correr imagens reais (nginx faz chown para o uid 101; USER ≠ 0 via
RunSpec.run_uid) exige uma gama de uids, não só o root mapeado. A fronteira
rootless→root foi auditada ofensivamente (holder valida SO_PEERCRED; join_netns
só recebe caminhos gerados server-side; nenhum caminho de mapeamento atinge o uid 0
real do host).
Trade-off. Dois regimes de código em pontos sensíveis (ex.: setup_dev antes do
pivot em root vs setup_dev_userns depois do setuid) e dependência dos helpers do
sistema.
ADR-3 — Ingress partilhado (holder + slirp único) vs um slirp por container
Decisão. Coexistem dois modelos: -p sem rede custom usa um slirp4netns por
container (simples, morre com ele); redes custom usam o ingress partilhado — um
holder netns com a bridge delonix0, UM slirp como ponte host↔infra e DNAT nft lá
dentro (crates/adapters/delonix-sdn/src/infra.rs, doc do módulo: "substitui, a prazo, o
modelo 1 slirp por container").
Porquê. O ingress dá rede entre containers (bridge comum, DNS/DHCP internos),
firewall/egress/rate-limit por container e — porque publish/unpublish são estado do
dataplane — reconfiguração de portas a quente. Um slirp por container não dá nenhuma
destas.
Trade-offs. Dois processos de infra de longa duração (geridos por ref-count, não
por daemon); toda a configuração dentro do netns tem de ser executada pelo próprio
holder (não há nsenter --user --net a partir do host — gotcha documentado no
módulo); e a limitação 1 enquanto o re-exec não chegar ao run normal.
ADR-4 — Estado em ficheiros JSON, não numa BD
Decisão. Persistência por um-JSON-por-registo (Store/JsonStore,
crates/adapters/delonix-state/src/store.rs) sob $DELONIX_ROOT; o CRI e o ingress
seguem o mesmo padrão (ficheiros de pid/refcount/status).
Porquê. Sem daemon não há dono natural de uma BD; ficheiros são inspecionáveis
(cat), sobrevivem a crashes de qualquer processo e não acrescentam dependências.
A compatibilidade com formatos antigos faz-se no Deserialize (ex.: Status aceita
o legado {"Exited": code}).
Trade-offs. Sem transacções nem queries; a coerência com o kernel é eventual
(reconciliação na leitura); concorrência resolvida caso a caso (lockfile no
refcount do ingress).
ADR-5 — Idempotência sem-estado no cluster apply (Terraform sem tfstate)
Decisão. Cada passo de preparação de host é um par check/apply
(HostRecipe, cmd/k8s_recipes.rs); kubeadm_init/kubeadm_join verificam
/etc/kubernetes/{admin,kubelet}.conf no host antes de agir. Não existe ficheiro de
estado local.
Porquê. A condição real no host é a única verdade — nunca dessincroniza de um
.tfstate porque não há nenhum; re-correr o apply é sempre seguro. O catálogo é
partilhado com o build da imagem dourada para os dois caminhos nunca divergirem.
Trade-offs. Cada execução re-verifica tudo (mais lento, hoje sequencial). O
stack apply local partilha a ausência de ficheiro de estado, mas desde a v0.47.0
converge: calcula um plano (stack plan) e reconfigura portas, volumes, redes,
memória e CPU a quente, sem mudar o PID, recusando com o nome do campo o que
obrigaria a recriar. Continua fail-fast sem rollback, e continua a ser convergência
a pedido — um loop de reconciliação contínua é, por decisão, trabalho de um
orchestrator externo e não deste runtime.
Nota de segurança. Tudo o que entra num comando remoto é validado por whitelist
antes de qualquer interpolação (valid_endpoint/valid_cidr/valid_version,
cmd/cluster.rs) — shell_quote protege a fronteira ssh, nunca o conteúdo.
ADR-6 — CRI como fronteira Kubernetes (e não um kubelet embutido)
Decisão. A integração k8s é exclusivamente o servidor runtime.v1
(crates/interfaces/delonix-cri): o kubelet manda, o Delonix executa. O servidor delega as
operações com clone num processo single-threaded e mantém o estado CRI à parte
(lifecycle.rs).
Porquê. O CRI é a interface estável e estreita do ecossistema — o Delonix
substitui containerd/CRI-O sem inventar protocolo; scheduling/restart/GC ficam do
lado do kubelet, coerente com o daemonless (o kubelet é o monitor que o runtime não
tem). A imagem VM dourada já embala o delonix-cri como serviço systemd, fechando o
ciclo com o fluxo (d).
Trade-off. A limitação 7 (contrato interno CRI→CLI herdado da extracção) é o
preço em aberto de ter separado os dois binários.
ADR-7 — Fronteira público/privado com o PaaS
Decisão. Este repositório compila sozinho e não conhece tenant, licença,
billing nem consola. delonix-model e os contextos são a base que o lado privado reexporta —
nunca o inverso (doc do crate). Ficam aqui as peças que são mecanismo genuíno de
runtime/SDN: Secret Manager (secret/cred_vault), WireGuard do overlay (wg.rs).
Ficam fora as políticas de plataforma (quem publica o quê, planos, quotas) — os
diagramas param no "consumidor externo".
Porquê. Um motor opensource auditável exige uma árvore de dependências fechada
(cargo tree -e normal sem crates privados) e uma linha clara entre mecanismo e
política.
Trade-off. Alguma duplicação aparente de conceitos entre os dois lados (ex.: um
cofre de secrets do runtime vs um cofre de plataforma) é deliberada, não acidental.