diff --git a/Cargo.lock b/Cargo.lock index e96c944f..3abf601b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -523,6 +523,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "thiserror", "tiny_http", "tokio", "tokio-stream", diff --git a/controller/Cargo.toml b/controller/Cargo.toml index 143a0fa0..e44d71ac 100644 --- a/controller/Cargo.toml +++ b/controller/Cargo.toml @@ -34,6 +34,7 @@ rstest = "0.16.0" uuid = { version = "1.3.1", features = ["serde", "v4"] } backoff = { version = "0.4.0", features = ["tokio"]} rand = "0.8.4" +thiserror = "1.0.40" # Instrumentation tracing = { workspace = true } diff --git a/controller/src/api/external/routes/instance.rs b/controller/src/api/external/routes/instance.rs index 269c7bdd..f886025e 100644 --- a/controller/src/api/external/routes/instance.rs +++ b/controller/src/api/external/routes/instance.rs @@ -1,32 +1,38 @@ use definition::workload::WorkloadDefinition; use route_recognizer; use rusqlite::Connection; -use std::io; use std::str::FromStr; use std::sync::mpsc::Sender; -use tracing::{event, info, Level}; +use tracing::{event, Level}; -use crate::api; use crate::api::external::services::element::elements_set_right_name; use crate::api::external::services::instance::send_create_instance; use crate::api::types::element::OnlyId; use crate::api::types::instance::InstanceDefinition; -use crate::api::{ApiChannel, Crud}; +use crate::api::{ApiChannel, Crud, RikError}; use crate::core::instance::Instance; use crate::database::RikRepository; +use super::HttpResult; + pub fn get( _: &mut tiny_http::Request, _: &route_recognizer::Params, connection: &Connection, _: &Sender, -) -> Result>>, api::RikError> { +) -> HttpResult { if let Ok(mut instances) = RikRepository::find_all(connection, "/instance") { instances = elements_set_right_name(instances.clone()); - let instances_json = serde_json::to_string(&instances).unwrap(); + let instances_json = serde_json::to_string(&instances).map_err(RikError::ParsingError)?; + event!(Level::INFO, "instances.get, instances found"); + Ok(tiny_http::Response::from_string(instances_json) - .with_header(tiny_http::Header::from_str("Content-Type: application/json").unwrap()) + .with_header( + tiny_http::Header::from_str("Content-Type: application/json").map_err(|_| { + RikError::Error("tiny_http::Header failed to add header".to_string()) + })?, + ) .with_status_code(tiny_http::StatusCode::from(200))) } else { Ok(tiny_http::Response::from_string("Cannot find instances") @@ -39,11 +45,14 @@ pub fn create( _: &route_recognizer::Params, connection: &Connection, internal_sender: &Sender, -) -> Result>>, api::RikError> { +) -> HttpResult { let mut content = String::new(); - req.as_reader().read_to_string(&mut content).unwrap(); + req.as_reader() + .read_to_string(&mut content) + .map_err(RikError::IoError)?; - let mut instance: InstanceDefinition = serde_json::from_str(&content)?; + let mut instance: InstanceDefinition = + serde_json::from_str(&content).map_err(RikError::ParsingError)?; //Workload not found if RikRepository::find_one(connection, &instance.workload_id, "/workload").is_err() { @@ -98,11 +107,14 @@ pub fn create( ); } - Ok( - tiny_http::Response::from_string(serde_json::to_string(&instance_names).unwrap()) - .with_header(tiny_http::Header::from_str("Content-Type: application/json").unwrap()) - .with_status_code(tiny_http::StatusCode::from(201)), + Ok(tiny_http::Response::from_string( + serde_json::to_string(&instance_names).map_err(RikError::ParsingError)?, + ) + .with_header( + tiny_http::Header::from_str("Content-Type: application/json") + .map_err(|_| RikError::Error("tiny_http::Header failed to add header".to_string()))?, ) + .with_status_code(tiny_http::StatusCode::from(201))) } pub fn delete( @@ -110,14 +122,17 @@ pub fn delete( _: &route_recognizer::Params, connection: &Connection, internal_sender: &Sender, -) -> Result>>, api::RikError> { +) -> HttpResult { let mut content = String::new(); - req.as_reader().read_to_string(&mut content).unwrap(); - let OnlyId { id: delete_id } = serde_json::from_str(&content)?; + req.as_reader() + .read_to_string(&mut content) + .map_err(RikError::IoError)?; + let OnlyId { id: delete_id } = + serde_json::from_str(&content).map_err(RikError::ParsingError)?; if let Ok(instance) = RikRepository::find_one(connection, &delete_id, "/instance") { let instance_def: InstanceDefinition = - serde_json::from_value(instance.value.clone()).unwrap(); + serde_json::from_value(instance.value.clone()).map_err(RikError::ParsingError)?; let workload_def_rs = RikRepository::find_one(connection, &instance_def.workload_id, "/workload"); @@ -135,7 +150,8 @@ pub fn delete( .with_status_code(tiny_http::StatusCode::from(404))); } let workload_def: WorkloadDefinition = - serde_json::from_value(workload_def_rs.unwrap().value).unwrap(); + serde_json::from_value(workload_def_rs.map_err(RikError::DataBaseError)?.value) + .map_err(RikError::ParsingError)?; internal_sender .send(ApiChannel { action: Crud::Delete, @@ -143,7 +159,7 @@ pub fn delete( workload_definition: Some(workload_def), instance_id: Some(delete_id), }) - .unwrap(); + .map_err(|e| RikError::InternalCommunicationError(e.to_string()))?; event!( Level::INFO, diff --git a/controller/src/api/external/routes/mod.rs b/controller/src/api/external/routes/mod.rs index 9d27ca5b..d1ddf8d1 100644 --- a/controller/src/api/external/routes/mod.rs +++ b/controller/src/api/external/routes/mod.rs @@ -3,6 +3,7 @@ use rusqlite::Connection; use std::io; use std::sync::mpsc::Sender; use tiny_http::Method; +use tiny_http::Response; use tracing::{event, Level}; use crate::api; @@ -19,6 +20,8 @@ type Handler = fn( &Sender, ) -> Result>>, api::RikError>; +type HttpResult>> = Result, api::RikError>; + pub struct Router { routes: Vec<(tiny_http::Method, route_recognizer::Router)>, } diff --git a/controller/src/api/external/routes/tenant.rs b/controller/src/api/external/routes/tenant.rs index 8467b0b7..0c26f9d8 100644 --- a/controller/src/api/external/routes/tenant.rs +++ b/controller/src/api/external/routes/tenant.rs @@ -1,15 +1,14 @@ use route_recognizer; use rusqlite::Connection; -use std::io; use std::str::FromStr; use std::sync::mpsc::Sender; use tracing::{event, Level}; -use crate::api; +use super::HttpResult; use crate::api::external::services::element::elements_set_right_name; use crate::api::types::element::OnlyId; use crate::api::types::tenant::Tenant; -use crate::api::ApiChannel; +use crate::api::{ApiChannel, RikError}; use crate::database::RikRepository; pub fn get( @@ -17,13 +16,17 @@ pub fn get( _: &route_recognizer::Params, connection: &Connection, _: &Sender, -) -> Result>>, api::RikError> { +) -> HttpResult { if let Ok(mut tenants) = RikRepository::find_all(connection, "/tenant") { tenants = elements_set_right_name(tenants.clone()); - let tenants_json = serde_json::to_string(&tenants).unwrap(); + let tenants_json = serde_json::to_string(&tenants).map_err(RikError::ParsingError)?; event!(Level::INFO, "tenants.get, tenants found"); Ok(tiny_http::Response::from_string(tenants_json) - .with_header(tiny_http::Header::from_str("Content-Type: application/json").unwrap()) + .with_header( + tiny_http::Header::from_str("Content-Type: application/json").map_err(|_| { + RikError::Error("tiny_http::Header failed to add header".to_string()) + })?, + ) .with_status_code(tiny_http::StatusCode::from(200))) } else { Ok(tiny_http::Response::from_string("Cannot find tenant") @@ -36,15 +39,21 @@ pub fn create( _: &route_recognizer::Params, connection: &Connection, _: &Sender, -) -> Result>>, api::RikError> { +) -> HttpResult { let mut content = String::new(); - req.as_reader().read_to_string(&mut content).unwrap(); - let tenant: Tenant = serde_json::from_str(&content)?; + req.as_reader() + .read_to_string(&mut content) + .map_err(RikError::IoError)?; + let tenant: Tenant = serde_json::from_str(&content).map_err(RikError::ParsingError)?; if RikRepository::insert(connection, &tenant.name, &tenant.value).is_ok() { event!(Level::INFO, "Create tenant"); Ok(tiny_http::Response::from_string(content) - .with_header(tiny_http::Header::from_str("Content-Type: application/json").unwrap()) + .with_header( + tiny_http::Header::from_str("Content-Type: application/json").map_err(|_| { + RikError::Error("tiny_http::Header failed to add header".to_string()) + })?, + ) .with_status_code(tiny_http::StatusCode::from(200))) } else { event!(Level::ERROR, "Cannot create tenant"); @@ -58,13 +67,16 @@ pub fn delete( _: &route_recognizer::Params, connection: &Connection, _: &Sender, -) -> Result>>, api::RikError> { +) -> HttpResult { let mut content = String::new(); - req.as_reader().read_to_string(&mut content).unwrap(); - let OnlyId { id: delete_id } = serde_json::from_str(&content)?; + req.as_reader() + .read_to_string(&mut content) + .map_err(RikError::IoError)?; + let OnlyId { id: delete_id } = + serde_json::from_str(&content).map_err(RikError::ParsingError)?; if let Ok(tenant) = RikRepository::find_one(connection, &delete_id, "/tenant") { - RikRepository::delete(connection, &tenant.id).unwrap(); + RikRepository::delete(connection, &tenant.id).map_err(RikError::DataBaseError)?; event!(Level::INFO, "Delete tenant"); Ok(tiny_http::Response::from_string("").with_status_code(tiny_http::StatusCode::from(204))) } else { diff --git a/controller/src/api/external/routes/workload.rs b/controller/src/api/external/routes/workload.rs index 6e56ae51..1e5b550c 100644 --- a/controller/src/api/external/routes/workload.rs +++ b/controller/src/api/external/routes/workload.rs @@ -1,21 +1,17 @@ -use crate::api; +use super::HttpResult; use crate::api::external::services::element::elements_set_right_name; use crate::api::types::element::OnlyId; -use crate::api::{ApiChannel, Crud}; +use crate::api::{ApiChannel, Crud, RikError}; use crate::core::instance::Instance; use crate::database::RikRepository; use definition::workload::WorkloadDefinition; use route_recognizer; use rusqlite::Connection; use serde_json::json; -use std::io; use std::str::FromStr; use std::sync::mpsc::Sender; -use tiny_http::Response; use tracing::{event, Level}; -type HttpResult>> = Result, api::RikError>; - pub fn get( _: &mut tiny_http::Request, _: &route_recognizer::Params, @@ -24,7 +20,7 @@ pub fn get( ) -> HttpResult { if let Ok(mut workloads) = RikRepository::find_all(connection, "/workload") { workloads = elements_set_right_name(workloads.clone()); - let workloads_json = serde_json::to_string(&workloads).unwrap(); + let workloads_json = serde_json::to_string(&workloads).map_err(RikError::ParsingError)?; event!(Level::INFO, "workloads.get, workloads found"); Ok(tiny_http::Response::from_string(workloads_json) @@ -84,7 +80,8 @@ pub fn create( let mut content = String::new(); req.as_reader().read_to_string(&mut content).unwrap(); - let mut workload: WorkloadDefinition = serde_json::from_str(&content)?; + let mut workload: WorkloadDefinition = + serde_json::from_str(&content).map_err(RikError::ParsingError)?; if workload.replicas.is_none() { workload.replicas = Some(1); } @@ -104,18 +101,18 @@ pub fn create( if let Ok(inserted_id) = RikRepository::insert( connection, &name, - &serde_json::to_string(&workload).unwrap(), + &serde_json::to_string(&workload).map_err(RikError::ParsingError)?, ) { let workload_id: OnlyId = OnlyId { id: inserted_id }; event!( Level::INFO, "workload.create, workload successfully created" ); - Ok( - tiny_http::Response::from_string(serde_json::to_string(&workload_id).unwrap()) - .with_header(tiny_http::Header::from_str("Content-Type: application/json").unwrap()) - .with_status_code(tiny_http::StatusCode::from(200)), + Ok(tiny_http::Response::from_string( + serde_json::to_string(&workload_id).map_err(RikError::ParsingError)?, ) + .with_header(tiny_http::Header::from_str("Content-Type: application/json").unwrap()) + .with_status_code(tiny_http::StatusCode::from(200))) } else { event!(Level::ERROR, "workload.create, cannot create workload"); Ok(tiny_http::Response::from_string("Cannot create workload") @@ -131,7 +128,8 @@ pub fn delete( ) -> HttpResult { let mut content = String::new(); req.as_reader().read_to_string(&mut content).unwrap(); - let OnlyId { id: delete_id } = serde_json::from_str(&content)?; + let OnlyId { id: delete_id } = + serde_json::from_str(&content).map_err(RikError::ParsingError)?; if let Ok(workload) = RikRepository::find_one(connection, &delete_id, "/workload") { let definition: WorkloadDefinition = serde_json::from_value(workload.value).unwrap(); diff --git a/controller/src/api/mod.rs b/controller/src/api/mod.rs index 16fb5d2e..07c536d8 100644 --- a/controller/src/api/mod.rs +++ b/controller/src/api/mod.rs @@ -3,6 +3,9 @@ pub mod types; use definition::workload::WorkloadDefinition; use std::fmt::{Debug, Display, Formatter, Result}; +use thiserror::Error; + +use crate::database::DataBaseError; #[derive(Debug)] pub enum Crud { @@ -20,45 +23,25 @@ impl From for Crud { } } -#[derive(Debug)] +#[derive(Debug, Error)] pub enum RikError { + #[error("IO error: {0}")] IoError(std::io::Error), - HttpRequestError(serde_json::Error), + + #[error("HTTP request error: {0}")] + ParsingError(serde_json::Error), + + #[error("Internal communication error: {0}")] InternalCommunicationError(String), - InvalidName(String), -} -impl Display for RikError { - fn fmt(&self, f: &mut Formatter) -> Result { - match *self { - RikError::IoError(ref e) => write!(f, "{}", e), - RikError::HttpRequestError(ref e) => write!(f, "{}", e), - RikError::InternalCommunicationError(ref e) => write!(f, "{}", e), - RikError::InvalidName(ref e) => write!(f, "{}", e), - } - } -} -impl std::error::Error for RikError { - fn cause(&self) -> Option<&dyn std::error::Error> { - match *self { - RikError::IoError(ref e) => Some(e), - RikError::HttpRequestError(ref e) => Some(e), - // TODO: Implement other errors - _ => None, - } - } -} + #[error("Invalid name: {0}")] + InvalidName(String), -impl From for RikError { - fn from(e: std::io::Error) -> RikError { - RikError::IoError(e) - } -} + #[error("Error: {0}")] + Error(String), -impl From for RikError { - fn from(e: serde_json::Error) -> RikError { - RikError::HttpRequestError(e) - } + #[error("Database error: {0}")] + DataBaseError(DataBaseError), } pub struct ApiChannel { diff --git a/controller/src/database/mod.rs b/controller/src/database/mod.rs index 09a78970..67691b61 100644 --- a/controller/src/database/mod.rs +++ b/controller/src/database/mod.rs @@ -1,10 +1,22 @@ use crate::api::types::element::Element; use dotenv::dotenv; -use rusqlite::{params, Connection, Result}; +use rusqlite::{params, Connection}; use std::sync::Arc; +use thiserror::Error; use uuid::Uuid; +#[derive(Debug, Error)] +pub enum DataBaseError { + #[error("Error: {0}")] + SqlError(rusqlite::Error), + + #[error("Io error: {0}")] + IoError(std::io::Error), +} + +type Result = std::result::Result; + #[allow(dead_code)] pub struct RikDataBase { name: String, @@ -19,15 +31,17 @@ impl RikDataBase { pub fn init_tables(&self) -> Result<()> { let connection = self.open()?; // only work with sqlite for now - connection.execute_batch( - "CREATE TABLE IF NOT EXISTS cluster ( + connection + .execute_batch( + "CREATE TABLE IF NOT EXISTS cluster ( id TEXT PRIMARY KEY, name TEXT NOT NULL, value BLOB NOT NULL ); CREATE INDEX IF NOT EXISTS cluster_name_index ON cluster (name); CREATE INDEX IF NOT EXISTS cluster_name_id_index ON cluster (name,id);", - )?; + ) + .map_err(DataBaseError::SqlError)?; Ok(()) } @@ -37,10 +51,10 @@ impl RikDataBase { dotenv().ok(); let file_path = std::env::var("DATABASE_LOCATION").unwrap_or("/var/lib/rik/data/".to_string()); - std::fs::create_dir_all(&file_path).unwrap(); + std::fs::create_dir_all(&file_path).map_err(DataBaseError::IoError)?; let database_path = format!("{}{}.db", file_path, self.name); - Connection::open(database_path) + Connection::open(database_path).map_err(DataBaseError::SqlError) } } @@ -58,34 +72,36 @@ impl RikRepository { } pub fn delete(connection: &Connection, id: &String) -> Result<()> { - connection.execute("DELETE FROM cluster WHERE id = (?1)", params![id])?; + connection + .execute("DELETE FROM cluster WHERE id = (?1)", params![id]) + .map_err(DataBaseError::SqlError)?; Ok(()) } pub fn find_one(connection: &Connection, id: &String, element_type: &str) -> Result { - let mut stmt = connection.prepare(&format!( - "SELECT id, name, value FROM cluster WHERE id = '{}' AND name LIKE '{}%'", - id, element_type - ))?; - match stmt.query_row([], |row| { + let mut stmt = connection + .prepare(&format!( + "SELECT id, name, value FROM cluster WHERE id = '{}' AND name LIKE '{}%'", + id, element_type + )) + .map_err(DataBaseError::SqlError)?; + stmt.query_row([], |row| { Ok(Element::new(row.get(0)?, row.get(1)?, row.get(2)?)) - }) { - Ok(element) => Ok(element), - Err(err) => Err(err), - } + }) + .map_err(DataBaseError::SqlError) } pub fn check_duplicate_name(connection: &Connection, name: &str) -> Result { - let mut stmt = connection.prepare(&format!( - "SELECT id, name, value FROM cluster WHERE name LIKE '{}%'", - name - ))?; - match stmt.query_row([], |row| { + let mut stmt = connection + .prepare(&format!( + "SELECT id, name, value FROM cluster WHERE name LIKE '{}%'", + name + )) + .map_err(DataBaseError::SqlError)?; + stmt.query_row([], |row| { Ok(Element::new(row.get(0)?, row.get(1)?, row.get(2)?)) - }) { - Ok(element) => Ok(element), - Err(err) => Err(err), - } + }) + .map_err(DataBaseError::SqlError) } // TODO: add pagination @@ -104,16 +120,18 @@ impl RikRepository { let mut elements: Vec = Vec::new(); for element in elements_iter { - elements.push(element?); + elements.push(element.map_err(DataBaseError::SqlError)?); } Ok(elements) } pub fn update(connection: &Connection, id: &String, value: &String) -> Result<()> { - connection.execute( - "UPDATE cluster SET value=(?1) WHERE id = (?2)", - params![value, id], - )?; + connection + .execute( + "UPDATE cluster SET value=(?1) WHERE id = (?2)", + params![value, id], + ) + .map_err(DataBaseError::SqlError)?; Ok(()) } @@ -133,7 +151,7 @@ impl RikRepository { "INSERT INTO cluster (id, name, value) VALUES (?1, ?2, ?3)", params![id, name, value], ) - .unwrap(); + .map_err(DataBaseError::SqlError)?; Ok(id.to_string()) } } @@ -141,8 +159,10 @@ impl RikRepository { #[cfg(test)] mod test { - use crate::database::{RikDataBase, RikRepository}; - use crate::tests::fixtures::db_connection; + use crate::{ + database::{RikDataBase, RikRepository}, + tests::fixtures::db_connection, + }; use rstest::rstest; use uuid::Uuid;