Add a repository backend — guide
Class:
guide· Function:registry. The architecture isregistry-design.md. Worked example: a fictionalwidgetecosystem whose packages live atwidgets/{name}-{version}.widgetinside the archive.
A backend is a crate under server/repository/ that implements RepositoryBackendTrait (traits/src/lib.rs). Five moves.
Step 1 — declare the format
In traits/src/lib.rs, add the variant to ArtifactFormat (and RepositoryType), and give it a znippy discriminant in znippy_type_id():
pub enum ArtifactFormat { /* … */ Widget, Raw }
pub fn znippy_type_id(&self) -> i8 {
match self {
// … existing arms …
ArtifactFormat::Widget => skeletons::WidgetPlugin.type_id(), // znippy owns the number
}
}
Add the alias parse in from_format_str ("widget" => ArtifactFormat::Widget) and the endpoint_name arm. The znippy_type_ids_match_znippy_skeletons test fails if your number drifts from znippy — intentional; znippy is the source of truth.
Step 2 — create the backend crate
server/repository/znippy-widget/Cargo.toml (copy a sibling like znippy-helm), then src/lib.rs:
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use traits::{ArtifactFormat, ArtifactId, RepositoryBackendTrait};
use znippy_common::{ZnippyArchive, ZnippyReader};
pub struct WidgetRepoZnippy { pub name: String, reader: Option<Arc<dyn ZnippyReader>> }
impl WidgetRepoZnippy {
pub fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> {
let archive = ZnippyArchive::open(&archive_path)?;
Ok(Self { name, reader: Some(Arc::new(archive)) })
}
fn get_file(&self, path: &str) -> Result<Vec<u8>> {
self.reader.as_ref().ok_or_else(|| anyhow!("no reader"))?.extract_file(path)
}
}
impl RepositoryBackendTrait for WidgetRepoZnippy {
fn name(&self) -> &str { &self.name }
fn format(&self) -> ArtifactFormat { ArtifactFormat::Widget }
fn is_writable(&self) -> bool { false } // znippy backends are read-only
fn fetch(&self, id: &ArtifactId) -> Result<Option<Vec<u8>>> {
let path = format!("widgets/{}-{}.widget", id.name, id.version);
Ok(self.get_file(&path).ok())
}
fn put(&self, _id: &ArtifactId, _data: &[u8]) -> Result<()> {
Err(anyhow!("Znippy repository is read-only"))
}
fn handle_http2_request(&self, suburl: &str, _body: &[u8])
-> Result<(u16, Vec<(String, String)>, Vec<u8>)>
{
let parts: Vec<&str> = suburl.trim_start_matches('/').split('/').collect();
match parts.as_slice() {
[repo, "widgets", name, version] if *repo == self.name() => {
let path = format!("widgets/{}-{}.widget", name, version);
match self.get_file(&path) {
Ok(data) => Ok((200,
vec![("Content-Type".into(), "application/octet-stream".into())], data)),
Err(_) => Ok((404, vec![], b"not found".to_vec())),
}
}
_ => Ok((404, vec![], b"not found".to_vec())),
}
}
}
The two methods that carry all the per-ecosystem logic are fetch (the ArtifactId→path mapping) and handle_http2_request (the protocol framing). If your ecosystem has an index (a packument, a /simple/ page, an index.yaml), generate it here by scanning the archive — see znippy-helm/src/index.rs for a worked on-demand-index generator (parses each chart's Chart.yaml, computes a SHA-256, emits a valid index.yaml).
Step 3 — register it
Two edits wire the plugin into the engine:
server/lib/Cargo.toml— addrepository-znippy-widgetas a dependency.server/lib/src/repository.rs—use repository_znippy_widget::WidgetRepoZnippy;and the match arm inbackend_from_config():
"widget" => znippy_backed!(WidgetRepoZnippy, "Widget"),
That match is the registry. Nothing on the request path changes — dispatch is already Arc<dyn RepositoryBackendTrait> through FastRoutes.
Step 4 — configure & route
Repository(
ron_name: "widget-prod",
ron_repo_type: "widget",
ron_upstreams: [],
ron_archive_path: Some("/var/lib/holger/widgets.znippy"),
ron_out: Some(RepositoryIO( ron_storage_endpoint: "znippy", ron_exposed_endpoint: "default" )),
)
wire_holger() instantiates the backend, wires it to the default exposed endpoint, and adds it to that endpoint's FastRoutes. A request for repo widget-prod now dispatches to WidgetRepoZnippy.
Step 5 — publish order
Append the crate to publish_order in workspace_holger/release/nornir.toml, in the same parallel group as the other server/repository/znippy-* crates, before server/lib.
LAW reminder: ship a .nornir design + guide note for the new backend, and make its tests inject real input and assert real output (open a tiny fixture archive, fetch a known id, assert the exact bytes) — never just "didn't panic".