feat: initialize NORA artifact registry

Cloud-native multi-protocol artifact registry in Rust.

- Docker Registry v2
- Maven (+ proxy)
- npm (+ proxy)
- Cargo, PyPI
- Web UI, Swagger, Prometheus
- Local & S3 storage
- 32MB Docker image

Created by DevITWay
https://getnora.io
This commit is contained in:
2026-01-25 17:03:18 +00:00
commit 586420a476
36 changed files with 7613 additions and 0 deletions

267
nora-registry/src/main.rs Normal file
View File

@@ -0,0 +1,267 @@
mod auth;
mod backup;
mod config;
mod health;
mod metrics;
mod openapi;
mod registry;
mod storage;
mod tokens;
mod ui;
use axum::{middleware, Router};
use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;
use tokio::signal;
use tracing::{error, info, warn};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use auth::HtpasswdAuth;
use config::{Config, StorageMode};
pub use storage::Storage;
use tokens::TokenStore;
#[derive(Parser)]
#[command(
name = "nora",
version,
about = "Multi-protocol artifact registry"
)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
/// Start the registry server (default)
Serve,
/// Backup all artifacts to a tar.gz file
Backup {
/// Output file path (e.g., backup.tar.gz)
#[arg(short, long)]
output: PathBuf,
},
/// Restore artifacts from a backup file
Restore {
/// Input backup file path
#[arg(short, long)]
input: PathBuf,
},
/// Migrate artifacts between storage backends
Migrate {
/// Source storage: local or s3
#[arg(long)]
from: String,
/// Destination storage: local or s3
#[arg(long)]
to: String,
/// Dry run - show what would be migrated without copying
#[arg(long, default_value = "false")]
dry_run: bool,
},
}
pub struct AppState {
pub storage: Storage,
pub config: Config,
pub start_time: Instant,
pub auth: Option<HtpasswdAuth>,
pub tokens: Option<TokenStore>,
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
// Initialize logging (JSON for server, plain for CLI commands)
let is_server = matches!(cli.command, None | Some(Commands::Serve));
init_logging(is_server);
let config = Config::load();
// Initialize storage based on mode
let storage = match config.storage.mode {
StorageMode::Local => {
if is_server {
info!(path = %config.storage.path, "Using local storage");
}
Storage::new_local(&config.storage.path)
}
StorageMode::S3 => {
if is_server {
info!(
s3_url = %config.storage.s3_url,
bucket = %config.storage.bucket,
"Using S3 storage"
);
}
Storage::new_s3(&config.storage.s3_url, &config.storage.bucket)
}
};
// Dispatch to command
match cli.command {
None | Some(Commands::Serve) => {
run_server(config, storage).await;
}
Some(Commands::Backup { output }) => {
if let Err(e) = backup::create_backup(&storage, &output).await {
error!("Backup failed: {}", e);
std::process::exit(1);
}
}
Some(Commands::Restore { input }) => {
if let Err(e) = backup::restore_backup(&storage, &input).await {
error!("Restore failed: {}", e);
std::process::exit(1);
}
}
Some(Commands::Migrate { from, to, dry_run }) => {
eprintln!("Migration from '{}' to '{}' (dry_run: {})", from, to, dry_run);
eprintln!("TODO: Migration not yet implemented");
std::process::exit(1);
}
}
}
fn init_logging(json_format: bool) {
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
if json_format {
tracing_subscriber::registry()
.with(env_filter)
.with(fmt::layer().json().with_target(true))
.init();
} else {
tracing_subscriber::registry()
.with(env_filter)
.with(fmt::layer().with_target(false))
.init();
}
}
async fn run_server(config: Config, storage: Storage) {
let start_time = Instant::now();
// Load auth if enabled
let auth = if config.auth.enabled {
let path = Path::new(&config.auth.htpasswd_file);
match HtpasswdAuth::from_file(path) {
Some(auth) => {
info!(users = auth.list_users().len(), "Auth enabled");
Some(auth)
}
None => {
warn!(file = %config.auth.htpasswd_file, "Auth enabled but htpasswd file not found or empty");
None
}
}
} else {
None
};
// Initialize token store if auth is enabled
let tokens = if config.auth.enabled {
let token_path = Path::new(&config.auth.token_storage);
info!(path = %config.auth.token_storage, "Token storage initialized");
Some(TokenStore::new(token_path))
} else {
None
};
let state = Arc::new(AppState {
storage,
config,
start_time,
auth,
tokens,
});
let app = Router::new()
.merge(health::routes())
.merge(metrics::routes())
.merge(ui::routes())
.merge(openapi::routes())
.merge(auth::token_routes())
.merge(registry::docker_routes())
.merge(registry::maven_routes())
.merge(registry::npm_routes())
.merge(registry::cargo_routes())
.merge(registry::pypi_routes())
.layer(middleware::from_fn(metrics::metrics_middleware))
.layer(middleware::from_fn_with_state(
state.clone(),
auth::auth_middleware,
))
.with_state(state.clone());
let addr = format!("{}:{}", state.config.server.host, state.config.server.port);
let listener = tokio::net::TcpListener::bind(&addr)
.await
.expect("Failed to bind");
info!(
address = %addr,
version = env!("CARGO_PKG_VERSION"),
storage = state.storage.backend_name(),
auth_enabled = state.auth.is_some(),
"Nora started"
);
info!(
health = "/health",
ready = "/ready",
metrics = "/metrics",
ui = "/ui/",
api_docs = "/api-docs",
docker = "/v2/",
maven = "/maven2/",
npm = "/npm/",
cargo = "/cargo/",
pypi = "/simple/",
"Available endpoints"
);
// Graceful shutdown on SIGTERM/SIGINT
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.expect("Server error");
info!(
uptime_seconds = state.start_time.elapsed().as_secs(),
"Nora shutdown complete"
);
}
/// Wait for shutdown signal (SIGTERM or SIGINT)
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("Failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {
info!("Received SIGINT, starting graceful shutdown...");
}
_ = terminate => {
info!("Received SIGTERM, starting graceful shutdown...");
}
}
}