use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ArtifactId {
pub namespace: Option<String>,
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum ArtifactFormat {
Maven3,
Pip,
Rust,
Go,
Nuget,
Npm,
Gem,
Deb,
Rpm,
Helm,
Docker,
Conda,
Composer,
Znippy,
Raw,
}
impl ArtifactFormat {
pub fn znippy_type_id(&self) -> i8 {
use znippy_common::plugins::{cargo_native, skeletons};
use znippy_common::plugin::ArchiveTypePlugin;
match self {
ArtifactFormat::Rust => cargo_native::CargoPlugin::new().type_id(),
ArtifactFormat::Pip => 2,
ArtifactFormat::Maven3 => 3,
ArtifactFormat::Go => skeletons::GoPlugin.type_id(),
ArtifactFormat::Nuget => skeletons::NugetPlugin.type_id(),
ArtifactFormat::Npm => znippy_common::plugins::npm_native::NpmPlugin.type_id(),
ArtifactFormat::Rpm => znippy_common::plugins::rpm_native::RpmPlugin.type_id(),
ArtifactFormat::Deb => znippy_common::plugins::deb_native::DebPlugin.type_id(),
ArtifactFormat::Gem => znippy_common::plugins::gem_native::GemPlugin.type_id(),
ArtifactFormat::Docker => skeletons::DockerPlugin.type_id(),
ArtifactFormat::Helm => skeletons::HelmPlugin.type_id(),
ArtifactFormat::Conda => znippy_common::plugins::conda_native::CondaPlugin.type_id(),
ArtifactFormat::Composer => skeletons::ComposerPlugin.type_id(),
ArtifactFormat::Znippy => 100,
ArtifactFormat::Raw => 0,
}
}
pub fn from_format_str(s: &str) -> Option<Self> {
Some(match s.to_lowercase().as_str() {
"maven2" | "maven" | "maven3" | "java" => ArtifactFormat::Maven3,
"pip" | "pypi" | "python" => ArtifactFormat::Pip,
"rust" | "cargo" => ArtifactFormat::Rust,
"go" | "golang" => ArtifactFormat::Go,
"nuget" | "dotnet" => ArtifactFormat::Nuget,
"npm" | "node" => ArtifactFormat::Npm,
"gem" | "ruby" | "rubygems" => ArtifactFormat::Gem,
"deb" | "debian" | "apt" => ArtifactFormat::Deb,
"rpm" | "yum" | "dnf" => ArtifactFormat::Rpm,
"helm" | "chart" => ArtifactFormat::Helm,
"docker" | "oci" => ArtifactFormat::Docker,
"conda" | "anaconda" => ArtifactFormat::Conda,
"composer" | "php" => ArtifactFormat::Composer,
"znippy" | "snippy" => ArtifactFormat::Znippy,
"raw" => ArtifactFormat::Raw,
_ => return None,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum StorageType {
Znippy,
Rocksdb,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum RepositoryType {
Rust,
Pip,
Maven3,
Go,
Nuget,
Npm,
Gem,
Deb,
Rpm,
Helm,
Docker,
Conda,
Composer,
Znippy,
Raw,
}
impl RepositoryType {
pub fn endpoint_name(&self) -> &'static str {
match self {
RepositoryType::Rust => "rust",
RepositoryType::Pip => "pip",
RepositoryType::Maven3 => "maven3",
RepositoryType::Go => "go",
RepositoryType::Nuget => "nuget",
RepositoryType::Npm => "npm",
RepositoryType::Gem => "gem",
RepositoryType::Deb => "deb",
RepositoryType::Rpm => "rpm",
RepositoryType::Helm => "helm",
RepositoryType::Docker => "docker",
RepositoryType::Conda => "conda",
RepositoryType::Composer => "composer",
RepositoryType::Znippy => "znippy",
RepositoryType::Raw => "raw",
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ArchiveInfo {
pub file_count: u64,
pub total_uncompressed_bytes: u64,
pub archive_path: String,
}
#[async_trait]
pub trait RepositoryBackendTrait: Send + Sync {
fn name(&self) -> &str;
fn format(&self) -> ArtifactFormat;
fn is_writable(&self) -> bool;
fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>>;
fn put(&self, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()>;
fn archive_files(&self, prefix: Option<&str>) -> anyhow::Result<Vec<String>> {
let _ = prefix;
Ok(Vec::new())
}
fn archive_info(&self) -> anyhow::Result<ArchiveInfo> {
Ok(ArchiveInfo::default())
}
fn list(&self, name_filter: Option<&str>, limit: usize) -> anyhow::Result<Vec<ArtifactEntry>> {
let _ = (name_filter, limit);
Ok(Vec::new())
}
fn has_archive(&self) -> bool {
false
}
fn handle_http2_request(
&self,
method: &str,
suburl: &str,
body: &[u8],
) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)>;
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Default)]
pub enum UpstreamAuth {
#[default]
None,
Basic { username: String, password: String },
Bearer { token: String },
ApiKey { header: String, key: String },
Mtls { cert_pem: String, key_pem: String },
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ArtifactMeta {
pub size: Option<u64>,
pub sha256: Option<String>,
pub content_type: Option<String>,
}
pub trait RemoteUpstream: RepositoryBackendTrait {
fn base_url(&self) -> &str;
fn auth(&self) -> &UpstreamAuth;
fn exists(&self, id: &ArtifactId) -> anyhow::Result<bool> {
Ok(self.fetch(id)?.is_some())
}
fn metadata(&self, id: &ArtifactId) -> anyhow::Result<Option<ArtifactMeta>>;
}
pub fn upstream_path(fmt: ArtifactFormat, id: &ArtifactId) -> String {
match fmt {
ArtifactFormat::Rust => format!("{n}/{n}-{v}.crate", n = id.name, v = id.version),
ArtifactFormat::Maven3 => {
let g = id.namespace.as_deref().unwrap_or("").replace('.', "/");
format!("{g}/{a}/{v}/{a}-{v}.jar", a = id.name, v = id.version)
}
ArtifactFormat::Pip => {
format!("packages/source/{n}/{n}-{v}.tar.gz", n = id.name, v = id.version)
}
ArtifactFormat::Npm => format!("{n}/-/{n}-{v}.tgz", n = id.name, v = id.version),
ArtifactFormat::Nuget => {
let n = id.name.to_lowercase();
format!("{n}/{v}/{n}.{v}.nupkg", n = n, v = id.version)
}
ArtifactFormat::Gem => format!("gems/{n}-{v}.gem", n = id.name, v = id.version),
_ => format!("{}/{}", id.name, id.version),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoteRepository {
pub name: String,
pub format: String,
pub repo_type: String,
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoteAsset {
pub path: String,
pub download_url: String,
pub content_type: Option<String>,
pub size: Option<u64>,
}
#[async_trait]
pub trait ConnectorTrait: Send + Sync {
fn name(&self) -> &str;
async fn list_repositories(&self) -> anyhow::Result<Vec<RemoteRepository>>;
async fn list_assets(&self, repository: &str) -> anyhow::Result<Vec<RemoteAsset>>;
async fn download_asset(&self, asset: &RemoteAsset) -> anyhow::Result<Vec<u8>>;
async fn upload_asset(
&self,
repository: &str,
path: &str,
data: &[u8],
) -> anyhow::Result<()>;
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RepositoryInfo {
pub name: String,
pub repo_type: String,
pub writable: bool,
pub has_archive: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ArtifactEntry {
pub id: ArtifactId,
pub size_bytes: i64,
pub content_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Health {
pub status: String,
pub version: String,
pub uptime_seconds: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ServerProfile {
pub profile: String,
pub read_only: bool,
pub label: String,
pub writable_repo_count: i32,
}
impl Default for ServerProfile {
fn default() -> Self {
Self {
profile: "dynamic".into(),
read_only: false,
label: "DYNAMIC / WRITABLE".into(),
writable_repo_count: 0,
}
}
}
#[async_trait]
pub trait HolgerObject: Send + Sync {
async fn fetch(&self, repository: &str, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>>;
async fn put(&self, repository: &str, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()>;
async fn list_repositories(&self) -> anyhow::Result<Vec<RepositoryInfo>>;
async fn list_artifacts(
&self,
repository: &str,
name_filter: Option<String>,
limit: u32,
page_token: Option<String>,
) -> anyhow::Result<(Vec<ArtifactEntry>, String)> {
let _ = (repository, name_filter, limit, page_token);
Ok((Vec::new(), String::new()))
}
async fn list_archive_files(
&self,
repository: &str,
prefix: Option<String>,
) -> anyhow::Result<Vec<String>> {
let _ = (repository, prefix);
Ok(Vec::new())
}
async fn archive_info(&self, repository: &str) -> anyhow::Result<ArchiveInfo> {
let _ = repository;
Ok(ArchiveInfo::default())
}
async fn health(&self) -> anyhow::Result<Health>;
async fn server_profile(&self) -> anyhow::Result<ServerProfile> {
Ok(ServerProfile::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "testmatrix")]
fn fstatus(component: &str, check: &str, ok: bool, detail: &str) {
nornir_testmatrix::functional_status(component, check, ok, detail);
}
#[test]
fn znippy_type_ids_match_znippy_skeletons() {
assert_eq!(ArtifactFormat::Go.znippy_type_id(), 4);
assert_eq!(ArtifactFormat::Nuget.znippy_type_id(), 5);
assert_eq!(ArtifactFormat::Npm.znippy_type_id(), 6);
assert_eq!(ArtifactFormat::Rpm.znippy_type_id(), 8);
assert_eq!(ArtifactFormat::Deb.znippy_type_id(), 9);
assert_eq!(ArtifactFormat::Gem.znippy_type_id(), 11);
assert_eq!(ArtifactFormat::Docker.znippy_type_id(), 12);
assert_eq!(ArtifactFormat::Helm.znippy_type_id(), 13);
assert_eq!(ArtifactFormat::Conda.znippy_type_id(), 14);
assert_eq!(ArtifactFormat::Composer.znippy_type_id(), 17);
assert_eq!(ArtifactFormat::Rust.znippy_type_id(), 1);
assert_eq!(ArtifactFormat::Pip.znippy_type_id(), 2);
assert_eq!(ArtifactFormat::Maven3.znippy_type_id(), 3);
#[cfg(feature = "testmatrix")]
{
let ok = ArtifactFormat::Helm.znippy_type_id() == 13
&& ArtifactFormat::Rust.znippy_type_id() == 1
&& ArtifactFormat::Composer.znippy_type_id() == 17;
fstatus(
"traits",
"znippy_type_ids_match",
ok,
&format!(
"Rust=1 Pip=2 Maven3=3 Helm=13 Composer=17 (helm={})",
ArtifactFormat::Helm.znippy_type_id()
),
);
}
}
#[test]
fn znippy_type_ids_derive_from_znippy_handlers_not_holger_copies() {
use znippy_common::plugin::ArchiveTypePlugin;
use znippy_common::plugins::{
cargo_native::CargoPlugin, conda_native::CondaPlugin, deb_native::DebPlugin,
gem_native::GemPlugin, npm_native::NpmPlugin, rpm_native::RpmPlugin, skeletons,
};
let pairs: &[(ArtifactFormat, i8)] = &[
(ArtifactFormat::Rust, CargoPlugin::new().type_id()),
(ArtifactFormat::Npm, NpmPlugin.type_id()),
(ArtifactFormat::Gem, GemPlugin.type_id()),
(ArtifactFormat::Conda, CondaPlugin.type_id()),
(ArtifactFormat::Rpm, RpmPlugin.type_id()),
(ArtifactFormat::Deb, DebPlugin.type_id()),
(ArtifactFormat::Go, skeletons::GoPlugin.type_id()),
(ArtifactFormat::Nuget, skeletons::NugetPlugin.type_id()),
(ArtifactFormat::Docker, skeletons::DockerPlugin.type_id()),
(ArtifactFormat::Helm, skeletons::HelmPlugin.type_id()),
(ArtifactFormat::Composer, skeletons::ComposerPlugin.type_id()),
];
let mut all_ok = true;
for (fmt, znippy_id) in pairs {
let holger_id = fmt.znippy_type_id();
if holger_id != *znippy_id {
all_ok = false;
}
assert_eq!(
holger_id, *znippy_id,
"{fmt:?}: holger znippy_type_id()={holger_id} drifted from znippy handler type_id()={znippy_id}"
);
}
#[cfg(feature = "testmatrix")]
fstatus(
"traits",
"znippy_type_ids_derive_from_handlers",
all_ok,
&format!("{} formats derive their discriminant from znippy handlers", pairs.len()),
);
let _ = all_ok;
}
#[test]
fn upstream_path_maps_per_ecosystem_layout() {
let id = ArtifactId { namespace: None, name: "serde".into(), version: "1.0.0".into() };
assert_eq!(upstream_path(ArtifactFormat::Rust, &id), "serde/serde-1.0.0.crate");
assert_eq!(upstream_path(ArtifactFormat::Npm, &id), "serde/-/serde-1.0.0.tgz");
assert_eq!(upstream_path(ArtifactFormat::Pip, &id), "packages/source/serde/serde-1.0.0.tar.gz");
let mvn = ArtifactId {
namespace: Some("com.example".into()),
name: "lib".into(),
version: "2.1".into(),
};
assert_eq!(upstream_path(ArtifactFormat::Maven3, &mvn), "com/example/lib/2.1/lib-2.1.jar");
}
#[test]
fn upstream_auth_defaults_to_none() {
assert_eq!(UpstreamAuth::default(), UpstreamAuth::None);
assert_ne!(
UpstreamAuth::Bearer { token: "t".into() },
UpstreamAuth::Basic { username: "u".into(), password: "p".into() }
);
}
#[test]
fn format_str_roundtrips_aliases() {
assert_eq!(ArtifactFormat::from_format_str("cargo"), Some(ArtifactFormat::Rust));
assert_eq!(ArtifactFormat::from_format_str("golang"), Some(ArtifactFormat::Go));
assert_eq!(ArtifactFormat::from_format_str("dotnet"), Some(ArtifactFormat::Nuget));
assert_eq!(ArtifactFormat::from_format_str("oci"), Some(ArtifactFormat::Docker));
assert_eq!(ArtifactFormat::from_format_str("nope"), None);
#[cfg(feature = "testmatrix")]
{
let ok = ArtifactFormat::from_format_str("cargo") == Some(ArtifactFormat::Rust)
&& ArtifactFormat::from_format_str("oci") == Some(ArtifactFormat::Docker)
&& ArtifactFormat::from_format_str("nope").is_none();
fstatus(
"traits",
"format_str_aliases_roundtrip",
ok,
"cargo->Rust golang->Go dotnet->Nuget oci->Docker nope->None",
);
}
}
}