Skip to content

Releases: ngolacloud/delonix

v0.1.0 — Delonix: a primeira release da nova identidade

Choose a tag to compare

@github-actions github-actions released this 28 Oct 11:16

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-se delonix.
  • Linha de versão: scripts/version_gate.py passou a ser consciente de linha — a
    linha delonix-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 como BREAKING no CHANGELOG); patch é compatível.
    A estabilização para SemVer estrito chega com 1.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 quando docs/dev/rt4-phase1-impl-map.md entrar no
    catálogo de páginas.
  • Imagens GHCR da linha anterior ficam no namespace antigo; a partir desta linha
    publicam-se em ghcr.io/ngolacloud/delonix.

v5.1.1 — o instalador mostra onde está a release, e o `--l18n` deixa de ser aceite depois de um subcomando

Choose a tag to compare

@github-actions github-actions released this 15 Oct 10:22

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.yml corre em push para a main e chumbou em 54179fdb e cb047291.
    Em a0da90f5, a tag, chumbou o rustfmt (diferenças em scaffold.rs) e o
    clippy -D warnings (dead_code nos dois helpers acima, e
    type_complexity no templates.rs gerado), 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 o rustfmt, os documentos gerados e dois testes
    (the_published_matrix_is_the_generated_one e
    no_string_in_the_sources_tells_the_operator_to_run_a_dead_command).
  • O release.yml falhou 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 o release.yml do 727c8d3d (a main
    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 --l18n e os endereços do instalador, como
    descrito acima.
  • Não corrido: a bateria scripts/e2e.sh, o arnês de caos e o cargo 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

Choose a tag to compare

@github-actions github-actions released this 15 Oct 08:08

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 é o KEPT_CAPS do motor em modo reject: um pod normal corre como
    dantes, e privileged: true ou um capabilities.add acima desse conjunto
    são recusados no CreateContainer. 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 planas vm build|convert|pull|push|ls-remote
    falham com unrecognized subcommand. Corte limpo, sem alias: o grupo vm
    está na lista dos não-estáveis.
  • delonix init recusa mais do que recusava (#842, #843): uma porta
    que este host não consegue publicar pára o --up antes 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 no template.meta pára o build do próprio
    motor.
  • cluster kubeadm recusa 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:

  1. o container de trabalho do build corre na netns do HOST (mesmo inode),
    onde a userns do motor não tem CAP_NET_BIND_SERVICE;
  2. nginx -t binda cada listen que lê — não é um parser;
  3. e tolera EADDRINUSE: só o EACCES é fatal, por isso o gate óbvio
    («ocupa a porta e constrói») passaria sobre o binário defeituoso;
  4. só o nginx tem o problema — httpd -t e haproxy -c construí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): denyPrivateEgress na 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 uma allowlist: de rede nem um egress allow a reabrem.
  • cluster apply pode 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.sh verifica os user namespaces através do perfil AppArmor do
    delonix
    (#775), que é o caminho real — não genericamente.
  • Avisos de dependência: rustls 0.23.46 (#839), unsafe-libyaml nomeado
    com a razão (#833).

Kubernetes (CRI)

  • cpuset_mems, unified e hugepage_limits: honrar-ou-recusar, com a
    sonda feita no cgroup onde o container vai REALMENTE viver (ADR-0038 item 3,
    #752).
  • UpdateContainerResources implementado 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_period parcial é 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 cri avisa 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
    Replace deitava fora o disco overlay.
  • Prontidão de VMaaS (#741): guest agent, o vm ssh corrigido na netns,
    e vm.disk.resize.
  • etcd backup/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ó

  • VirtualMachineService servido (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 drivers btrfs, zfs e lvm-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 ps sem -a lista só o que corre, e a API Docker honra o
    all e devolve as portas (#816).
  • vm image <verbo> como ponto de entrada único (#772).
  • O COPY do VMfile mantém o significado que tem num Dockerfile (#783).
  • secret rotate-key já não faz a versão recuar (ACH-034,...
Read more

v5.0.2 — uma rota entre redes passa a servir os containers ligados a mais de uma

Choose a tag to compare

@github-actions github-actions released this 11 Oct 00:50

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::plan calcula-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_routes executa-o: no netroute add/del (todos os
    containers) e no attach-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 warnings do
    delonix-sdn, version_gate, arch_fitness e lang_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 a v5.0.2 de
    volta na main.

v5.0.1 — duas falhas que abriam em vez de fechar: `--secret-files` e a admissão por scan

Choose a tag to compare

@github-actions github-actions released this 10 Oct 20:39

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 testes scan do
    delonix-runtime-bin, cargo clippy -D warnings dos dois crates tocados,
    version_gate, arch_fitness (o library_prints sobe de 89 para 90 com a
    linha de erro do init, já na linha de base do commit) e lang_ratchet.
  • Não corrido neste ramo: a bateria scripts/e2e.sh e o arnês de caos — o
    ramo de manutenção não tem CI própria (o ci.yml só corre na main). A CI
    completa corre no PR que funde a v5.0.1 de volta na main.

v5.0.0 — o `USER` da imagem é quem corre, o bloco `provider` tipado, e o contrato de nó servido

Choose a tag to compare

@github-actions github-actions released this 08 Oct 15:57
2837f58

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 o USER da 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
    run di-lo em voz alta, em vez de falhar.
  • O --user deixou 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/passwd e o próprio binário do serviço — e
    atravessava os mounts, re-apropriando um bind mount do HOST (um ficheiro de
    1000:1000 passava 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. No odoo:20.0 (~2 GB), o primeiro arranque com -u odoo passou 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 build segue a mesma regra: um RUN corre como o USER em vigor nesse
    passo (antes corria sempre como root, mesmo depois de um USER 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 em 127.0.0.1 depois
    de um start — que é o que um unit do net boot enable corre, 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 em 0.0.0.0, exposta à LAN inteira,
    porque a shell que correu o start tinha 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 de spec.containers[] de um Pod, onde as
    probes e o lifecycle não existem e até aqui desapareciam em silêncio.
    stack validate continua 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: false de um SystemContainer respondia DX-1000 unknown field — check the spelling a um campo bem escrito; agora responde DX-1540, que diz o
    que o campo é.
  • O mesmo nome de Pod noutra namespace é um conflito, não «already exists»; e
    Container e Service passaram 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). Um plan que 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_netfilter as 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, o iifname que a camada IP
    vê é a BRIDGE, nunca a porta. Medido: um container com NET_ADMIN forjava 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-check existem para um container que
    encaminha, mas são PERMISSÕES na política do nó. Sem política, nada é concedido,
    e mode: warn não concede.

Ciclo de vida

  • Um --wait que esgota o tempo sai com 124 (DX-8503) e deixa a VM a correr
    (decisão D6, #661). Antes devolvia 0: um --wait que não viu a VM responder
    reportava sucesso.
  • --device-read-bps/--device-write-bps são recusados quando não há disco que
    os aceite
    (#663). O io.max passou a nomear o disco em que o rootfs do
    container vive; antes o limite era escrito e não aplicado.
  • Um rm -f que desistiu não é ressuscitado pelo supervisor (#647). Com o
    disco saturado, um rm -f devolvia DX-8101 e 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 stop que desistiu mantém o processo registado, e um start que falha
    depois d...
Read more

v4.5.0 — rede que falha fechada, Proxmox medido rota a rota, providers num ficheiro por nó

Choose a tag to compare

@github-actions github-actions released this 28 Sep 15:57
7b1a766

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 um dir, uma action ou uma política fora dos valores documentados:
    antes, um dir desconhecido era lido como in e uma política desconhecida
    como allow.
  • Uma falha do isolamento de namespace é um erro, não um aviso, em
    container run, start e pod create. O attach é desfeito. Antes, o run
    saía com rc=0 e o container ficava alcançável a partir de outra namespace.
  • O egress da origem é avaliado sempre. Antes, um accept na chain do
    destino terminava a avaliação e a política egress deny da origem nunca
    corria (medido: A→B passou 3/3 com o egress de A em deny). Agora são duas
    base chains, fwout e fwcont.
  • Regras de entrada explícitas deixam de desligar o isolamento (#560). Um
    container com, por exemplo, ingress deny tcp/22 passava a aceitar ligações
    novas de qualquer outra namespace em todas as outras portas. Agora há um
    guardrail que nenhuma regra de entrada retira; um allow explí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 namespace default
    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/disconnect falham quando a firewall falha, em vez de
    avisar e sair com 0.
  • network rm recusa 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. O stack --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); o stop mantém o
    lease e o start volta ao mesmo endereço; o rm liberta também os leases
    das redes extra. --subnet 10.200.0.0/16 (o espaço do ingress) e
    10.0.2.0/24 passam a ser recusados.
  • O pool de DHCP das VMs vive dentro da rede. Numa rede que não é /16 o
    DHCP servia a rede errada (numa /24 oferecia um endereço /16 com outro
    router, e a VM ficava sem IP). O pool fica agora no topo da rede, com
    min(240, tamanho/8) endereços; num /16 continua .254.10–.249 byte a
    byte, para nenhuma VM existente mudar de IP. Consequência: um /28 passa 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/burst só dígitos, e o type de
    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 bridge valida as subnets: nem default, 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
    respondem 501 com DX-6302
    (#555): firewall, egress, attach e
    attach-extra. As leituras e o publish, que grava o registo, continuam.
    Foi uma decisão explícita: recusar todas, em vez de as alargar.

Containers e cgroups

  • --cpuset, --io-weight e --device-read/write-bps/iops são recusados
    (exit 69) quando o cgroup do container não tem o controlador
    (#545). Antes,
    --device-write-bps 5mb escrevia a 1,6 GB/s e saía com 0, só com um aviso. A
    válvula de escape DELONIX_ALLOW_UNENFORCED_LIMITS continua a valer. Chega a
    todos os pontos de entrada (CLI, manifesto, compose, Docker API, CRI); o CRI
    passa a receber exit 69 para um cpuset_cpus sem cpuset, como já recebia
    para CPU e memória. Como root, o delonix.slice passa a delegar também o
    cpuset, que antes era ignorado em silêncio.
  • sudo delonix system setup --delegate escreve 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. Sob sudo, o relatório passa a ser sobre o
    utilizador que escreveu o sudo, e o passo seguinte nomeia
    systemctl restart user@<uid>.service: um daemon-reload sozinho é um falso
    verde.
  • container rm -f só volta quando o processo morreu (#562). Antes voltava
    no sinal: com o disco carregado, o delete pod --force voltava 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 do stop. Passado esse limite, mantém o registo e
    devolve DX-8101 container.still_exiting (exit 124); um rm -f repetido
    remove-o quando o processo morrer.
  • Um container --rm é montado com o overlay volatile (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 true em
    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 defaultProvider que o processo não consegue servir falha com 69, a
    nomear o provider (ADR-0054, #514). Antes era descartado, e o vm create
    seguinte podia criar a VM num hipervisor local. Um providers.yaml
    ilegível faz falhar os pedidos de VM sem --backend; os comandos que não
    escolhem provider continuam a funcionar.
  • Um providers.yaml com type: opnsense passa a registar o appliance
    (#556). Registar não contacta nada.
  • Proxmox: network com o nome de uma rede do motor é recusado (#478). Antes
    era aceite e deitado fora: o registo dizia Network: lab-net e a VM ficava
    no vmbr0.
  • Proxmox: uma tarefa que acaba em WARNINGS: <n> é um sucesso (#546), e as
    linhas WARN: 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
    reloadnetworkall do PVE acaba OK logo 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 --require com 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 down recusa uma raiz que não existe, em vez de a recriar.
  • kind: App deixa 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_up e no apply_firewall_all (nsenter para 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 rm nã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 o deny da anterior. Um publish que falha a meio desfaz só as
    portas que publicou; uma rede criada por uma VM passa a aparecer no
    network ls; o httproute escreve 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...
Read more

v4.4.0 — `drift`, a matriz do Compose com denominador, e um portão de performance que se recusa a julgar

Choose a tag to compare

@github-actions github-actions released this 23 Sep 12:48
ae5a26d

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, um nft à 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 do plan e do diff.
  • 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_wiring lê o stack.rs em vez de confiar num
    comentário — e chumbou: o network_access_rule::actual recebe docs e
    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 config em 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 ao compose config o que acontece.
    As 88 concordam.
  • delonix migrate assess -f docker-compose.yml é a outra metade. O
    compose up pá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 json e --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 como missing o que o motor recusa com mensagem própria; e
    estava INCOMPLETA: sysctls, ulimits, gpus e userns_mode têm flag no
    container run e 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.
  • cri e oci continuam 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.py LÊ 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: 0 dentro da tolerância (25%), 1 regressão, e
    3 recusa-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: --json era aceite e
    IGNORADO (a variável era atribuída e nunca lida); time_n não olhava para o
    exit status, por isso um docker com 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_*.py corria em CI — o
    test_release_verify.py nasceu no dia anterior com dez testes que nada
    executava. Job script-tests novo, 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 apply sobe ou aperta; nunca o baixa — teardown: false, e
    stack destroy/--prune recusam-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_ROOT isolado: um container run --privileged
    REALMENTE recusado por uma política aplicada por manifesto; reaplicar com um
    campo mudado converge in-place (stack plan mostra ~, 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
    o type=secret já usava (mount_live, invisível à imagem final). O directório
    é <root>/build-cache/mounts/<sha256(id)> — em hash, nunca a string crua:
    um id omitido é por omissão o target, um caminho arbitrário controlado pelo
    Dockerfile. sharing= e ro são recusados em vez de aceites-e-ignorados.
    Validado ao vivo: dois delonix build separados escreveram no MESMO cache mount
    e o segundo viu o que o primeiro escreveu; o conteúdo nunca apareceu na imagem
    final (confirmado por container run dentro dela).
  • SBOM SPDX 2.3 automático por imagem construída — delonix build escreve-o
    best-effort logo a seguir a um commit com sucesso, e delonix image sbom <ref>
    serve-o. Reutiliza o extract_sbom já existente (apk/dpkg, sem montar nem
    correr a imagem). Validado ao vivo: um build de alpine:3.20 + apk add curl
    produziu um SBOM com 25 pacotes, e o image sbom devolveu 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
    --mount estavam em português desde sempre e nunca tinham sido revistas.

POST /images/create na Docker API (M02, #460)

  • O docker pull através do socket passa a funcionar: streaming chunked-JSON,
    delegando no pull que já existe. Movido de API_UNIMPLEMENTED para a matriz.
  • Validado com um docker CLI 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 de ARG B=${A}${A} dobra o valor guardado a cada linha — uma bomba
    de expansão expone...
Read more

v4.3.0 — `delonix hosts sync`, e o instalador passa a aceitar aarch64

Choose a tag to compare

@github-actions github-actions released this 21 Sep 22:02
ec5f5ab

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 sync publica 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 com hosts: [host] e um publicado aqui partilham um bloco e uma
    regra de recusa. Todos os nomes apontam para 127.0.0.1: é a porta publicada
    do proxy L7 que o host usa para chegar a uma carga na SDN.
  • --print mostra o bloco e não toca em nada; --off remove-o e deixa
    de o manter.
  • Depois de ligado, container run --expose e rm mantêm o bloco atual, em
    melhor esforço: um nome que uma rota pediu falha o apply, mas um nome
    publicado por este comando é uma conveniência que o operador ligou, e um
    run --expose sem 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 sudo lê 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 o vm bridge já tinha (adopt_invoking_user_root); um
    DELONIX_ROOT explícito continua a ganhar.
  • Grupo novo, não estável (docs/cli-stability.md): documentado na tabela do
    README.rst e no site, com página própria (docs/comandos/hosts.html).

Instalador: aceita aarch64 (#447)

  • O install.sh aceita aarch64 (e arm64, normalizado) em vez de recusar tudo
    o que não é x86_64. O nome do asset compõe-se de uname -m:
    delonix[-cri|-mcp|-mgmt]-aarch64-linux — os que o release.yml publica
    desde a v4.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 pede qemu-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.fd nem o hypervisor-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.sh que a v4.2.0 publicou é o antigo, que
    ainda recusa aarch64. Este é o primeiro release cujo install.sh o aceita.

Documentação

  • Manual do contribuidor (docs/dev/) atualizado para a v4.2.0 (#453):
    vm build/vm.yaml, o verify-images.sh, os binários aarch64, o
    --performance e o nome padrão de serviço.
  • Notas da v4.2.0 corrigidas (#454): afirmavam que uma VM libvirt era
    recusada e que kind: IPPool era uma fase posterior. Ambas estão construídas
    (fases 2 e 3 do ADR-0046); só o announce: l2 e o hosts: 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/hosts real (exige root; foi
    provada só num ficheiro de teste, verificando o conteúdo por md5: escrita,
    idempotência, recusa por conflito, recusa sem permissão e --off a 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, um container run --expose
    só avisa, e o hosts 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 caminhos dnf/apk.
    O que foi provado: a composição dos nomes de asset — a função fetch_asset
    real do script, contra os assets publicados da v4.2.0, com controlos
    negativos.
  • O que a v4.2.0 já listava continua por validar: a instalação de pacotes
    e os perfis dourados do vm 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

Choose a tag to compare

@github-actions github-actions released this 21 Sep 20:18
0491548

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 build existe agora. Até aqui só havia delonix image vm build; os dois partilham o mesmo conjunto de argumentos e fazem o mesmo.
  • vm build . escolhe a receita como o docker build: um -f explícito
    decide (um .yaml/.yml lê-se como vm.yaml, o resto como VMfile); sem
    -f, um vm.yaml na pasta vence um VMfile, que vence a receita dourada.
    -t dá o nome do resultado e o ${TAG} dentro do ficheiro; --target
    escolhe uma imagem quando o ficheiro declara várias. Com um vm.yaml, o -t
    passa a ser opcional (usa o tag: da imagem).
  • vm.yaml é o «compose» do VMfile (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: um VMfile sintetizado, a receita dourada (profile: rootless|k8s),
    um VMfile existente (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.yaml nomeia um construtor (builder: proxmox), nunca um caminho: o
    motor resolve-o para scripts/appliances/build-<nome>.sh dentro do
    repositório, valida os argumentos e as variáveis de ambiente (sem PATH,
    LD_*, BASH_ENV…), corre-o com um OUT_DIR isolado ao lado do store,
    importa o único qcow2 que ele deixa e apaga a pasta de trabalho, mesmo em
    falha. Um vm.yaml alheio não consegue fazer o host correr um ficheiro à sua
    escolha.
  • images/: uma pasta por imagem, cada uma com vm.yaml e 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/doc de propósito: tem as licenças que uma imagem
    publicada tem de redistribuir.
  • scripts/verify-images.sh constrói as receitas num DELONIX_ROOT isolado 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
    delonix com sudo, por isso verificar isso na receita entregue não provava
    nada) e um --self-test que exige que as verificações falhem numa imagem
    que ninguém construiu.
  • Um teste fecha o ciclo: every_shipped_recipe_is_valid_and_complete falha
    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.yaml e dos appliances passam pelo catálogo (pt.po).

Serviços de VM por nome (#444, #448, ADR-0046/0047/0048)

  • VirtualMachine.spec.expose publica um serviço HTTP/S de uma VM por nome:
    baixa no load para um HTTPRoute/<vm>-expose. O resolvedor de rotas conhece
    VMs Cloud Hypervisor (na SDN) e VMs libvirt em nat/bridge (ADR-0046,
    fase 2): estas chegam-se por uma segunda instância do proxy no netns do host,
    sem root, que escuta só em 127.0.0.1. Uma VM libvirt em modo user-mode não
    tem endereço alcançável e é recusada com a correção (netMode: nat em
    qemu:///system); um documento que mistura backends das duas metades é
    recusado, com a indicação de qual cai em qual.
  • kind: IPPool (grupo networking; 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 com spec.pool (e expose[].pool). Uma rota reclama um só
    endereço enquanto estiver declarada, fica alcançável nele em vez de em
    127.0.0.1, e o hosts: [host] aponta o nome para ele. É um Kind completo
    (apply, plan, deriva, --prune, get/describe/delete ippools) e aplica-se
    antes do HTTPRoute. Só announce: local: o endereço já tem de estar numa
    interface do host, e o apply verifica-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] num HTTPRoute (e em expose[].hosts) escreve cada host
    como 127.0.0.1 <host> num bloco delimitado do /etc/hosts do 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 linha END
    é 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 o actual() 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 coluna SVC em
    container/vm/stack ls. O ADR-0047 (o proxy L7 autorizar origens de
    containers, para hosts: containers) fica só como proposta, e o
    hosts: guest e o announce: l2 do 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
    seu verify-*.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-mcp e delonix-mgmt para aarch64, com o mesmo SHA256SUMS,
    assinatura e proveniência. Este é o primeiro release que os produz — ver
    a lista abaixo. O install.sh continua 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/k8s de 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, --boot e --appliance do verify-images.sh estão escritas mas
    nunca executaram. O que foi provado com imagens reais: as quatro distros
    offline, os dois ramos (dpkg e rpm) 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/hosts real e a receber
      resposta de um backend (escrever o ficheiro real exige root); o açúcar
      expose: foi coberto por validate, --dry-run e 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 mesmo apply), uma VM libvirt em modo bridge, e o
      reinício do proxy depois de a VM ganhar outro endereço (a rota guarda o
      endereço no apply);
    • 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 o validate a recusar announce: l2 (só o apply recusa).
  • Appliances #450: o cliente web do Carbonio num browser e a entrega de
    correio de/para o exterior; uma falha de arranque do carbonio-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-arm64 do release.yml só corre
    com uma tag v*, por isso nunca tinha sido executado antes desta. O
    strip em aarch64, a assinatura e a proveniência dos novos assets são a
    primeira execução.