mirror of
https://codeberg.org/Toasterson/ips.git
synced 2026-04-10 05:10:42 +00:00
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:
parent
340e58ca09
commit
f2a3bc4d7c
16 changed files with 1641 additions and 501 deletions
1788
Cargo.lock
generated
1788
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
pkg6depotd.kdl
Normal file
14
pkg6depotd.kdl
Normal 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"
|
||||
}
|
||||
|
|
@ -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
45
pkg6depotd/src/cli.rs
Normal 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
87
pkg6depotd/src/config.rs
Normal 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)))
|
||||
}
|
||||
}
|
||||
5
pkg6depotd/src/daemon/mod.rs
Normal file
5
pkg6depotd/src/daemon/mod.rs
Normal 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
27
pkg6depotd/src/errors.rs
Normal 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>;
|
||||
1
pkg6depotd/src/http/handlers/mod.rs
Normal file
1
pkg6depotd/src/http/handlers/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod versions;
|
||||
15
pkg6depotd/src/http/handlers/versions.rs
Normal file
15
pkg6depotd/src/http/handlers/versions.rs
Normal 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()
|
||||
}
|
||||
1
pkg6depotd/src/http/middleware/mod.rs
Normal file
1
pkg6depotd/src/http/middleware/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
// Placeholder for middleware
|
||||
4
pkg6depotd/src/http/mod.rs
Normal file
4
pkg6depotd/src/http/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod server;
|
||||
pub mod routes;
|
||||
pub mod handlers;
|
||||
pub mod middleware;
|
||||
10
pkg6depotd/src/http/routes.rs
Normal file
10
pkg6depotd/src/http/routes.rs
Normal 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))
|
||||
}
|
||||
12
pkg6depotd/src/http/server.rs
Normal file
12
pkg6depotd/src/http/server.rs
Normal 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()))
|
||||
}
|
||||
|
|
@ -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
2
pkg6depotd/src/repo.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Placeholder for repository access helpers
|
||||
// Will adapt libips types for the server
|
||||
15
pkg6depotd/src/telemetry/mod.rs
Normal file
15
pkg6depotd/src/telemetry/mod.rs
Normal 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();
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue