diff options
author | Toby Vincent <tobyv13@gmail.com> | 2022-03-17 18:19:50 -0500 |
---|---|---|
committer | Toby Vincent <tobyv13@gmail.com> | 2022-03-17 18:19:50 -0500 |
commit | e18482558a43330cab726ae92518904be346a248 (patch) | |
tree | 0ec733e16c2511734a155b5d59c6bd5ddba00aa1 /zoned | |
parent | 8858295736b507825f279f51303ab6e9f939f423 (diff) |
refactor: migrated from rocket to axum
Diffstat (limited to 'zoned')
-rw-r--r-- | zoned/Cargo.toml | 9 | ||||
-rw-r--r-- | zoned/src/api.rs | 109 | ||||
-rw-r--r-- | zoned/src/config.rs | 113 | ||||
-rw-r--r-- | zoned/src/error.rs | 56 | ||||
-rw-r--r-- | zoned/src/lib.rs | 8 | ||||
-rw-r--r-- | zoned/src/main.rs | 39 | ||||
-rw-r--r-- | zoned/src/state.rs | 20 |
7 files changed, 238 insertions, 116 deletions
diff --git a/zoned/Cargo.toml b/zoned/Cargo.toml index deb76fa..408a4d4 100644 --- a/zoned/Cargo.toml +++ b/zoned/Cargo.toml @@ -13,13 +13,14 @@ description = "daemon for managing containers using systemd-nspawn and ZFS" [dependencies] anyhow = "1.0.53" -figment = "0.10.6" -lazy_static = "1.4.0" -rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] } -rocket_okapi = { version = "0.8.0-rc.1", features = ["rapidoc", "swagger"] } +axum = { version = "0.4.6", features = ["json"] } +figment = { version = "0.10.6", features = ["toml", "env", "test"] } serde = "1.0.136" +serde_json = "1.0.79" thiserror = "1.0.30" +tokio = { version = "1.17.0", features = ["full"] } tracing = "0.1.31" +tracing-subscriber = "0.3.9" zone_core = { version = "0.1.0", path = "../zone_core" } zone_nspawn = { version = "0.1.0", path = "../zone_nspawn" } zone_zfs = { version = "0.1.0", path = "../zone_zfs", features = ["chrono"] } diff --git a/zoned/src/api.rs b/zoned/src/api.rs index 37f2bef..8dc2c39 100644 --- a/zoned/src/api.rs +++ b/zoned/src/api.rs @@ -1,84 +1,61 @@ -use crate::{Config, Error, Result}; -use rocket::{get, post, serde::json::Json, Build, Rocket, State}; -use rocket_okapi::{ - openapi, openapi_get_routes, - rapidoc::{make_rapidoc, GeneralConfig, HideShowConfig, RapiDocConfig}, - settings::UrlObject, - swagger_ui::{make_swagger_ui, SwaggerUIConfig}, +use axum::{ + extract::{Extension, Query}, + routing::{get, post}, + Json, Router, }; -use zone_core::Container; +use std::sync::Arc; +use tracing::warn; +use zone_core::{Container, ContainerOptions, FilterContainer}; use zone_nspawn::NSpawn; +use crate::{Error, Result, State}; + +pub fn build_routes() -> Router { + Router::new() + .route("/test", get(test_endpoint)) + .route("/container", post(clone_container)) + .route("/container/list?<container..>", get(container_list)) +} + /// # Test endpoint /// /// Returns a list of containers based on the query. -#[openapi(tag = "Testing")] -#[get("/test")] -pub fn test_endpoint(zfs: &State<zone_zfs::ZFS>) -> Json<String> { - Json(zfs.config.pool_name.to_owned()) +async fn test_endpoint(Extension(state): Extension<Arc<State>>) -> Json<String> { + Json(state.zfs.config.pool_name.to_owned()) } /// List containers /// /// Returns a list of containers based on the query. -#[openapi(tag = "Container")] -#[get("/container/list?<container..>")] -pub fn container_list(container: Container) -> Result<Json<Vec<Container>>> { - NSpawn::get_containers() - .map(|v| container.filter_from(v)) - .map(Json::from) - .map_err(Error::from) +async fn container_list( + container: Option<Query<ContainerOptions>>, +) -> Result<Json<Vec<Container>>> { + let mut containers = NSpawn::get_containers()?.into_iter().filter_map(|c| { + Container::try_from(c) + .map_err(|err| warn!("Ignoring invalid nspawn container {:?}", err)) + .ok() + }); + + match container { + Some(Query(params)) => Ok(containers.filter_container(params).into()), + _ => Ok(containers.collect::<Vec<_>>().into()), + } } /// Create container /// /// Creates a new container volume from the provided container json data -#[openapi(tag = "Container")] -#[post("/container", data = "<container>")] -fn clone_container( - container: Json<Container>, - zfs: &State<zone_zfs::ZFS>, +async fn clone_container( + Json(container): Json<Container>, + Extension(state): Extension<Arc<State>>, ) -> Result<Json<Container>> { - zfs.clone_from_latest( - format!("{}-{}", container.user, container.id).into(), - container.template.to_owned().into(), - )? - .try_into() - .map_err(Error::from) - .map(Container::into) -} - -pub fn build_rocket(config: &Config) -> Result<Rocket<Build>> { - let nspawn = zone_nspawn::NSpawn::custom(&config.nspawn_config).map_err(Error::from)?; - let zfs = zone_zfs::ZFS::custom(&config.zfs_config).map_err(Error::from)?; - - Ok(rocket::custom(&config.rocket_config) - .mount( - "/", - openapi_get_routes![test_endpoint, container_list, clone_container,], - ) - .mount( - "/swagger-ui/", - make_swagger_ui(&SwaggerUIConfig { - url: "../openapi.json".to_owned(), - ..Default::default() - }), - ) - .mount( - "/rapidoc/", - make_rapidoc(&RapiDocConfig { - general: GeneralConfig { - spec_urls: vec![UrlObject::new("General", "../openapi.json")], - ..Default::default() - }, - hide_show: HideShowConfig { - allow_spec_url_load: false, - allow_spec_file_load: false, - ..Default::default() - }, - ..Default::default() - }), - ) - .manage(nspawn) - .manage(zfs)) + state + .zfs + .clone_from_latest( + format!("{}-{}", container.user, container.id).into(), + container.template.into(), + )? + .try_into() + .map_err(Error::from) + .map(Container::into) } diff --git a/zoned/src/config.rs b/zoned/src/config.rs index c9cd5f3..f6622f1 100644 --- a/zoned/src/config.rs +++ b/zoned/src/config.rs @@ -1,10 +1,115 @@ +use figment::Figment; use serde::{Deserialize, Serialize}; +use std::net::{IpAddr, SocketAddr}; -#[derive(Default, Serialize, Deserialize)] +use crate::{Error, Result}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Config { - pub(crate) rocket_config: rocket::Config, + pub ip_address: IpAddr, + pub port: u16, + pub zfs: zone_zfs::Config, + pub nspawn: zone_nspawn::Config, +} + +impl Default for Config { + fn default() -> Self { + Self { + ip_address: zone_core::DEFAULT_IP_ADDRESS, + port: zone_core::DEFAULT_PORT, + zfs: Default::default(), + nspawn: Default::default(), + } + } +} + +impl TryFrom<Figment> for Config { + type Error = Error; + + fn try_from(value: Figment) -> Result<Self> { + value.extract().map_err(Into::into) + } +} + +impl From<Config> for SocketAddr { + fn from(val: Config) -> Self { + SocketAddr::from((val.ip_address, val.port)) + } +} +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use figment::providers::{Format, Serialized, Toml}; + + use super::*; + + #[test] + fn defaults() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "Config.toml", + r#" + ip_address = "127.0.0.1" + port = 8000 + + [zfs] + quota = "16G" + pool_name = "pool" + mountpoint = "/srv" + "#, + )?; + + let config: Config = Figment::from(Serialized::defaults(Config::default())) + .merge(Toml::file("Config.toml")) + .extract()?; + + assert_eq!(config, Config::default()); + + Ok(()) + }); + } + + #[test] + fn custom() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "Config.toml", + r#" + ip_address = "192.168.1.1" + port = 6555 + + [zfs] + quota = 42000000 + pool_name = "fool" + mountpoint = "/mnt" + + [nspawn] + network_configs_path = "/etc/zoned/network.d" + "#, + )?; + + let config: Config = Figment::from(Serialized::defaults(Config::default())) + .merge(Toml::file("Config.toml")) + .extract()?; - pub(crate) zfs_config: zone_zfs::Config, + assert_eq!( + config, + Config { + ip_address: [192, 168, 1, 1].into(), + port: 6555, + zfs: zone_zfs::Config { + quota: 42_000_000u64.into(), + pool_name: String::from("fool"), + mountpoint: PathBuf::from("/mnt"), + }, + nspawn: zone_nspawn::Config { + network_configs_path: Some(PathBuf::from("/etc/zoned/network.d")), + }, + } + ); - pub(crate) nspawn_config: zone_nspawn::Config, + Ok(()) + }); + } } diff --git a/zoned/src/error.rs b/zoned/src/error.rs index 14b30db..98fbf2f 100644 --- a/zoned/src/error.rs +++ b/zoned/src/error.rs @@ -1,52 +1,54 @@ -use rocket::{ - http::Status, - response::{self, Responder}, - Request, -}; -use rocket_okapi::{ - gen::OpenApiGenerator, okapi::openapi3::Responses, response::OpenApiResponderInner, - util::ensure_status_code_exists, +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, }; +use serde_json::json; use thiserror::Error; -use zone_core::Container; pub type Result<T> = std::result::Result<T, Error>; #[derive(Error, Debug)] pub enum Error { - #[error("Container Error {0:?}")] - Container(Container), + #[error("Container Error: {0:?}")] + Container(String), - #[error("ZFS Error {source:?}")] + #[error("ZFS Error: {source:?}")] ZFS { #[from] source: zone_zfs::Error, }, - #[error("NSpawn Error {source:?}")] + #[error("NSpawn Error: {source:?}")] Nspawn { #[from] source: zone_nspawn::Error, }, + #[error("Config Error: {source:?}")] + Config { + #[from] + source: figment::Error, + }, + + #[error("Container not found")] + NotFound, + #[error(transparent)] Other(#[from] anyhow::Error), } -impl<'r, 'o: 'r> Responder<'r, 'o> for Error { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { - // https://stuarth.github.io/rocket-error-handling/ - // match self { - // _ => Status::InternalServerError.respond_to(req), - // } - Status::InternalServerError.respond_to(req) - } -} +impl IntoResponse for Error { + fn into_response(self) -> Response { + let (status, error_message) = match self { + Error::Container(source) => (StatusCode::NOT_FOUND, source), + err => (StatusCode::UNPROCESSABLE_ENTITY, format!("{}", err)), + }; + + let body = Json(json!({ + "error": error_message, + })); -impl OpenApiResponderInner for Error { - fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> { - let mut responses = Responses::default(); - ensure_status_code_exists(&mut responses, 500); - Ok(responses) + (status, body).into_response() } } diff --git a/zoned/src/lib.rs b/zoned/src/lib.rs index f6c3926..944a264 100644 --- a/zoned/src/lib.rs +++ b/zoned/src/lib.rs @@ -1,7 +1,9 @@ -pub use api::build_rocket; -pub use config::Config; -pub use error::{Error, Result}; +pub use crate::api::build_routes; +pub use crate::config::Config; +pub use crate::error::{Error, Result}; +pub use crate::state::State; mod api; mod config; mod error; +mod state; diff --git a/zoned/src/main.rs b/zoned/src/main.rs index 7a39ea5..caff470 100644 --- a/zoned/src/main.rs +++ b/zoned/src/main.rs @@ -1,27 +1,42 @@ +use axum::AddExtensionLayer; use figment::{ providers::{Env, Format, Serialized, Toml}, Figment, }; -use zoned::Config; +use std::{net::SocketAddr, sync::Arc}; +use tracing::{debug, error}; +use zoned::{build_routes, Config, State}; -#[rocket::main] +#[tokio::main] async fn main() { - let config: Config = Figment::from(Serialized::defaults(Config::default())) + tracing_subscriber::fmt::init(); + + let figment = Figment::from(Serialized::defaults(Config::default())) .merge(Toml::file(Env::var_or("ZONED_CONFIG", "/etc/zoned/Config.toml")).nested()) - .merge(Env::prefixed("ZONED_")) - .extract() - .expect("Failed to parse config"); + .merge(Env::prefixed("ZONED_")); - let rocket = match zoned::build_rocket(&config) { - Ok(rocket) => rocket, + let config = match Config::try_from(figment) { + Ok(config) => config, Err(err) => { - eprintln!("{}", err); + error!("{}", err); std::process::exit(1) } }; - match rocket.launch().await { - Ok(()) => println!("Rocket shut down gracefully."), - Err(err) => eprintln!("Rocket had an error: {}", err), + let shared_state = match State::try_from(config) { + Ok(state) => Arc::new(state), + Err(err) => { + error!("{}", err); + std::process::exit(1) + } }; + + let routes = build_routes().layer(AddExtensionLayer::new(shared_state)); + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + + debug!("listening on {}", addr); + axum::Server::bind(&addr) + .serve(routes.into_make_service()) + .await + .unwrap(); } diff --git a/zoned/src/state.rs b/zoned/src/state.rs new file mode 100644 index 0000000..68ab0a6 --- /dev/null +++ b/zoned/src/state.rs @@ -0,0 +1,20 @@ +use zone_nspawn::NSpawn; +use zone_zfs::ZFS; + +use crate::{Config, Error, Result}; + +pub struct State { + pub zfs: ZFS, + pub nspawn: NSpawn, +} + +impl TryFrom<Config> for State { + type Error = Error; + + fn try_from(config: Config) -> Result<Self> { + Ok(State { + zfs: zone_zfs::ZFS::custom(&config.zfs)?, + nspawn: zone_nspawn::NSpawn::custom(&config.nspawn)?, + }) + } +} |