Repository navigation
Releases: ngolacloud/delonix
Release list
v0.1.0 — Delonix: a primeira release da nova identidade
v0.1.0 — Delonix: a primeira release da nova identidade
Esta release abre a linha Delonix. O produto delonix-runtime passou a chamar-se
Delonix e o repositório mudou para github.com/ngolacloud/delonix. O número de
versão foi re-baseado de propósito de 5.1.1 para 0.1.0 — uma linha nova, não
uma regressão (ver VERSIONING.md).
O motor é o mesmo. Toda a capacidade herdada da linha delonix-runtime
(v1.0.0 … v5.1.1) continua: containers e microVMs rootless-first e daemonless,
pods, OCI, SDN rootless, VMs (Cloud Hypervisor/libvirt/Proxmox), delonix-cri,
cluster kubeadm, os Kinds declarativos e os providers. Nada disto é trabalho
novo da 0.1.0 — o histórico está no CHANGELOG.md,
secção «linha delonix-runtime». As releases v1.*…v5.1.1 ficam publicadas e
imutáveis.
O que esta release muda
- Identidade: nome, repositório (
ngolacloud/delonix), site
(ngolacloud.github.io/delonix), imagem (ghcr.io/ngolacloud/delonix). O binário
da CLI continua a chamar-sedelonix. - Linha de versão:
scripts/version_gate.pypassou a ser consciente de linha — a
linhadelonix-runtime(majors 1..5) está congelada e a linha Delonix abre em
0.1.0; a comparação é dentro da linha. - Política de compatibilidade (pré-1.0): enquanto em
0.x, um minor pode
quebrar contratos (anunciado comoBREAKINGno CHANGELOG); patch é compatível.
A estabilização para SemVer estrito chega com1.0.0.
O que NÃO muda (contratos operacionais)
Uma instalação 5.1.1 mantém tudo ao passar para 0.1.0: estado em
/var/lib/delonix e ~/.local/share/delonix, config em /etc/delonix, sockets
/run/delonix-*.sock, serviços systemd, variáveis DELONIX_* e os grupos de
schema *.delonix.io.
Actualizar
Ver o guia de migração.
Em resumo: reinstalar pelo install.sh (o "latest" do GitHub é por data, resolve
para 0.1.0); os cinco binários (delonix, -cri, -mcp, -mgmt, -node-api)
têm de ficar todos na mesma versão. Um auto-update por ordem SemVer não dispara
(0.1.0 < 5.1.1); é uma transição de linha, anunciada.
Limitações conhecidas
- A documentação regenerada já usa a identidade nova; as páginas do handbook
(docs/handbook/) regeneram quandodocs/dev/rt4-phase1-impl-map.mdentrar no
catálogo de páginas. - Imagens GHCR da linha anterior ficam no namespace antigo; a partir desta linha
publicam-se emghcr.io/ngolacloud/delonix.
v5.1.1 — o instalador mostra onde está a release, e o `--l18n` deixa de ser aceite depois de um subcomando
v5.1.1 — o instalador mostra onde está a release, e o --l18n deixa de ser aceite depois de um subcomando
Release de correcção sobre a v5.1.0, com três commits em cima da tag e
nenhum merge (git rev-list --count 1843b321..a0da90f5): uma linha nova no fim
do instalador, uma mudança à flag --l18n com dois helpers sem chamador, e o
aumento de versão com a documentação regenerada. Estas notas foram escritas
depois de a release sair; a release foi publicada com as notas geradas pelo
GitHub.
Ler antes de actualizar: --l18n depois de um subcomando passa a ser um erro (cb047291)
A flag --l18n perdeu o global = true. A intenção era que ela deixasse de
aparecer no --help de todos os subcomandos. Mas o global também é o que deixa a
flag ser lida depois de um subcomando, por isso a mudança fez mais do que
esconder a flag. Medido no binário publicado (delonix 5.1.1, commit a0da90f5):
| Invocação | Resultado |
|---|---|
delonix --l18n=pt volume --help |
ajuda em português |
delonix volume --help --l18n=pt |
ajuda em português |
DELONIX_L18N=pt delonix volume inspect x |
erro[DX-4201] … (em português) |
delonix --l18n=pt volume inspect x |
erro[DX-4201] … (em português) |
delonix volume inspect x --l18n=pt |
rc=2, error: unexpected argument '--l18n' found |
delonix volume ls --l18n=pt |
rc=2, o mesmo erro |
O --help continua a funcionar em qualquer posição, porque a língua é decidida
lendo o argv antes de o clap o processar, e o --help sai durante o
processamento. O que deixa de funcionar é um comando normal com a flag no fim.
Isto contradiz o próprio texto de ajuda da flag, que manda usá-la com
<command> --help, e os exemplos do manual que a tratam como global.
Na v5.1.1, o que funciona sempre é pôr a flag antes do subcomando
(delonix --l18n=pt …) ou usar DELONIX_L18N=pt.
A correcção já está na main (d22e9336, #862): a flag volta a ser global
e fica com hide = true, ou seja, é aceite em todo o lado e não aparece em
ajuda nenhuma. Sai na próxima release.
O instalador mostra o endereço da release no fim (54179fdb)
Quando a instalação termina sem falhas, o install.sh imprime agora uma linha
downloads and documentation: com o endereço da release instalada.
Com a instalação por omissão, o primeiro endereço está partido. Sem
--version, o VERSION do script vale latest, e o instalador imprime
…/releases/tag/latest (HTTP 404) e, a seguir, …/releases/latest (redirecciona
para a release mais recente). Com --version v5.1.1 imprime só
…/releases/tag/v5.1.1 (HTTP 200). Medido com curl a 2026-10-21. A main
tem a mesma linha; a correcção fica para outro commit.
Dois helpers de template, ainda sem chamador (cb047291)
O build.rs passa a ler a chave tls= do template.meta, e o scaffold.rs
ganha template_supports_tls e template_has_version. Na v5.1.1 nada os chama
(git grep na tag), e é por isso que o clippy -D warnings da CI chumba a tag
com dead_code nas duas funções. Na main passaram a ser usados para esconder
--tls-port/--hostname e -v/--template-version da autocompletação dos
templates que não os aceitam. Isso também sai na próxima release.
Versão e documentação (a0da90f5)
O Cargo.toml passa a 5.1.1 e as páginas de docs/comandos/ são
regeneradas a partir do binário, para o gate de documentação do workflow de
release não falhar por documentação desactualizada.
Como foi validada esta release
Foi mal validada, e isso fica escrito aqui.
- Os três commits foram empurrados directamente para a
main, sem PR. A
ci.ymlcorre em push para amaine chumbou em54179fdbecb047291.
Ema0da90f5, a tag, chumbou orustfmt(diferenças emscaffold.rs) e o
clippy -D warnings(dead_codenos dois helpers acima, e
type_complexitynotemplates.rsgerado), e os outros jobs,test
incluído, foram cancelados. - Parte disto já vinha da
v5.1.0: a CI do commit da v5.1.0
(1843b321) também chumbava orustfmt, os documentos gerados e dois testes
(the_published_matrix_is_the_generated_onee
no_string_in_the_sources_tells_the_operator_to_run_a_dead_command). - O
release.ymlfalhou três vezes (08:57, 08:58 e 09:38 UTC de 2026-10-15)
antes de publicar à quarta, às 10:05 UTC. As corridas falhadas não foram
investigadas para estas notas. A corrida que publicou usou orelease.ymldo727c8d3d(amain
de então) e construiu a tag: o binário publicado diz
commit: a0da90f50. - Depois da publicação foram medidos, contra o binário publicado e contra o
GitHub, o comportamento do--l18ne os endereços do instalador, como
descrito acima. - Não corrido: a bateria
scripts/e2e.sh, o arnês de caos e ocargo test
sobre esta tag.
v5.1.0 — o que o motor não consegue impor passa a recusar, e os projectos que ele gera nascem com portão
v5.1.0 — o que o motor não consegue impor passa a recusar, e os projectos que ele gera nascem com portão
Duzentos e oitenta e nove commits desde a v5.0.2 (197 sem contar os merges),
de #723 a #843 — medidos em a46095a3
(git rev-list --count v5.0.2..a46095a3). O número vai com o commit em que
foi medido: sem isso envelhece em silêncio assim que a main cresce.
MINOR, e a razão é verificável: nada nesta release quebra um contrato que
alguma versão tenha declarado. As recusas novas (há muitas) caem todas sobre
pedidos que o motor já não conseguia cumprir e reportava como sucesso, ou
sobre superfícies que o docs/cli-stability.md lista como «sem promessa
declarada em nenhuma versão» — o init entre elas.
O fio é o mesmo das duas últimas releases, agora empurrado para os sítios onde
ainda havia um sim barato: o que o motor não consegue impor, recusa; o que
não entende, não engole; e o que não mediu, não afirma. A novidade é que
isso chegou ao que o motor gera — um projecto saído do delonix init
passa a nascer com testes, dois CIs, Conventional Commits, SonarQube e um
README que aconselha arquitectura, com um portão a exigi-lo.
Ler antes de actualizar
Nenhuma destas é uma quebra de contrato declarado, mas as quatro mudam o que
um comando que hoje devolve 0 faz amanhã.
- O CRI tem tecto de capabilities POR OMISSÃO (
#828). Sem configuração, o
tecto é oKEPT_CAPSdo motor em modoreject: um pod normal corre como
dantes, eprivileged: trueou umcapabilities.addacima desse conjunto
são recusados noCreateContainer. Ausente ou vazio já não quer dizer
«sem tecto» — a única saída é escrevê-la (DELONIX_CRI_CAP_CEILING=all), e
um valor ou modo inválido impede o servidor de arrancar em vez de cair para
«ilimitado». delonix vm image <verbo>é o único caminho (#772).image vm …,
image --vm …e as formas planasvm build|convert|pull|push|ls-remote
falham comunrecognized subcommand. Corte limpo, sem alias: o grupovm
está na lista dos não-estáveis.delonix initrecusa mais do que recusava (#842,#843): uma porta
que este host não consegue publicar pára o--upantes do build; um
init -t <outro>sobre um directório que já tem outro projecto é um
conflito (saída 5) em vez de «nothing to do» seguido de um build do projecto
errado; e uma chave desconhecida notemplate.metapára o build do próprio
motor.cluster kubeadmrecusa o CNI por omissão num cluster multi-nó
(#810), antes de criar uma única VM. Antes provisionava tudo e o cluster
ficava sem rede entre nós.
Um projecto gerado nasce com portão (#842, #843)
Começou num bug de produção: delonix init -t nginx --port 9000 --tls-port 943
gerava o projecto e o build morria sempre em nginx: [emerg] bind() to 0.0.0.0:943 failed (13: Permission denied), no passo que só devia verificar a
sintaxe da configuração.
Quatro medições, e cada uma mudou a correcção:
- o container de trabalho do
buildcorre na netns do HOST (mesmo inode),
onde a userns do motor não temCAP_NET_BIND_SERVICE; nginx -tbinda cadalistenque lê — não é um parser;- e tolera
EADDRINUSE: só oEACCESé fatal, por isso o gate óbvio
(«ocupa a porta e constrói») passaria sobre o binário defeituoso; - só o nginx tem o problema —
httpd -tehaproxy -cconstruíram com as
mesmas portas sem uma queixa.
O check passa a correr sobre uma cópia cujos dois listen são sockets unix. A
porta em si nunca foi inutilizável: dentro da netns do próprio container o
mesmo listen 943 devolve rc=0.
Os onze templates foram depois gerados com a porta adversarial e
construídos: 11/11. A varredura encontrou outro defeito, que fica aberto
e está escrito: init -t go ./go gera com exit 0 um módulo go que o Go
recusa, e -t django ./django um pacote que depende de si próprio.
E o contrato dos templates ganhou portão. Antes, nginx, httpd, haproxy e
odoo tinham zero CI, SonarQube e CONTRIBUTING.md; o nestjs tinha
perdido o commitlint dos dois irmãos; e nenhum dos onze tinha secção de
arquitectura no README. Agora todos têm, dois testes exigem-no, e o
scripts/init-ci.sh executa o CI gerado dos onze — o job do GitLab e os
passos do GitHub nos sete de aplicação, o ci-check.sh nos três de edge (que
constrói a imagem e a sonda de fora, a única forma em que o próprio smoke
é honesto) e o pre-commit no odoo.
Correr esse CI em vez de o ler encontrou três bugs nos projectos gerados: o
smoke.sh dos três de edge ignorava os URLs que recebe como argumento
(127.0.0.1 escrito à mão, logo FAIL contra qualquer servidor correcto fora
do loopback); um check do haproxy exigia um container gerido pelo delonix e
passou a skip com a razão; e o prettier da OCA reformatava os ficheiros
que o init gera, chumbando o gate do odoo num projecto intocado.
Segurança
- Toda a extracção de layer passou a ter tecto (
#828,#788): número de
entradas e bytes descomprimidos, contra uma bomba de descompressão, e todos
os caminhos confinados. - Um bit setuid alheio é largado no caminho de extracção como root real
(#800). - O portão de admissão falha FECHADO numa FALHA de scan (
#778) — não só
num resultado de scan com achados. A distinção é a que interessa: «não
consegui analisar» não é «está limpo». - Uma guarda opcional mantém as cargas fora de espaço privado e CGNAT atrás
do uplink (#819):denyPrivateEgressna política do nó, desligada por
omissão, com concessões por prefixo. Vive numa chain própria antes de tudo o
resto, por isso nem umaallowlist:de rede nem umegress allowa reabrem. cluster applypode fixar a chave de host do SSH em vez de confiar na
primeira ligação (#796), opt-in.- Uma admissão que não restringe nada é REGISTADA como tal (
#767). install.shverifica os user namespaces através do perfil AppArmor do
delonix (#775), que é o caminho real — não genericamente.- Avisos de dependência:
rustls0.23.46 (#839),unsafe-libyamlnomeado
com a razão (#833).
Kubernetes (CRI)
cpuset_mems,unifiedehugepage_limits: honrar-ou-recusar, com a
sonda feita no cgroup onde o container vai REALMENTE viver (ADR-0038 item 3,
#752).UpdateContainerResourcesimplementado e sensível ao lugar (ADR-0038
item 4,#755): sob um scope do kubelet são propriedades do systemd, fora
dele é escrita na leaf, e a fusão é campo a campo — «não especificado» num
update é «não tocar», não «sem limite».- Um
cpu_quota/cpu_periodparcial é recusado em vez de escrever um
limite de CPU absurdo (#766). - Um tecto de hugepage a zero sobre uma pool vazia não é um pedido
(#787). - O
serve criavisa alto quando não há tecto de capabilities (#794), e
nomeia o erro do motor, não a linha de progresso antes dele (#795). - O fallback de root do ADR-0062 chega ao
ContainerStatus.info(#750).
VMs
- Hotplug a quente de CPU e memória: Cloud Hypervisor (ADR-0068 fase 1,
#749) e, a seguir, libvirt e Proxmox, com uma mudança de forma a
reiniciar em vez de substituir a VM (ADR-0012/ADR-0068,#831) — o
Replacedeitava fora o disco overlay. - Prontidão de VMaaS (
#741): guest agent, ovm sshcorrigido na netns,
evm.disk.resize. etcdbackup/restore para um control-plane stacked provisionado em VM
(#747).- Uma VM cloud-init no Proxmox ganha a porta série em que o
console=ttyS0
arranca (#781) — sem ela o primeiro boot entrava em pânico. - Avaliação ao vivo das capacidades de Proxmox e Cloud Hypervisor (
#744,
#742), com a correcção do índice de tokens da firewall de vnet. - Um teste ao vivo que não corre já não passa em silêncio (
#779,#802):
é#[ignore], e um portão mantém-no assim.
API de nó
VirtualMachineServiceservido (ADR-0040 P5,#746), e a resolução das
omissões de vCPU/memória/backend que a própria imagem declara (#763).- Activação por socket e saída por inactividade (ADR-0040 P5,
#822). - Corrida no livro de operações e identidade de idempotência corrigidas
(#762, ADR-0076 em#765). - O passo E do ADR-0042 validado ao vivo pelo socket (
#814).
Rede, IPAM e DNS
- Uma zona nomeia o seu controlador de IPAM, e as subnets mudam no lugar
(ADR-0063 D1/D2,#754): o gateway e os ranges passaram a campo quente, e
uma entrada de gateway movida por um rollback é reparada. - O plano lê as entradas de gateway do IPAM (
#834): antes, uma entrada
libertada à mão planeava «sem alterações». - Uma reserva de zona pode nomear uma carga que é uma VM (ADR-0063 D3.3,
#768). - O motor limpa os registos de gateway que um nó Proxmox deixa para trás
(ADR-0064 D6,#758), e as escritas da firewall de vnet recusam
incondicionalmente em vez de falharem a meio (#757). - Uma rota de rede é utilizável a partir de uma rede adicional (
#733). - Um arranque falhado já não desfaz a rede de um container que está a
correr (#801).
Armazenamento
- Pools de armazenamento (ADR-0067 P1–P4/P6,
#830): o helper root
activado por socket e os driversbtrfs,zfselvm-thin, com posse
marcada e recusa acima dos 95 %. - Encolher uma quota rígida recusa um uso que não se conseguiu medir
(#782) — desconhecido nunca é zero.
Compose, Docker API e CLI
- Compose contra uma amostra real, credenciais de serviço (ADR-0048 fase
3) e a API Docker ao nível do Testcontainers (#821). container pssem-alista só o que corre, e a API Docker honra o
alle devolve as portas (#816).vm image <verbo>como ponto de entrada único (#772).- O
COPYdo VMfile mantém o significado que tem num Dockerfile (#783). secret rotate-keyjá não faz a versão recuar (ACH-034,...
v5.0.2 — uma rota entre redes passa a servir os containers ligados a mais de uma
v5.0.2 — uma rota entre redes passa a servir os containers ligados a mais de uma
Release de correcção sobre a v5.0.1, cortada do mesmo ramo de manutenção
(release/5.0.x) com exactamente um commit de código em cima da tag — o
cherry-pick -x de delonix-runtime#733. Os commits que entraram na main desde
a v5.0.0 e trazem funcionalidade nova continuam fora: saem na próxima MINOR.
Uma NetworkRoute aberta não passava tráfego a um container ligado por network connect (9d0f5ec2, #733)
Uma NetworkRoute é um par iifname A . oifname B no mapa @netpair do holder:
serve um pacote que ENTRA no holder pela bridge da rede A. Um container ligado à
rede A com network connect (uma rede adicional, não a primária) ficava com a
tabela de rotas que tinha: um pacote para a rede B saía pela rota por omissão,
entrava pela bridge da rede PRIMÁRIA, e nunca casava com o par. A rota aparecia
listada, o par estava instalado, e nada a atravessava — medido na v4.5.0: 100 % de
perda nos dois sentidos, com as rotas abertas nos dois sentidos.
O holder passa a manter, em cada container com mais de uma rede, uma rota para o
prefixo da outra rede pelo gateway desta rede, na interface que o container tem
aqui, e a rota espelhada do outro lado, para a resposta voltar por onde veio (pela
rota por omissão levaria um endereço de origem que o anti-spoofing dessa interface
recusa).
route_plan::plancalcula-o, puro e com testes unitários: a interface por
omissão fica intocada, um destino directamente ligado nunca é encaminhado por um
gateway, um destino dá uma rota.sync_workload_routesexecuta-o: nonetroute add/del(todos os
containers) e noattach-extra/detach-extra(esse container). As rotas levam
um identificador de protocolo próprio, por isso só se remove o que o motor
instalou.
A regra de composição do ADR-0013 mantém-se: uma rota na tabela de um container
diz por onde o pacote sai; o par continua a decidir se é encaminhado. Medido com a
correcção na main: A → B 0 % de perda, B → A continua 100 % (a rota é dirigida),
os dois sentidos abertos 0 %/0 %, um container ligado depois de a rota existir
0 %, e com as rotas fechadas 100 % e nada nas duas tabelas. A tabela de uma
microVM é do convidado e não é coberta.
Como foi validada esta release
- O cherry-pick aplicou sem conflito sobre a
v5.0.1. - Neste ramo:
cargo test -p delonix-sdn --lib,cargo clippy -D warningsdo
delonix-sdn,version_gate,arch_fitnesselang_ratchet. - Não corrido neste ramo: a bateria
scripts/e2e.sh(a secção de 8 checks que
o #733 acrescentou) e o arnês de caos. A medição ao vivo acima foi feita na
main, com o mesmo commit. A CI completa corre no PR que funde av5.0.2de
volta namain.
v5.0.1 — duas falhas que abriam em vez de fechar: `--secret-files` e a admissão por scan
v5.0.1 — duas falhas que abriam em vez de fechar: --secret-files e a admissão por scan
Release de correcção sobre a v5.0.0, cortada de um ramo de manutenção
(release/5.0.x) com exactamente dois commits de código em cima da tag — os
dois cherry-pick -x da main — mais a acção .github/actions/protoc, só de CI,
que o release.yml da main exige na árvore do tag. Os outros ~140 commits que entraram na main desde a
v5.0.0 não vêm aqui: trazem funcionalidade nova, e funcionalidade não sai
numa PATCH. Quem precisa só de segurança actualiza para esta; o resto sai na
próxima MINOR.
As duas falhas são da mesma família, a que este motor persegue em todo o lado:
um caminho de erro que, em vez de recusar, seguia em frente e deixava o resultado
parecer bom.
--secret-files deixava de fechar quando o tmpfs não montava (3626ae70)
Cada passo do write_secret_files — o create_dir_all, as duas chamadas
mount(2) do tmpfs em /run/secrets, e cada escrita de ficheiro — tinha o erro
descartado. Se o tmpfs não montasse (uma flag recusada por um kernel, um
/run/secrets já ocupado, um userns mais restrito que o esperado), o ciclo
continuava a escrever directamente no /run/secrets que existisse, ou seja na
camada persistente do container. Daí o segredo sobrevive a um container commit
para uma imagem publicada e a qualquer backup ou snapshot do container — a
exposição exacta que --secret-files existe para evitar face às variáveis de
ambiente.
Agora a montagem e a escrita são duas funções que propagam o erro, e o init do
container aborta (exit 126, o mesmo padrão do setup_rootfs) em vez de
arrancar um container cujos segredos podem estar em disco. Um container que não
arranca não fuga nada; um que escreveu em silêncio o segredo para a camada
persistente já fugiu.
O que está provado e o que não está: a metade de escrita (write_secret_values)
tem teste unitário próprio contra uma pasta temporária; a propagação do erro da
montagem é imposta pelo compilador (?), não por um mount(2) simulado. A falha
de montagem em si não foi reproduzida ao vivo.
A admissão por scan deixava passar quando o próprio scan falhava (f59c9f8a, #778)
Com DELONIX_SCAN_ON_PULL=<severidade>, o admission_scan_on_pull tratava
qualquer erro do scanner como «esta imagem não tem gestor de pacotes que eu
leia» (DX-1401, o caso legítimo de uma imagem scratch): avisava e deixava o pull
passar. Um advisories.json truncado (o scan --update escrevia-o sem
atomicidade, e uma escrita interrompida deixava-o cortado) ou um blob de layer
ilegível seguiam o mesmo caminho. Um portão documentado como fail-closed passava
a não fazer nada no instante em que a sua própria base de dados se partia, com um
aviso no stderr como único sinal.
Agora só o DX-1401 deixa passar — a distinção é feita pelo número do dicionário,
não pelo texto da mensagem. Qualquer outra falha do scan recusa o pull e remove
a imagem. E a mensagem «Image removed» deixou de ser dita incondicionalmente:
diz se a remoção correu mesmo.
Medido ao vivo na main, com DELONIX_ROOT isolado: imagem scratch
(hello-world) avisa e entra (rc 0, inalterado); vulnerabilidade real
(alpine:3.19, busybox < 1.37.0) é recusada (rc 1, inalterado);
advisories.json corrompido à mão é agora recusado e removido (rc 1) — antes
entrava com rc 0.
Como foi validada esta release
- Os dois cherry-picks aplicaram sem conflito sobre a
v5.0.0. - Neste ramo:
cargo test -p delonix-linux --lib, os testesscando
delonix-runtime-bin,cargo clippy -D warningsdos dois crates tocados,
version_gate,arch_fitness(olibrary_printssobe de 89 para 90 com a
linha de erro do init, já na linha de base do commit) elang_ratchet. - Não corrido neste ramo: a bateria
scripts/e2e.she o arnês de caos — o
ramo de manutenção não tem CI própria (oci.ymlsó corre namain). A CI
completa corre no PR que funde av5.0.1de volta namain.
v5.0.0 — o `USER` da imagem é quem corre, o bloco `provider` tipado, e o contrato de nó servido
v5.0.0 — o USER da imagem é quem corre, o bloco provider tipado, e o contrato de nó servido
Duzentos e dois commits desde a v4.5.0 (162 sem contar os merges), de #572 a
#720 — medidos em 831b9601, a ponta depois do segundo merge da main de
2026-10-08 (git rev-list --count v4.5.0..831b9601).
O número vai agora com o commit em que foi medido, porque sem isso envelhece em
silêncio: os «180 (152)» desta nota estavam certos em ea38d89b, onde foram
contados, e deixaram de bater com a branch assim que ela cresceu. Fixá-lo a um
sha torna-o verificável em vez de uma afirmação sobre «a ponta».
Numerada como MAJOR por uma razão só, e está na secção seguinte: um container
passa a correr como o utilizador que a imagem declara. Todo o resto desta release
é aditivo ou é uma recusa a substituir um sucesso que o motor não conseguia
provar — mas essa mudança quebra o contrato do container run, e um contrato
quebrado não sai numa MINOR.
O fio condutor é o das últimas releases, levado à superfície declarativa: o que
o motor não consegue provar não se reporta como sucesso, e o que ele não entende
não se engole em silêncio. Nesta release isso chega aos manifestos (um campo
desconhecido recusa o documento inteiro), ao isolamento (uma namespace que o host
não imporia é recusada em vez de aplicada), aos limites (--device-* sem disco
que o aceite) e ao ciclo de vida (um rm -f que desistiu não é ressuscitado pelo
supervisor).
Quebra de contrato — ler antes de actualizar
Um container corre como o USER da imagem (ADR-0062, #632, #633, #637,
#638, #639). Até à v4.5.0, container run ignorava o USER que a imagem
declara e o processo corria como uid 0 — desde a v1.0.0, sem erro nem aviso.
Medido com haproxy:3.4-alpine, que declara USER haproxy: id dentro do
container respondia uid=0.
O que muda, e o que fazer:
- Sem
--user, o processo corre como oUSERda imagem. Um serviço que
dependia de ser root sem o pedir passa a não o ser. Para o manter,--user 0. - Num host rootless sem intervalo de subuid só cabe um uid: aí fica a 0 e o
rundi-lo em voz alta, em vez de falhar. - O
--userdeixou de entregar o sistema de ficheiros inteiro ao utilizador.
A varredura antiga (chown_tree_once("/")) dava-lhe 940 das 986 entradas de uma
imagem de 15 MB — incluindo/etc/passwde o próprio binário do serviço — e
atravessava os mounts, re-apropriando um bind mount do HOST (um ficheiro de
1000:1000passava a um subuid, e o dono deixava de conseguir escrever no seu
próprio ficheiro). Agora o container recebe o que a imagem lhe dá: um índice
de donos gravado ao extrair a layer, aplicado uma vez pelo init, nunca noutro
sistema de ficheiros. Medido depois, na mesma imagem: 2 entradas do utilizador
em vez de 940, camada de escrita de 56 K em vez de 13 MB, e o bind do host
intacto. Noodoo:20.0(~2 GB), o primeiro arranque com-u odoopassou de
«ainda a varrer aos 16 minutos» para 1,7 s. - Um volume nomeado VAZIO fica do dono que a imagem dá ao ponto de montagem,
ou do utilizador do container quando a imagem não nomeia ninguém. Um volume com
dados e um bind mount nunca são tocados — um filestore escrito como root por
uma corrida anterior não é migrado. - O
buildsegue a mesma regra: umRUNcorre como oUSERem vigor nesse
passo (antes corria sempre como root, mesmo depois de umUSER app), e a cache
de layers preserva os donos — um rebuild com cache perdia-os.
Duas assinaturas públicas do delonix-sdn mudaram (#718):
can_bind_host_port e host_port_busy passaram a receber um Proto. Quem
consome o crate por git + tag não parte hoje, mas subir esse pin exige uma
passagem. A assinatura antiga não podia ficar porque era a armadilha: as duas
sondas abriam sempre um TcpListener, qualquer que fosse o spec, logo uma
publicação UDP era verificada contra TCP — cega por construção, com o conflito a
aparecer só dentro do slirp como JSON opaco. O enum obriga cada sítio de chamada
a dizer o transporte, e recusa nessa fronteira o sctp que o CRI consegue emitir
em vez de o sondar como TCP.
Efeito lateral da mesma passagem: a extracção passou a preservar setuid, setgid
e sticky. O /tmp de todos os containers era 777 em vez de 1777, e um su
setuid perdia o bit. Uma layer extraída por um motor anterior é corrigida a partir
dos cabeçalhos do blob na primeira vez que uma imagem a usa.
Mudanças de comportamento
O endereço de bind de uma porta publicada passa a ser REGISTADO (#718). Era
estado USADO na publicação e nunca persistido: o registo guardava a spec como foi
escrita (51072:80) e o DELONIX_PUBLISH_ADDR era relido do ambiente a cada
start. Quinta ocorrência da armadilha «estado necessário para RECONSTRUIR o
recurso tem de ser persistido». Medido nos dois sentidos, contra o bind lido do
kernel:
- publicada com
DELONIX_PUBLISH_ADDR=0.0.0.0, voltava em127.0.0.1depois
de umstart— que é o que um unit donet boot enablecorre, sem ambiente
nenhum. Serviço em baixo, listagem a dizer publicado; - e o INVERSO, que é o que pesa em segurança: uma porta publicada de propósito
sem endereço (só loopback) voltava em0.0.0.0, exposta à LAN inteira,
porque a shell que correu ostarttinha a variável exportada.
Nos dois casos rc=0, sem aviso, com o net ingress ls a imprimir a linha igual
antes e depois — 51070:80 e 51072:80 eram bytes idênticos em disco com
alcance oposto. Agora a spec guardada é sempre addr:hostPort:contPort/proto, e
o container ls mostra o endereço (0.0.0.0:55070->5070/udp) na coluna que se
lê precisamente para decidir o que está exposto. Uma spec sem endereço passou
a querer dizer «registo anterior a esta versão».
Recusas novas. Em todos estes casos o motor aceitava e não cumpria.
Um hostPort que o nó não publica (ADR-0074, #720) — e a recusa é por
caminho, porque os caminhos diferem e medi-los foi o que o mostrou: o
slirp4netns recusa SCTP no add_hostfwd (bad arguments.proto), o portmap
do CNI publica-o ponta a ponta, e o hostNetwork não publica nada (o container
está na netns REAL do host e liga as portas ele próprio). Uma resposta cega teria
fechado uma porta que está aberta em modo root. A recusa chega do
RunPodSandbox, antes de o sandbox existir, como failed_precondition — que o
kubelet mostra como evento no pod em vez de um StartContainer retentado para
sempre. Um containerPort sem hostPort continua descartado, que é a forma que
um Service SCTP usa: nenhum serviço SCTP é afectado.
Manifestos (ADR-0069, #682, #687)
- Um campo que o motor não entende recusa o manifesto, antes de qualquer
efeito — incluindo dentro dos itens despec.containers[]de um Pod, onde as
probes e olifecyclenão existem e até aqui desapareciam em silêncio.
stack validatecontinua a RELATAR em vez de recusar, e
DELONIX_MANIFEST_LENIENT=1é a válvula explícita. - Um campo que um Kind recusa pelo NOME mantém o seu próprio código. O
unprivileged: falsede umSystemContainerrespondiaDX-1000 unknown field — check the spellinga um campo bem escrito; agora respondeDX-1540, que diz o
que o campo é. - O mesmo nome de Pod noutra namespace é um conflito, não «already exists»; e
ContainereServicepassaram a ser identificados por(namespace, nome)—
dois homónimos em namespaces diferentes deixaram de ser um recurso no plano. - Um store ilegível é um erro, não uma máquina vazia (VM, volumes, presença de
containers). Umplanque lê zero por não ter conseguido ler propunha criar
tudo de novo.
Isolamento e rede
- Uma namespace com nome num host que não filtra o tráfego de bridge é recusada
(DX-6305, exit 69, decisão D5 do plano de maturidade,#664). Sem
br_netfilteras chains são instaladas, os sets são preenchidos, todos os
comandos reportam sucesso — e a fronteira não existe. É a forma mais cara de
falha silenciosa que este motor pode ter, porque o que falha é uma propriedade
de segurança que se lê como aplicada. Válvula:
DELONIX_ALLOW_UNENFORCED_ISOLATION=1. - O anti-spoofing mudou para uma tabela
bridge, onde realmente corta
(#671). A regra antiga (ip … iifname <veth> ip saddr != <ip> drop) tinha o
contador a zero: entre duas portas da mesma bridge, oiifnameque a camada IP
vê é a BRIDGE, nunca a porta. Medido: um container comNET_ADMINforjava a
origem e chegava ao vizinho (3/3), e forjando o endereço de um membro de outra
namespace furava o isolamento. Agora prende por porta o MAC, a origem IPv4 e o
emissor ARP. Uma lição que fica: uma regra de segurança só está provada
quando um pacote forjado é contado a cair — a auditoria que «fechou» isto
leu o ruleset e viu a regra lá. - As concessões são do administrador, não do pedido (
#672):
--allow-source <cidr>e--no-source-checkexistem para um container que
encaminha, mas são PERMISSÕES na política do nó. Sem política, nada é concedido,
emode: warnnão concede.
Ciclo de vida
- Um
--waitque esgota o tempo sai com 124 (DX-8503) e deixa a VM a correr
(decisão D6,#661). Antes devolvia 0: um--waitque não viu a VM responder
reportava sucesso. --device-read-bps/--device-write-bpssão recusados quando não há disco que
os aceite (#663). Oio.maxpassou a nomear o disco em que o rootfs do
container vive; antes o limite era escrito e não aplicado.- Um
rm -fque desistiu não é ressuscitado pelo supervisor (#647). Com o
disco saturado, umrm -fdevolviaDX-8101e mantinha o registo como a regra
manda — e quando o processo finalmente saía, o supervisor de--restart always
reiniciava-o. A intenção de remoção passa a ficar gravada ANTES do sinal. - Um
stopque desistiu mantém o processo registado, e umstartque falha
depois d...
v4.5.0 — rede que falha fechada, Proxmox medido rota a rota, providers num ficheiro por nó
v4.5.0 — rede que falha fechada, Proxmox medido rota a rota, providers num ficheiro por nó
Cento e três commits desde a v4.4.0 (110 contando os merges), de #474 a #576.
Numerada como MINOR: há verbos novos (vm resize, vm cloud-init,
vm move, provider ls|describe|matrix|config, net netns gc,
serve node-api), Kinds novos (NetworkGateway, NetworkZone), um ficheiro de
configuração novo (providers.yaml) e uma ferramenta MCP nova
(workload.usage). Nenhuma superfície foi removida. Várias mudanças alteram,
de propósito, comportamento que já existia: um limite, uma regra ou uma
remoção que antes passava com um aviso ou em silêncio passa a ser recusada.
Estão todas juntas na primeira secção, antes das novidades.
O fio condutor é o de sempre, levado mais longe: um resultado que o motor
não consegue provar não se reporta como sucesso. Isto vale para uma regra de
firewall inválida, para um rm -f cujo processo ainda não morreu, para uma
tarefa do Proxmox que «acabou OK» sem ter aplicado nada, e para um push que
morria sem dizer porquê.
Mudanças de comportamento
Quem actualiza deve ler esta secção primeiro.
Rede de containers (auditoria de rede, sessões S1–S4, #554, #560, #553, #555, #552)
- A firewall por container falha fechada. Uma spec com uma regra inválida é
recusada inteira (antes, a regra era saltada e o resto aplicado). Também se
recusa umdir, umaactionou uma política fora dos valores documentados:
antes, umdirdesconhecido era lido comoine uma política desconhecida
comoallow. - Uma falha do isolamento de namespace é um erro, não um aviso, em
container run,startepod create. O attach é desfeito. Antes, orun
saía com rc=0 e o container ficava alcançável a partir de outra namespace. - O egress da origem é avaliado sempre. Antes, um
acceptna chain do
destino terminava a avaliação e a políticaegress denyda origem nunca
corria (medido: A→B passou 3/3 com o egress de A emdeny). Agora são duas
base chains,fwoutefwcont. - Regras de entrada explícitas deixam de desligar o isolamento (#560). Um
container com, por exemplo,ingress deny tcp/22passava a aceitar ligações
novas de qualquer outra namespace em todas as outras portas. Agora há um
guardrail que nenhuma regra de entrada retira; umallowexplícito continua
a abrir o par que nomeia. Um container com regras de entrada explícitas
deixa de aceitar ligações novas de outras namespaces nas portas que nenhuma
regra abre. - Remover a última regra (
ingress rm/clear) fora da namespacedefault
mantém a chain, que é também o isolamento. Antes, o container ficava aberto
a outras namespaces. - Redes com CIDR fora de 10.200–254 passam a ter firewall e isolamento.
Antes ficavam sem chain e fora do conjunto de workloads: medido em
172.30.5.0/24, duas namespaces falavam 2/2 nos dois sentidos. network connect/disconnectfalham quando a firewall falha, em vez de
avisar e sair com 0.network rmrecusa uma rede com alguma coisa ligada (DX-5307, exit 5)
e nomeia os containers, os membros de pod e as VMs. Antes saía com rc=0 e
deixava o container «Up» numa bridge apagada. Ostack --replace/destroy
mantém o seu caminho explícito.- IPAM transaccional (#553): uma reserva de um IP que já é de outro dono é
recusada (DX-5308, antes gravava-se com um aviso); ostopmantém o
lease e ostartvolta ao mesmo endereço; ormliberta também os leases
das redes extra.--subnet 10.200.0.0/16(o espaço do ingress) e
10.0.2.0/24passam a ser recusados. - O pool de DHCP das VMs vive dentro da rede. Numa rede que não é
/16o
DHCP servia a rede errada (numa/24oferecia um endereço/16com outro
router, e a VM ficava sem IP). O pool fica agora no topo da rede, com
min(240, tamanho/8)endereços; num/16continua.254.10–.249byte a
byte, para nenhuma VM existente mudar de IP. Consequência: um/28passa a
ter 11 endereços para containers e 2 para o pool das VMs. - O holder de rede valida cada token de controlo antes de correr um comando
(S4, #552): um token que vá para um argv não pode começar por-, IPs e
gateways têm de ser IPv4 estritos,rate/burstsó dígitos, e otypede
um plugin CNI tem de ser um nome simples (antes,"type": "/tmp/x"executava
esse binário no holder). As linhas que um cliente legítimo envia não mudam. vm bridgevalida as subnets: nemdefault, nem/0, nem uma subnet
sobreposta à SDN, e o root é verificado antes de qualquer comando.
API de gestão (delonix-mgmt)
- As sete mutações de rede que escreviam o dataplane sem passar pelo registo
respondem501comDX-6302(#555): firewall, egress,attache
attach-extra. As leituras e opublish, que grava o registo, continuam.
Foi uma decisão explícita: recusar todas, em vez de as alargar.
Containers e cgroups
--cpuset,--io-weighte--device-read/write-bps/iopssão recusados
(exit 69) quando o cgroup do container não tem o controlador (#545). Antes,
--device-write-bps 5mbescrevia a 1,6 GB/s e saía com 0, só com um aviso. A
válvula de escapeDELONIX_ALLOW_UNENFORCED_LIMITScontinua a valer. Chega a
todos os pontos de entrada (CLI, manifesto, compose, Docker API, CRI); o CRI
passa a receber exit 69 para umcpuset_cpussemcpuset, como já recebia
para CPU e memória. Como root, odelonix.slicepassa a delegar também o
cpuset, que antes era ignorado em silêncio.sudo delonix system setup --delegateescreve sempre o drop-in (#550).
Antes, a escrita só era alcançável num ramo que um processo root nunca toma,
por isso quase nunca escrevia. Sobsudo, o relatório passa a ser sobre o
utilizador que escreveu osudo, e o passo seguinte nomeia
systemctl restart user@<uid>.service: umdaemon-reloadsozinho é um falso
verde.container rm -fsó volta quando o processo morreu (#562). Antes voltava
no sinal: com o disco carregado, odelete pod --forcevoltava em 0,76 s
com os membros ainda vivos em estado D. Agora espera pela saída real, com o
mesmo limite de 30 s dostop. Passado esse limite, mantém o registo e
devolve DX-8101container.still_exiting(exit 124); umrm -frepetido
remove-o quando o processo morrer.- Um container
--rmé montado com o overlayvolatile(ADR-0056, #537).
A saída deixa de esperar pelo writeback do sistema de ficheiros do host.
Medido com 1 GB sujo noutro ficheiro do disco:run --rm … alpine trueem
0,16 s, contra 9,5–14 s num container mantido. Containers mantidos, de pod e
de CRI não mudam.
Providers e VMs
- Um
defaultProviderque o processo não consegue servir falha com 69, a
nomear o provider (ADR-0054, #514). Antes era descartado, e ovm create
seguinte podia criar a VM num hipervisor local. Umproviders.yaml
ilegível faz falhar os pedidos de VM sem--backend; os comandos que não
escolhem provider continuam a funcionar. - Um
providers.yamlcomtype: opnsensepassa a registar o appliance
(#556). Registar não contacta nada. - Proxmox:
networkcom o nome de uma rede do motor é recusado (#478). Antes
era aceite e deitado fora: o registo diziaNetwork: lab-nete a VM ficava
novmbr0. - Proxmox: uma tarefa que acaba em
WARNINGS: <n>é um sucesso (#546), e as
linhasWARN:ficam no livro de tarefas. Antes era lida como falha, e a acção
podia ser reenviada por cima de um recurso vivo. - Proxmox: um apply da SDN espera pelo reload de rede de cada nó (#571). O
reloadnetworkalldo PVE acabaOKlogo que arranca os reloads; um reload
que falhe, ou que não apareça dentro do timeout, recusa agora o apply com
DX-6512. vm create --requirecom uma cloud image filtra de facto (#539). Antes o
libvirt era pedido por nome e o filtro nunca corria.
Outros
- O binário já não morre por SIGPIPE numa escrita para um socket (#573). Um
EPIPE numa ligação de rede passa a ser um erro devolvido (com retry, no push).
Um stdout ou stderr fechado (image ls | head) continua a terminar o processo
por SIGPIPE, como antes. net netns downrecusa uma raiz que não existe, em vez de a recriar.kind: Appdeixa de apagar a cache de layers do CNB depois de cada build
(#529), como o ADR-0035 já dizia.
Rede: o que mais entrou nas sessões S1–S6
A auditoria de rede (docs/discovery/62_NAAS_FASE0_AUDITORIA.md, #544)
inventariou a rede do motor, encontrou cinco recursos com dois escritores e
sete achados de fail-open, e repartiu a correcção em sessões. As mudanças de
comportamento estão acima; o resto é isto:
- Migração do despacho sem passo manual (#560). Um holder criado por um
binário anterior tem o despacho antigo, de uma chain. O host migra-o agora no
ensure_upe noapply_firewall_all(nsenterpara o userns e o netns do
pin,nft -f, uma transacção atómica e idempotente), uma vez por pin. Se a
migração falhar, é erro com o remédio. network rmnão deixa nada no holder (#555). Saem as chaves de
@netpair, o membro de@dlxbr, as regras de egress e o conjunto FQDN, e a
thread de DHCP pára. Antes, uma rede recriada com o mesmo nome nascia sem
DHCP e com odenyda anterior. Umpublishque falha a meio desfaz só as
portas que publicou; uma rede criada por uma VM passa a aparecer no
network ls; ohttprouteescreve sob lock e de forma atómica.- Um só alocador de
/16(#553), sob um lock, que vê os registos e as
NetDefs em qualquer forma. Antes havia dois, que se ignoravam. As escritas de
NetDef trancam e são atómicas: 60 escritas de egress concorrentes com
mudanças de gateway deixavam 0 sobreviventes. - Posse pela marca, não pelo nome, nos providers remotos (S6, #558). Um alias
ou regra do OPNsense e uma vnet da SDN do Proxmox eram do motor se tive...
v4.4.0 — `drift`, a matriz do Compose com denominador, e um portão de performance que se recusa a julgar
v4.4.0 — drift, a matriz do Compose com denominador, e um portão de performance que se recusa a julgar
Dezassete commits desde a v4.3.0. Numerada como MINOR: há três verbos novos
(delonix drift, delonix migrate assess, compatibility compose) e um Kind
novo (RuntimePolicy), tudo aditivo — nenhuma superfície foi removida nem
mudou de significado. O tema comum das três melhorias de cabeça é o mesmo:
deixar de responder a uma pergunta com outra.
Uma mudança de PROCESSO entra também, e afecta quem publica: empurrar uma tag
já não publica nada — ver a última secção.
delonix drift — o que a MÁQUINA mudou, sem o manifesto na comparação (M05, #470)
O stack plan responde «o que faria um apply», e essa resposta mistura duas
coisas de origens diferentes: as edições que alguém fez ao FICHEIRO e as que
alguém fez ao NÓ. O próprio --help do plan já admitia o limite — «with the
manifest unchanged, whatever it prints IS drift» — e é essa condição que o torna
inutilizável como portão: num PR o ficheiro mudou sempre.
- Compara o carimbo do último apply (
delonix.io/last-applied) contra o
observado. O que diferir aconteceu fora do caminho declarativo: um
container update, umnftà mão, alguém a resolver um incidente de
madrugada. - O manifesto é OPCIONAL, e é isso que lhe dá o caso de uso principal: num nó
sem o repositório o comando responde na mesma. Os seis Kinds que só se
enumeram a partir do documento (RuntimePolicy,Image,App,
NetworkPolicy,HTTPRoute,Gateway) são nomeados na saída em vez de
omitidos — uma tabela mais curta lê-se como «nó limpo». - Um recurso sem carimbo é um TERCEIRO ESTADO, não deriva (
unstamped):
criado à mão, ou adoptado por uma versão anterior ao carimbo. Não há intenção
registada com que comparar, e chamar-lhe deriva era inventar uma linha de base. - Só se comparam as chaves que o carimbo traz — um campo presente no
observado e ausente do carimbo é um default do motor que o apply nunca
definiu, e reportá-lo enterrava a linha que interessa debaixo de ruído. --stack <nome>,-o json, e--detailed-exitcode(2 = há deriva), o mesmo
contrato doplane dodiff.- O gate apanhou um erro real na primeira corrida. A lista de Kinds
dependentes-de-documento seria a sétima lista deste repositório que tem de
concordar com outra e deixa de concordar em silêncio, por isso
doc_scoped_matches_the_stack_wiringlê ostack.rsem vez de confiar num
comentário — e chumbou: onetwork_access_rule::actualrecebedocse
ignora-os, logo não pertencia à lista.
Grupo novo, não estável (docs/cli-stability.md), com página própria no site.
compatibility compose + migrate assess — o terceiro estado precisa de um denominador (M02, #471)
O compatibility docker publica três estados desde que existe: servido,
recusado com razão, em falta. O compose não tinha o terceiro, e o próprio
módulo dizia porquê — «tem uma allowlist mas nenhuma superfície de três estados
própria para reutilizar». O estado «em falta» precisa de um DENOMINADOR.
- A lista da especificação foi MEDIDA, não escrita de memória: um ficheiro
compose mínimo por chave candidata,docker compose configem cada um, e
ficou só o que o cliente aceitou — 89 chaves de serviço e 8 de topo, contra
o docker v29.8.1, a 2026-09-23. - Hoje: 28 servidas, 12 recusadas com razão, 49 em falta, de 89. No topo, 7
servidas e 1 recusada, de 8. - A tabela DESCREVE o comportamento, e há um check que o exige. Não confere a
tabela contra si própria: pega em cada uma das 88 chaves que classifica,
escreve um ficheiro que a usa, e pergunta aocompose configo que acontece.
As 88 concordam. delonix migrate assess -f docker-compose.ymlé a outra metade. O
compose uppára na primeira chave que não entende — correcto para CORRER,
inútil para DECIDIR. Lê o YAML cru (o parser tipado recusaria no primeiro
desconhecido, que é precisamente o conjunto a enumerar), não cria nem puxa
nada, e conta USOS de chave e não chaves distintas: três serviços com
devices:são três decisões.-o jsone--detailed-exitcode.- Dois achados enquanto se construía: a tabela que eu ia escrever JÁ EXISTIA
(ENGINE_HAS_IT_SERVICE, a que já produz a recusa que nomeia a flag) — a minha
cópia classificava comomissingo que o motor recusa com mensagem própria; e
estava INCOMPLETA:sysctls,ulimits,gpuseuserns_modetêm flag no
container rune caíam no erro genérico «not understood», que manda o leitor
embora em vez de lhe dizer para onde ir. O teste que verificava duas linhas à
mão passou a percorrer as doze. crieocicontinuam de fora, por razões diferentes: o número do CRI vive
num documento mantido à mão e nada no binário o consegue derivar (seria uma
segunda cópia de um número sem forma de dar pelo seu envelhecimento); o OCI não
tem trabalho de conformidade neste repositório, logo a contagem honesta é zero.
O portão de regressão de performance, e a terceira saída (M11, #469)
O scripts/bench.sh já media os três motores na mesma máquina e já se recusava a
correr numa bancada carregada. Faltava a outra metade: nada comparava o número de
hoje com um gravado.
scripts/bench_gate.pyLÊ uma medição e nunca a faz — duas opiniões sobre o
mesmo número é como elas começam a divergir.- Três saídas, não duas:
0dentro da tolerância (25%),1regressão, e
3recusa-se a julgar. A terceira é a que o torna confiável, e vem do
incidente de 2026-08-10: docker 1406, podman 1351, delonix 640, e a corrida
seguinte 208/268/89. Um portão ingénuo teria lido isso como «o delonix regrediu
7×» por um problema da máquina. O docker e o podman são âncoras: se
degradaram tanto como nós, o que mudou foi a bancada — e sem âncora nenhuma
também recusa, em vez de adivinhar. - Baseline por MÁQUINA. Uma mediana medida num desktop de 32 threads não diz
nada sobre uma VM de 2 vCPU, e comparar as duas fabrica exactamente o ruído que
o gate existe para filtrar. - Dispersão, porque o limiar de load não chega: medido a load 6.44 (abaixo do
limite), uma linha de dez amostras do docker foi de 434 ms a 5 407 ms e a
mediana absorveu-o. Acima de 3× o gate não julga; gravar é mais estrito,
1,5× — uma corrida de juízo deita-se fora, uma baseline é reusada por todas
as que vierem a seguir. - Três defeitos do harness corrigidos de caminho:
--jsonera aceite e
IGNORADO (a variável era atribuída e nunca lida);time_nnão olhava para o
exit status, por isso umdockercom o daemon parado falhava em milissegundos
e entrava na tabela como o mais rápido dos três; e não havia medida de
dispersão nenhuma. - Um gate morto: nenhum
scripts/test_*.pycorria em CI — o
test_release_verify.pynasceu no dia anterior com dez testes que nada
executava. Jobscript-testsnovo, que falha se não encontrar nenhum.
Baseline gravada na máquina de desenvolvimento (2026-09-23, ceec7f8c): docker
224 ms, podman 279 ms, delonix 87 ms.
kind: RuntimePolicy — o tecto de admissão em forma declarativa (M04, #462)
- O
policy.json(o tecto que o nó impõe no ponto de admissão) ganha Kind
próprio.stack applysobe ou aperta; nunca o baixa —teardown: false, e
stack destroy/--prunerecusam-no explicitamente. delonix policy unseté o único verbo que o remove: pede confirmação (ou
--force) e imprime sempre o que estava lá antes de apagar.- Kinds no manifesto: 19 → 20.
- Validado ao vivo com
DELONIX_ROOTisolado: umcontainer run --privileged
REALMENTE recusado por uma política aplicada por manifesto; reaplicar com um
campo mudado converge in-place (stack planmostra~, não-/+); dois
documentos do Kind no mesmo manifesto são recusados antes de qualquer escrita.
Build: cache mounts e SBOM por imagem (M03, #468 e #466)
RUN --mount=type=cache,target=<path>[,id=<name>]— directório persistente
e de leitura-escrita que sobrevive entre builds, generalizando o mecanismo que
otype=secretjá usava (mount_live, invisível à imagem final). O directório
é<root>/build-cache/mounts/<sha256(id)>— em hash, nunca a string crua:
umidomitido é por omissão otarget, um caminho arbitrário controlado pelo
Dockerfile.sharing=erosão recusados em vez de aceites-e-ignorados.
Validado ao vivo: doisdelonix buildseparados escreveram no MESMO cache mount
e o segundo viu o que o primeiro escreveu; o conteúdo nunca apareceu na imagem
final (confirmado porcontainer rundentro dela).- SBOM SPDX 2.3 automático por imagem construída —
delonix buildescreve-o
best-effort logo a seguir a um commit com sucesso, edelonix image sbom <ref>
serve-o. Reutiliza oextract_sbomjá existente (apk/dpkg, sem montar nem
correr a imagem). Validado ao vivo: um build dealpine:3.20+apk add curl
produziu um SBOM com 25 pacotes, e oimage sbomdevolveu o ficheiro
byte-a-byte idêntico — prova do cache hit, não de recomputação. - Dívida LANG-01 corrigida de caminho: as mensagens de erro do parser de
--mountestavam em português desde sempre e nunca tinham sido revistas.
POST /images/create na Docker API (M02, #460)
- O
docker pullatravés do socket passa a funcionar: streaming chunked-JSON,
delegando no pull que já existe. Movido deAPI_UNIMPLEMENTEDpara a matriz. - Validado com um
dockerCLI real (29.8.1) — duas imagens, incluindo uma de 8
layers / 26 MB.
Fuzz em CI, e o que encontrou na primeira corrida (M04, #461 e #463)
- Job de fuzz novo (60 s por alvo), sobre os parsers escritos à mão que comem
input não confiado. - Na primeira corrida real encontrou um out-of-memory no
parse_dockerfile:
uma cadeia deARG B=${A}${A}dobra o valor guardado a cada linha — uma bomba
de expansão expone...
v4.3.0 — `delonix hosts sync`, e o instalador passa a aceitar aarch64
v4.3.0 — delonix hosts sync, e o instalador passa a aceitar aarch64
Quatro commits desde a v4.2.0. Numerada como MINOR: há um grupo de comandos
novo (delonix hosts, marcado não estável). Uma release pequena, de duas
mudanças que o utilizador vê e duas de documentação; cada secção diz o que foi
provado e o que não foi, e o que não foi validado está listado no fim.
delonix hosts sync — os nomes de serviço no /etc/hosts (#449, ADR-0048, fase 2)
delonix hosts syncpublica o nome padrão de cada container exposto
(<nome>.<ns>.svc.delonix.internal) num bloco delimitado do/etc/hosts—
o mesmo bloco, por raiz de estado, do ADR-0046, por isso um nome pedido por
uma rota comhosts: [host]e um publicado aqui partilham um bloco e uma
regra de recusa. Todos os nomes apontam para127.0.0.1: é a porta publicada
do proxy L7 que o host usa para chegar a uma carga na SDN.--printmostra o bloco e não toca em nada;--offremove-o e deixa
de o manter.- Depois de ligado,
container run --exposeermmantêm o bloco atual, em
melhor esforço: um nome que uma rota pediu falha oapply, mas um nome
publicado por este comando é uma conveniência que o operador ligou, e um
run --exposesem root não falha por o ficheiro precisar de root — só
avisa. - Sem root recusa e imprime o bloco a acrescentar à mão. Um nome que já tem
entrada fora do bloco é recusado (nunca é sobreposto), e uma recusa nunca deixa
o estado a meio: o ficheiro fica intacto e o interruptor desligado. - Sob
sudolê o registo do utilizador que o invocou, e não o do root. Sem
isto,sudo delonix hosts sync(o passo natural quando o comando recusa sem
root) leria/var/lib/delonix, que está vazio, e publicaria «0 nomes». É a
mesma correção que ovm bridgejá tinha (adopt_invoking_user_root); um
DELONIX_ROOTexplícito continua a ganhar. - Grupo novo, não estável (
docs/cli-stability.md): documentado na tabela do
README.rste no site, com página própria (docs/comandos/hosts.html).
Instalador: aceita aarch64 (#447)
- O
install.shaceitaaarch64(earm64, normalizado) em vez de recusar tudo
o que não é x86_64. O nome do asset compõe-se deuname -m:
delonix[-cri|-mcp|-mgmt]-aarch64-linux— os que orelease.ymlpublica
desde av4.2.0. Outras arquiteturas continuam a recusar, dizendo quais são
suportadas. - Em aarch64 não existe a variante
-v3(é um nível de microarquitetura do
x86-64), e a sonda de QEMU pedeqemu-system-aarch64— antes pedia o
emulador x86 e instalava-o num host ARM. - Fora de x86_64 não se descarregam o Cloud Hypervisor estático, o EDK2
CLOUDHV.fdnem ohypervisor-fw(são todos builds x86-64, e os checksums
fixados também); o instalador di-lo, e o backend de VM é o libvirt. Não foram
inventados pins para arm64. - Nota importante: o
install.shque av4.2.0publicou é o antigo, que
ainda recusa aarch64. Este é o primeiro release cujoinstall.sho aceita.
Documentação
- Manual do contribuidor (
docs/dev/) atualizado para av4.2.0(#453):
vm build/vm.yaml, overify-images.sh, os binários aarch64, o
--performancee o nome padrão de serviço. - Notas da
v4.2.0corrigidas (#454): afirmavam que uma VM libvirt era
recusada e quekind: IPPoolera uma fase posterior. Ambas estão construídas
(fases 2 e 3 do ADR-0046); só oannounce: l2e ohosts: guest(fase 4)
faltam. O erro foi copiar o corpo de um PR desatualizado.
O que NÃO foi validado
delonix hosts sync: a escrita no/etc/hostsreal (exige root; foi
provada só num ficheiro de teste, verificando o conteúdo pormd5: escrita,
idempotência, recusa por conflito, recusa sem permissão e--offa restaurar o
ficheiro byte a byte) e um cliente a resolver um nome do bloco e a alcançar o
backend. A atualização automática só funciona quando o ficheiro é gravável
pelo processo: para um utilizador rootless típico, umcontainer run --expose
só avisa, e ohosts syncé, na prática, um comando manual. A fase 3 do
ADR-0048 (credenciais) não está nesta release.- Instalador em aarch64: uma instalação completa num host aarch64 real nunca
correu (não se instalou um binário ARM em cima de uma máquina x86), nem os
nomes dos pacotes de QEMU por distro em arm64, nem os caminhosdnf/apk.
O que foi provado: a composição dos nomes de asset — a funçãofetch_asset
real do script, contra os assets publicados dav4.2.0, com controlos
negativos. - O que a
v4.2.0já listava continua por validar: a instalação de pacotes
e os perfis dourados dovm build, o arranque das imagens construídas, os
construtores de appliance reais, e o que o ADR-0046 marca como não medido.
v4.2.0 — `vm build` com `vm.yaml`, receitas por distro e por appliance, serviços de VM por nome
v4.2.0 — vm build com vm.yaml, receitas por distro e por appliance, serviços de VM por nome
Dezassete commits desde a v4.1.1. Numerada como MINOR: há superfície nova de
CLI (vm build, o ficheiro vm.yaml), um Kind novo (IPPool) e novos campos
de manifesto. A série vem
de várias frentes independentes, e cada secção diz o que foi provado e o que
não foi — o que não foi validado está listado no fim, junto.
vm build e o vm.yaml — a experiência do docker build para VMs (#451)
delonix vm buildexiste agora. Até aqui só haviadelonix image vm build; os dois partilham o mesmo conjunto de argumentos e fazem o mesmo.vm build .escolhe a receita como odocker build: um-fexplícito
decide (um.yaml/.ymllê-se comovm.yaml, o resto comoVMfile); sem
-f, umvm.yamlna pasta vence umVMfile, que vence a receita dourada.
-tdá o nome do resultado e o${TAG}dentro do ficheiro;--target
escolhe uma imagem quando o ficheiro declara várias. Com umvm.yaml, o-t
passa a ser opcional (usa otag:da imagem).vm.yamlé o «compose» doVMfile(o «Dockerfile»). Esquema estrito — uma
chave desconhecida é erro, e um campo que a rota escolhida não cumpre é
recusado pelo nome, nunca ignorado. Descreve o qcow2 completo: base, tamanho,
hostname, vcpus/memória, pacotes, utilizadores (sudo, grupos, shell, chaves),
serviços, ficheiros (com o caminho de destino completo e o modo),env,
cloud-init,run:, o que remover (remove.packages/paths/users/services,
aplicado depois de tudo o resto, para também podar o que um pacote trouxe) e a
limpeza (cache de pacotes, logs, histórico, /tmp, machine-id).${TAG}e
${VAR:-valor}expandem-se nos valores, não nos comentários.- Sem segundo motor de build. Cada imagem compila para um construtor que já
existia: umVMfilesintetizado, a receita dourada (profile: rootless|k8s),
umVMfileexistente (build.file) ou um construtor de appliance. - Rota
appliance:para o que não se descreve como edições a uma cloud image
(Proxmox a partir da ISO, OpenStack a puxar ~20 GiB de contentores). O
vm.yamlnomeia um construtor (builder: proxmox), nunca um caminho: o
motor resolve-o parascripts/appliances/build-<nome>.shdentro do
repositório, valida os argumentos e as variáveis de ambiente (semPATH,
LD_*,BASH_ENV…), corre-o com umOUT_DIRisolado ao lado do store,
importa o único qcow2 que ele deixa e apaga a pasta de trabalho, mesmo em
falha. Umvm.yamlalheio não consegue fazer o host correr um ficheiro à sua
escolha. images/: uma pasta por imagem, cada uma comvm.yamle README de ponta a
ponta. Quatro distros que constroem offline (ubuntu,debian,rocky,
fedora) e oito appliances (opnsense,proxmox,truenas,openstack,
monitoring,carbonio,glpi,wazuh). Os scripts continuam em
scripts/appliances/, de onde o workflow que publica as imagens os lê. As
receitas mantêm/usr/share/docde propósito: tem as licenças que uma imagem
publicada tem de redistribuir.scripts/verify-images.shconstrói as receitas numDELONIX_ROOTisolado e
lê o conteúdo do qcow2 (ou da VM) contra o que a receita declarou. Tem uma
receita-sonda com valores que a base não pode ter (a base já traz uma conta
delonixcom sudo, por isso verificar isso na receita entregue não provava
nada) e um--self-testque exige que as verificações falhem numa imagem
que ninguém construiu.- Um teste fecha o ciclo:
every_shipped_recipe_is_valid_and_completefalha
se uma receita entregue deixar de ser válida ou apontar para um ficheiro ou
construtor que não existe. - Mensagens de erro do
vm.yamle dos appliances passam pelo catálogo (pt.po).
Serviços de VM por nome (#444, #448, ADR-0046/0047/0048)
VirtualMachine.spec.exposepublica um serviço HTTP/S de uma VM por nome:
baixa noloadpara umHTTPRoute/<vm>-expose. O resolvedor de rotas conhece
VMs Cloud Hypervisor (na SDN) e VMs libvirt emnat/bridge(ADR-0046,
fase 2): estas chegam-se por uma segunda instância do proxy no netns do host,
sem root, que escuta só em127.0.0.1. Uma VM libvirt em modo user-mode não
tem endereço alcançável e é recusada com a correção (netMode: natem
qemu:///system); um documento que mistura backends das duas metades é
recusado, com a indicação de qual cai em qual.kind: IPPool(gruponetworking; ADR-0046, fase 3): um livro de reservas
de endereços do host (um endereço, um intervalo ou um CIDR; só IPv4) que as
rotas reclamam comspec.pool(eexpose[].pool). Uma rota reclama um só
endereço enquanto estiver declarada, fica alcançável nele em vez de em
127.0.0.1, e ohosts: [host]aponta o nome para ele. É um Kind completo
(apply,plan, deriva,--prune,get/describe/delete ippools) e aplica-se
antes doHTTPRoute. Sóannounce: local: o endereço já tem de estar numa
interface do host, e oapplyverifica-o ligando-o;announce: l2,
interface, IPv6 e BGP são recusados (fase 4, por fazer). Um pool com leases
não se apaga nem encolhe por baixo de um lease, e a libertação é por
declaração — não há um ceifador a adivinhar quem está vivo.hosts: [nome]numHTTPRoute(e emexpose[].hosts) escreve cada host
como127.0.0.1 <host>num bloco delimitado do/etc/hostsdo operador
(cmd/hosts_file.rs), lendo os nomes das duas instâncias do proxy. Só o bloco
é tocado; um nome já presente fora dele é recusado, e um bloco sem linhaEND
é recusado em vez de apagar o resto do ficheiro. Sem root, recusa e mostra o
bloco a acrescentar.- Deriva corrigida: uma rota para uma VM lia-se como deriva permanente,
porque oactual()só mapeava endereços para nomes de containers. - ADR-0048, fase 1 (#448): o nome padrão de serviço
<nome>.<ns>.svc.delonix. internal(o FQDN antigo continua como alias) e a colunaSVCem
container/vm/stack ls. O ADR-0047 (o proxy L7 autorizar origens de
containers, parahosts: containers) fica só como proposta, e o
hosts: gueste oannounce: l2do ADR-0046 (fase 4) estão por fazer.
Appliances de monitorização e correio (#450)
Só scripts em scripts/appliances/ (sem código do motor): imagens de
monitorização (monitoring-7.0-r5: Zabbix 7.0 LTS + Grafana + Prometheus + Loki
- NetFlow, já ligados, com um hub WireGuard
monitoring-vpn), Carbonio CE
(carbonio-26.6.0-r3), GLPI 11 (glpi-11.0.9-r1) e Wazuh 4.14.7. Cada uma com o
seuverify-*.sh.
Instalador: --performance (#440)
install.sh --performance / --no-performance; sem elas pergunta cada ponto
(Enter = sim; sem terminal, não). CPU em modo performance (perfil de energia,
governor, EPP) por um serviço que guarda o estado do arranque e o repõe no
stop; THP em madvise com irqbalance; e um timer de utilizador
system prune --auto --threshold 75 (nunca toca em volumes). Avisa se houver um
delonix-cri/-mcp/-mgmt diferente noutro ponto do PATH.
--version nos servidores irmãos (#442, #443, #445)
delonix-cri, delonix-mcp e delonix-mgmt respondem a --version/-V e
saem, em vez de arrancarem o servidor.
Binários aarch64 (#439, #446)
- Um job de CI nativo em aarch64 (
test (arm64)) faz build e corre a suite
completa; antes, os ramos#[cfg(target_arch)]nunca eram executados. - O workflow de release passa a construir e publicar
delonix,delonix-cri,
delonix-mcpedelonix-mgmtpara aarch64, com o mesmoSHA256SUMS,
assinatura e proveniência. Este é o primeiro release que os produz — ver
a lista abaixo. Oinstall.shcontinua a recusar aarch64 (o passo
seguinte não está nesta release).
O que NÃO foi validado
vm build: a instalação de pacotes (network: true), os perfis
rootless/k8sde ponta a ponta e o arranque de uma VM a partir das imagens
construídas nunca correram; nenhum construtor de appliance real foi executado
(a canalização foi provada com um construtor falso). As fases--packages,
--profile,--boote--appliancedoverify-images.shestão escritas mas
nunca executaram. O que foi provado com imagens reais: as quatro distros
offline, os dois ramos (dpkgerpm) e o auto-teste.- Serviços de VM (o que o próprio ADR-0046 marca como não medido):
- um cliente a resolver um nome do bloco do
/etc/hostsreal e a receber
resposta de um backend (escrever o ficheiro real exige root); o açúcar
expose:foi coberto porvalidate,--dry-rune testes unitários, não
por tráfego; - as duas instâncias do proxy a correr ao mesmo tempo (uma rota de container e
uma de VM libvirt no mesmoapply), uma VM libvirt em modobridge, e o
reinício do proxy depois de a VM ganhar outro endereço (a rota guarda o
endereço noapply); IPPool: um endereço realmente encaminhável (tudo foi medido com aliases de
loopback,127.0.0.10-11), dois reclamantes a esgotar um pool por duas
rotas vivas, a instância do host com um endereço do pool,expose[].pool
por tráfego, e ovalidatea recusarannounce: l2(só oapplyrecusa).
- um cliente a resolver um nome do bloco do
- Appliances #450: o cliente web do Carbonio num browser e a entrega de
correio de/para o exterior; uma falha de arranque docarbonio-videoserver
na primeira passagem, não investigada. - Instalador
--performance: o instalador de ponta a ponta, o serviço systemd
real, o timer de GC e o ganho medido. - Binários aarch64 da release: o job
build-arm64dorelease.ymlsó corre
com uma tagv*, por isso nunca tinha sido executado antes desta. O
stripem aarch64, a assinatura e a proveniência dos novos assets são a
primeira execução.