Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

.nornir/registry-guide.md

Add a repository backend — guide

Class: guide · Function: registry. The architecture is registry-design.md. Worked example: a fictional widget ecosystem whose packages live at widgets/{name}-{version}.widget inside 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:

  1. server/lib/Cargo.toml — add repository-znippy-widget as a dependency.
  2. server/lib/src/repository.rs — use repository_znippy_widget::WidgetRepoZnippy; and the match arm in backend_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".