Introduce foundational structure for pkg6depotd

- Added initial implementation of the `pkg6depotd` server with modular components for CLI parsing, configuration management, HTTP handling, repository access, and daemonization.
- Implemented basic server startup logic with a default router and placeholder handlers.
- Integrated telemetry initialization and configuration fallback mechanism for ease of development.
- Updated `Cargo.toml` and `Cargo.lock` to include dependencies necessary for server functionality.
This commit is contained in:
Till Wegmueller 2025-12-08 20:11:05 +01:00
parent 340e58ca09
commit f2a3bc4d7c
No known key found for this signature in database
16 changed files with 1641 additions and 501 deletions

1788
Cargo.lock generated

File diff suppressed because it is too large Load diff

14
pkg6depotd.kdl Normal file
View file

@ -0,0 +1,14 @@
server {
bind "0.0.0.0:8080"
workers 4
}
repository {
root "/tmp/pkg_repo"
mode "readonly"
}
telemetry {
service-name "pkg6depotd"
log-format "json"
}

View file

@ -9,6 +9,45 @@ repository.workspace = true
readme.workspace = true
keywords.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# Async Runtime & Web Framework
tokio = { version = "1.47", features = ["full"] }
axum = { version = "0.8", features = ["macros"] }
hyper = { version = "1", features = ["full"] }
tower = { version = "0.5", features = ["util", "timeout", "limit", "load-shed"] }
tower-http = { version = "0.6", features = ["trace", "fs", "cors", "compression-full", "timeout", "request-id", "util"] }
rustls = "0.23"
tokio-rustls = "0.26"
axum-server = { version = "0.8", features = ["tls-rustls"] } # Simplifies TLS with Axum
socket2 = "0.6"
bytes = "1"
http-body-util = "0.1"
# CLI & Config
clap = { version = "4.5", features = ["derive", "env"] }
knuffel = "3.2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dirs = "6"
nix = { version = "0.30", features = ["signal", "process", "user", "fs"] }
# Telemetry
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
opentelemetry = "0.31"
opentelemetry_sdk = { version = "0.31", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.31", features = ["grpc-tonic"] } # Check compatibility with otel 0.22
tracing-opentelemetry = "0.32"
# Error Handling
thiserror = "2"
miette = { version = "7.6.0", features = ["fancy"] }
# Project Dependencies
libips = { path = "../libips" }
[dev-dependencies]
reqwest = { version = "0.12", features = ["blocking", "json"] }
assert_cmd = "2"
predicates = "3"
tempfile = "3"

45
pkg6depotd/src/cli.rs Normal file
View file

@ -0,0 +1,45 @@
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "pkg6depotd")]
#[command(about = "IPS Package Depot Server", long_about = None)]
pub struct Cli {
#[arg(short, long, value_name = "FILE")]
pub config: Option<PathBuf>,
#[arg(long)]
pub no_daemon: bool,
#[arg(long, value_name = "FILE")]
pub pid_file: Option<PathBuf>,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand)]
pub enum Commands {
/// Start the server (default)
Start,
/// Stop the running server
Stop,
/// Check server status
Status,
/// Reload configuration
Reload,
/// Test configuration
ConfigTest,
/// Check health
Health,
/// Admin commands
Admin {
#[command(subcommand)]
cmd: AdminCommands,
},
}
#[derive(Subcommand)]
pub enum AdminCommands {
AuthCheck,
}

87
pkg6depotd/src/config.rs Normal file
View file

@ -0,0 +1,87 @@
use std::path::PathBuf;
use crate::errors::DepotError;
use std::fs;
#[derive(Debug, knuffel::Decode, Clone)]
pub struct Config {
#[knuffel(child)]
pub server: ServerConfig,
#[knuffel(child)]
pub repository: RepositoryConfig,
#[knuffel(child)]
pub telemetry: Option<TelemetryConfig>,
#[knuffel(child)]
pub publishers: Option<PublishersConfig>,
#[knuffel(child)]
pub admin: Option<AdminConfig>,
#[knuffel(child)]
pub oauth2: Option<Oauth2Config>,
}
#[derive(Debug, knuffel::Decode, Clone)]
pub struct ServerConfig {
#[knuffel(child, unwrap(arguments))]
pub bind: Vec<String>,
#[knuffel(child, unwrap(argument))]
pub workers: Option<usize>,
#[knuffel(child, unwrap(argument))]
pub max_connections: Option<usize>,
#[knuffel(child, unwrap(argument))]
pub reuseport: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub tls_cert: Option<PathBuf>,
#[knuffel(child, unwrap(argument))]
pub tls_key: Option<PathBuf>,
}
#[derive(Debug, knuffel::Decode, Clone)]
pub struct RepositoryConfig {
#[knuffel(child, unwrap(argument))]
pub root: PathBuf,
#[knuffel(child, unwrap(argument))]
pub mode: Option<String>,
}
#[derive(Debug, knuffel::Decode, Clone)]
pub struct TelemetryConfig {
#[knuffel(child, unwrap(argument))]
pub otlp_endpoint: Option<String>,
#[knuffel(child, unwrap(argument))]
pub service_name: Option<String>,
#[knuffel(child, unwrap(argument))]
pub log_format: Option<String>,
}
#[derive(Debug, knuffel::Decode, Clone)]
pub struct PublishersConfig {
#[knuffel(child, unwrap(arguments))]
pub list: Vec<String>,
}
#[derive(Debug, knuffel::Decode, Clone)]
pub struct AdminConfig {
#[knuffel(child, unwrap(argument))]
pub unix_socket: Option<PathBuf>,
}
#[derive(Debug, knuffel::Decode, Clone)]
pub struct Oauth2Config {
#[knuffel(child, unwrap(argument))]
pub issuer: Option<String>,
#[knuffel(child, unwrap(argument))]
pub jwks_uri: Option<String>,
#[knuffel(child, unwrap(arguments))]
pub required_scopes: Option<Vec<String>>,
}
impl Config {
pub fn load(path: Option<PathBuf>) -> crate::errors::Result<Self> {
let path = path.unwrap_or_else(|| PathBuf::from("pkg6depotd.kdl"));
let content = fs::read_to_string(&path)
.map_err(|e| DepotError::Config(format!("Failed to read config file {:?}: {}", path, e)))?;
knuffel::parse(path.to_str().unwrap_or("pkg6depotd.kdl"), &content)
.map_err(|e| DepotError::Config(format!("Failed to parse config: {:?}", e)))
}
}

View file

@ -0,0 +1,5 @@
// Placeholder for daemonization logic
pub fn daemonize() -> crate::errors::Result<()> {
// TODO: Implement double fork using nix
Ok(())
}

27
pkg6depotd/src/errors.rs Normal file
View file

@ -0,0 +1,27 @@
use miette::Diagnostic;
use thiserror::Error;
#[derive(Error, Debug, Diagnostic)]
pub enum DepotError {
#[error("Configuration error: {0}")]
#[diagnostic(code(ips::depot_error::config))]
Config(String),
#[error("IO error: {0}")]
#[diagnostic(code(ips::depot_error::io))]
Io(#[from] std::io::Error),
#[error("Address parse error: {0}")]
#[diagnostic(code(ips::depot_error::addr_parse))]
AddrParse(#[from] std::net::AddrParseError),
#[error("Server error: {0}")]
#[diagnostic(code(ips::depot_error::server))]
Server(String),
#[error("Repository error: {0}")]
#[diagnostic(code(ips::depot_error::repo))]
Repo(#[from] libips::repository::RepositoryError),
}
pub type Result<T> = std::result::Result<T, DepotError>;

View file

@ -0,0 +1 @@
pub mod versions;

View file

@ -0,0 +1,15 @@
use axum::response::IntoResponse;
pub async fn get_versions() -> impl IntoResponse {
// According to pkg5 depot docs: text/plain list of supported ops and versions.
// "pkg-server <version>\ninfo 0\n..."
let version_str = "pkg-server pkg6depotd-0.1\n\
info 0\n\
search 0\n\
versions 0\n\
catalog 0\n\
manifest 0\n\
file 0\n";
version_str.to_string()
}

View file

@ -0,0 +1 @@
// Placeholder for middleware

View file

@ -0,0 +1,4 @@
pub mod server;
pub mod routes;
pub mod handlers;
pub mod middleware;

View file

@ -0,0 +1,10 @@
use axum::{
routing::get,
Router,
};
use crate::http::handlers::versions;
pub fn app_router() -> Router {
Router::new()
.route("/versions/0/", get(versions::get_versions))
}

View file

@ -0,0 +1,12 @@
use tokio::net::TcpListener;
use axum::Router;
use std::net::SocketAddr;
use crate::errors::Result;
pub async fn run(router: Router, bind_addr: &str) -> Result<()> {
let addr: SocketAddr = bind_addr.parse()?;
let listener = TcpListener::bind(addr).await?;
tracing::info!("Listening on {}", addr);
axum::serve(listener, router).await.map_err(|e| crate::errors::DepotError::Server(e.to_string()))
}

View file

@ -1,3 +1,72 @@
fn main() {
println!("Hello, world!");
mod cli;
mod config;
mod errors;
mod http;
mod telemetry;
mod repo;
mod daemon;
use clap::Parser;
use cli::{Cli, Commands};
use config::Config;
use miette::Result;
#[tokio::main]
async fn main() -> Result<()> {
let args = Cli::parse();
// Load config
// For M1, let's just create a dummy default if not found/failed for testing purposes
// In a real scenario we'd want to be more specific about errors.
let config = match Config::load(args.config.clone()) {
Ok(c) => c,
Err(e) => {
eprintln!("Failed to load config: {}. Using default.", e);
Config {
server: config::ServerConfig {
bind: vec!["0.0.0.0:8080".to_string()],
workers: None,
max_connections: None,
reuseport: None,
tls_cert: None,
tls_key: None,
},
repository: config::RepositoryConfig {
root: std::path::PathBuf::from("/tmp/pkg_repo"),
mode: Some("readonly".to_string()),
},
telemetry: None,
publishers: None,
admin: None,
oauth2: None,
}
}
};
// Init telemetry
telemetry::init(&config);
match args.command.unwrap_or(Commands::Start) {
Commands::Start => {
if !args.no_daemon {
daemon::daemonize().map_err(|e| miette::miette!(e))?;
}
let router = http::routes::app_router();
let bind = config.server.bind.first().cloned().unwrap_or_else(|| "0.0.0.0:8080".to_string());
tracing::info!("Starting pkg6depotd on {}", bind);
http::server::run(router, &bind).await.map_err(|e| miette::miette!(e))?;
}
Commands::ConfigTest => {
println!("Configuration loaded successfully: {:?}", config);
}
_ => {
println!("Command not yet implemented");
}
}
Ok(())
}

2
pkg6depotd/src/repo.rs Normal file
View file

@ -0,0 +1,2 @@
// Placeholder for repository access helpers
// Will adapt libips types for the server

View file

@ -0,0 +1,15 @@
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use crate::config::Config;
pub fn init(_config: &Config) {
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,pkg6depotd=debug"));
let registry = tracing_subscriber::registry()
.with(env_filter)
.with(tracing_subscriber::fmt::layer());
// TODO: Add OTLP layer if configured in _config
registry.init();
}