znippy-plugin-skidbladnir — design
Class:
design· siblings:../../.nornir/plugins-design.md·../../.nornir/plugins-guide.md. Strict: states only the shape we ship. It teaches znippy's Arrow-IPC index what each entry of a Skidbladnir airgap bundle is — the Tillsynia/Njord deploy payload. It adds only metadata columns; the format, pack/move/unfold mechanics and sealing stay owned by znippy + Skidbladnir.
f0 — what it is
A Skidbladnir bundle folds a whole deployment — DB dump, document blobs, container images, infra operators + Talos boot media, declarative config, encrypted secrets — into one sealed, content-addressed archive. This plugin makes that bundle self-describing: every entry is tagged into the index so the sealed archive is queryable without unfolding it:
SELECT logical_path, component FROM bundle WHERE member_kind = 's3-blob';
SELECT version, watermark FROM bundle WHERE member_kind = 'manifest';
Native builtin, not WASM. It is a plain rlib linked into znippy-cli and registered in handlers.rs::builtin_handlers(); it does not build a cdylib and is not compiled to WASM. Discovery is via meta() like every handler. Selected with --format skidbladnir (aliases skid · airgap · tillsynia), type_id = 40 (clear of built-ins 1–18 and media 25).
f1 — the bundle layout (what it claims)
Ownership is decided purely from the relative path — no read, no parse (src/member.rs::classify, total + exhaustively tested):
tillsynia-<version>.znippy
├── lakespec.json # products + depends_on order + pki source
├── manifest/ # bundle.json (version·scope·watermark) + consistency.json
├── dbdump/ # atomic pg_dump stream (postgres)
├── s3/ # document blobs (minio; dokument.fil_nyckel)
├── app/ # container images: njord-web, njord-mail-server (oci tar)
├── infra/ # unfold operators + boot media
│ ├── terraform/ redfish/ kvm/
│ └── talos/ # talos-<ver>.iso + machineconfig.yaml
├── njordconf/ # declarative config (serverspec/services; helm optional)
└── secrets/ # *.age — encrypted; key supplied at unfold, never in bundle
f2 — the member_kind schema
MemberKind (src/member.rs) — the discriminant string in the member_kind column, and the component a member maps to:
member_kind |
claimed by | component |
lakespec |
lakespec.json |
— (bundle-level) |
manifest |
manifest/* |
— (bundle-level) |
dbdump |
dbdump/* |
postgres |
s3-blob |
s3/* |
minio |
image |
app/*.oci.tar |
web / mail-server (embedded image — archival/full tier) |
image-ref |
app/*.imageref · *.ref |
web / mail-server (registry pointer — plain tier, needs the registry) |
infra-terraform |
infra/terraform/* |
infra |
infra-redfish |
infra/redfish/* |
infra |
infra-kvm |
infra/kvm/* |
infra |
talos-iso |
infra/talos/*.iso |
infra |
talos-machineconfig |
infra/talos/* (non-iso) |
cluster |
config |
njordconf/* (serverspec/services) |
config |
helm-chart |
njordconf/**/*.tgz |
platform (the fresh-install chart — a fresh app self-migrates, so chart+values is all a platform-tier bundle needs) |
helm-values |
njordconf/values*.yaml |
platform (values overlay for the chart) |
secret |
secrets/* or any *.age |
secrets |
Arrow columns the plugin contributes (schema_fields, all Utf8 nullable):
| column | source |
member_kind |
MemberKind::as_str() |
component |
component_of(kind, path) |
logical_path |
the relative path (e.g. dokument.fil_nyckel) |
version |
parsed from lakespec/manifest JSON (else null) |
watermark |
parsed from manifest JSON — the delta clock (uppdaterad) (else null) |
encrypted |
is_encrypted(kind, path) — secret or *.age |
The per-entry BLAKE3 checksum already lives in the base index columns, so it is not duplicated here.
f3 — the ArchiveTypePlugin fill
Trait znippy-common/src/plugin.rs::ArchiveTypePlugin. Fill (NativeSkidbladnirPlugin, src/lib.rs):
| method | behaviour |
name |
"skidbladnir" |
type_id |
SKIDBLADNIR_TYPE_ID = 40 |
meta |
name + aliases (skid/airgap/tillsynia), ecosystem, extensions, the two subcommands |
matches_path |
classify(path).is_some() — claims recognised members, ignores stray files |
schema_fields |
the six columns above |
extract_metadata |
classify → fill member_kind + logical_path + encrypted (+ component); for lakespec/manifest also parse version/watermark |
run_command |
classify <path> (print kind/component/encrypted) · layout (print the canonical tree) |
extract_batch uses the trait default (per-file); the payload is small and classification is O(path), so no batch pass is needed.
f4 — laws honoured
| law | how |
| P-1 one archive = one ecosystem | --format skidbladnir selects this single handler |
| P-2 writes only declared columns | fills only its six schema_fields |
| P-3 discovery by record | listed via meta(), no name table |
| P-4 never panic | garbage/oversized JSON → version/watermark degrade to null; the member still writes with its path-derived member_kind |
f5 — read side (follow-up, not yet built)
Mirror the maven/rust/python typed views: a skidbladnir_view() that round-trips this branch of the DenseUnion out for the Skidbladnir CLI's verify / restore-order (they need an archive handle the plugin run_command does not have — hence they are not plugin subcommands). Round-trip coverage would land in plugin_read_api_test.rs.
Status
Built + green: crate (10 unit tests — classification, metadata, bomb-defense, schema/meta consistency), registered in builtin_handlers, znippy handlers lists it, classify/layout subcommands work, and an end-to-end compress --format skidbladnir round-trips all twelve member kinds through list. Read-side typed view is the open follow-up.