Um projecto Rust completo#
Escrever código que funciona é o primeiro terço. Os outros dois são mantê-lo e deixar outros mexerem-lhe sem medo. Este capítulo é a lista de decisões de um projecto de sistemas em Rust que a comunidade reconhece como saudável — e cada uma está aplicada neste repositório, que podes clonar e usar como template.
Estrutura#
delonix-rust-tutorial/
├── Cargo.toml # workspace + lints partilhados + perfil de release
├── rustfmt.toml clippy.toml deny.toml
├── minicontainer/ # o projecto (biblioteca + binário `mc`)
│ ├── src/ lib.rs main.rs error.rs spec.rs image.rs container.rs cgroup.rs state.rs fsutil.rs
│ └── tests/e2e.rs # testes de integração: correm o binário a sério
├── examples/ # cada exemplo dos capítulos, compilado e testado
├── scripts/ # demo.sh, make-rootfs.sh, extract-snippets.py …
├── content/ site-src/ build.py # este site
└── .github/workflows/ # ci.yml (qualidade) e pages.yml (publicação)
Três decisões a copiar:
- Biblioteca + binário fino.
lib.rstem a lógica (testável, reutilizável);main.rssó faz parse de argumentos e traduz erros em códigos de saída. Se a lógica vivesse nomain, só se testaria por subprocesso. tests/para o que é do binário,#[cfg(test)]para o que é da função. Os testes de integração usamenv!("CARGO_BIN_EXE_mc")— o binário real.- Um crate
examplespara o código dos capítulos: se um exemplo do tutorial estiver errado, o CI fica vermelho. Documentação que não compila mente.
Lints num só sítio#
[workspace]
resolver = "3"
members = ["minicontainer", "examples"]
[workspace.package]
edition = "2024"
rust-version = "1.90"
license = "Apache-2.0"
repository = "https://github.com/angolardevops/delonix-rust-tutorial"
# Lints partilhados: um sítio só, herdados por cada crate com `[lints] workspace = true`.
[workspace.lints.rust]
unsafe_op_in_unsafe_fn = "deny"
missing_debug_implementations = "warn"
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
unwrap_used = "warn" # nunca em código de produção; testes podem (allow local)
dbg_macro = "warn"
todo = "warn"
[profile.release]
lto = "thin"
codegen-units = 1
panic = "abort" # um container runtime não faz unwinding através de fork/exec[workspace.lints]— definidos uma vez, herdados com[lints] workspace = trueem cada crate. Sem isto, cada crate diverge.unwrap_used = "warn"— combinado comclippy.toml(allow-unwrap-in-tests = true): proibido em produção, livre nos testes.panic = "abort"no release — um runtime que fazfork/execnão faz unwinding através deles.unsafe_op_in_unsafe_fn = "deny"— cada operação insegura tem o seu bloco e o seu// SAFETY:.
Edição, MSRV e resolver
edition = "2024" e resolver = "3" são o presente; rust-version (o MSRV, versão mínima suportada) declara-se para o Cargo recusar compilar com um toolchain antigo com uma mensagem clara, em vez de um erro críptico a meio.
A pirâmide de testes#
| Nível | Onde | O que prova | Neste repositório |
|---|---|---|---|
| Unidade | #[cfg(test)] |
uma função pura, em microssegundos | spec, cgroup::limit_files, reconcile |
| Documentação | /// com ``` |
que o exemplo compila e o contrato | typestate com compile_fail |
| Propriedade/concorrência | #[cfg(test)] |
invariantes sob entradas/corridas | state: 16 threads a actualizar |
| Integração | tests/ |
o binário, com o kernel a sério | e2e.rs: PID 1, exit codes, capabilities |
| Conformidade | fora do repo | outro implementador cumpre a mesma norma | o mesmo bundle no runc |
| Caos | script | sobrevive a falhas injectadas | (no delonix: scripts/chaos.sh) |
Regras que o delonix aprendeu a pagar, e que os testes deste tutorial seguem:
Um teste que passa com o código apagado não prova nada
Antes de confiar num teste, reverte a correcção e vê-o falhar. O delonix chama a isto «verificado pela regra do repo»: um cenário de caos sobre convergência tinha a asserção «o PID não mudou», que um apply que não faz nada também satisfaz. A asserção certa observa o efeito (o registo mudou e o plano seguinte não tem nada a propor).
«Passou» não é «correu»
Os testes E2E deste repositório saltam com aviso quando o host não permite user namespaces — mas um salto silencioso lê-se como verde. Por isso o salto é dito (SKIP: user namespaces indisponíveis), e no delonix os runners alojados do GitHub bloqueiam userns e o job de caos fica «verde a saltar tudo»: um verde por ausência de execução é indistinguível de um verde por sucesso se só se olhar para o topo.
Um teste de concorrência a sério não pode ser óbvio. Este falha sem o flock:
#[test]
fn concurrent_updates_do_not_lose_writes() {
let (_d, s) = store();
let mut init = st("c");
init.pid = 0;
s.create(&init).unwrap();
let s = std::sync::Arc::new(s);
let handles: Vec<_> = (0..16)
.map(|_| {
let s = s.clone();
std::thread::spawn(move || {
s.update("c", |st| {
let v = st.pid;
std::thread::sleep(std::time::Duration::from_millis(2));
st.pid = v + 1;
Ok(())
})
.unwrap();
})
})
.collect();
handles.into_iter().for_each(|h| h.join().unwrap());
assert_eq!(s.load("c").unwrap().pid, 16);
}CI: o que corre em cada PR#
name: ci
on:
push: { branches: [main] }
pull_request:
permissions: { contents: read }
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -D warnings
MC_REQUIRE_E2E: "1" # em CI os testes E2E não podem saltar: falham se o runner não os deixar correr
jobs:
rust:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with: { components: "rustfmt, clippy" }
- uses: Swatinem/rust-cache@v2
- run: sudo apt-get update && sudo apt-get install -y busybox-static jq
# O Ubuntu 24.04 restringe user namespaces sem privilégio (AppArmor). Os testes E2E do
# minicontainer precisam deles; se o runner os recusar, os testes SALTAM com aviso.
- run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
- run: cargo fmt --all --check
- run: cargo clippy --workspace --all-targets -- -D warnings
- run: cargo test --workspace
- run: cargo test --workspace --doc
deny:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
site:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- run: pip install markdown
- run: python3 build.py --checkQuatro gates independentes: formatação (zero discussões de estilo), clippy com -D warnings (avisos são erros), testes (incluindo doc tests), e cargo deny (licenças, advisories e fontes das dependências). O job site garante que nenhum link interno do tutorial está partido.
# cargo-deny: licenças, advisories e fontes das dependências.
[graph]
all-features = true
[advisories]
version = 2
yanked = "deny"
[licenses]
version = 2
allow = ["MIT", "Apache-2.0", "Unicode-3.0"]
confidence-threshold = 0.9
unused-allowed-license = "allow"
[bans]
multiple-versions = "warn"
wildcards = "deny"
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]Fixa o que corre, não só o que compila
Usa cargo deny para recusar dependências com licença inesperada ou vulnerabilidade conhecida, e mantém a árvore pequena: cada dependência é superfície de ataque. O delonix confina dependências pesadas (ratatui, schemars, hyper) ao crate que precisa e verifica com cargo tree -e normal -p <crate> que os crates de mecanismo continuam limpos.
Documentação que se mantém#
///com exemplos — testados porcargo test --doc.//!no topo de cada módulo — porquê o módulo existe, não o que faz (isso lê-se no código). Vê os dominicontainer: quase todos explicam uma decisão ou uma armadilha.- ADRs (Architecture Decision Records) — um ficheiro por decisão de fronteira, com contexto, decisão e consequências. Uma decisão sem ADR é uma opinião. O delonix tem dezenas em
docs/adr/. - Comentários que dizem o que foi medido: «MEASURED 2026-09-15, k8s 1.36.4, …». Um comentário sem evidência envelhece; um com a medição pode ser contestado.
Versões e releases#
- SemVer:
MAJOR.MINOR.PATCH, e no Rust a API pública é o contrato —cargo semver-checksapanha quebras acidentais. - A versão no
Cargo.tomlestá sempre alinhada com a última tag publicada (o delonix impõe-o comversion_gate.py): «duas builds com a mesma versão não são a mesma build», e uma versão «de trabalho» que já não corresponde a nenhuma tag confunde quem reporta bugs. - Perfil de release com
lto = "thin",codegen-units = 1para um binário menor e mais rápido. - Reprodutibilidade:
Cargo.lockcommitado para binários e aplicações.
Segurança: hábitos, não auditorias#
- Valida na fronteira com tipos (newtype), não com
ifespalhados. - Recusa, não ignores: um campo aceite e ignorado é pior que um campo que não existe (é a regra que o
Spec::validatedominicontainercumpre para oseccomp). unsafemínimo e comentado.- Verifica o que descarregas (digest, checksum) antes de o usares, e falha fechado.
- Sem segredos em argumentos (visíveis no
ps): ficheiro com0600oustdin. - Ficheiros temporários com
O_EXCLe nome único, nunca um caminho fixo em/tmp(o delonix teve uma escalada de privilégio local exactamente por escrever/tmp/x.opara obpftoolroot carregar).
A checklist#
- [ ] Workspace com lints partilhados e
rustfmt.toml/clippy.toml - [ ] Biblioteca + binário fino; erros tipados na biblioteca
- [ ] Newtypes para tudo o que entra em caminhos, comandos ou nomes
- [ ]
unsafesó onde inevitável, sempre com// SAFETY: - [ ] Testes: unidade, doc, integração — e pelo menos um que prove o efeito, não o retorno
- [ ] CI:
fmt,clippy -D warnings,test,deny - [ ]
--helpe mensagens de erro que dizem o remédio - [ ] ADR para cada decisão de fronteira
- [ ] Nada aceite e ignorado em silêncio