diff --git a/api/Cargo.toml b/api/Cargo.toml index 9c6f1de..5ba95db 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -23,6 +23,8 @@ rand = "0.8.4" tokio = { version = "1.0", features = ["rt-multi-thread", "macros", "process"] } tonic = { version = "0.10.2", features = ["transport"] } prost = "0.12.1" +async-trait = "0.1.74" +mockall = "0.11.4" [build-dependencies] tonic-build = { version = "0.10.2", features = ["prost"] } diff --git a/api/src/api/mod.rs b/api/src/api/mod.rs index 1f902fc..4e3d19c 100644 --- a/api/src/api/mod.rs +++ b/api/src/api/mod.rs @@ -4,40 +4,29 @@ use actix_web::{post, web, Responder}; use log::{debug, error, info, trace, warn}; use crate::{ - api::service::LambdoApiService, + api::service::{LambdoApiService, LambdoApiServiceTrait}, model::{RunRequest, RunResponse}, vm_manager::{self, grpc_definitions::ExecuteResponse}, }; use std::error::Error; -#[post("/run")] -async fn run( - run_body: web::Json, - service: web::Data, -) -> Result> { - debug!( - "Received code execution request from http (language: {}, version: {})", - run_body.language, run_body.version - ); - trace!("Request body: {:?}", run_body); - - let response = service.run_code(run_body.into_inner()).await; +async fn run_code(run_resquest: RunRequest, service: &dyn LambdoApiServiceTrait) -> RunResponse { + let response = service.run_code(run_resquest).await; - let response = match response { + match response { Ok(response) => { info!("Execution ended for {:?}", response.id); trace!("Response: {:?}", response); parse_response(response) } - // for the moment just signal an internal server error Err(e) => match e { vm_manager::Error::Timeout => { warn!("Timeout while executing code"); - return Ok(web::Json(RunResponse { + RunResponse { status: 128, stdout: "".to_string(), stderr: "Timeout".to_string(), - })); + } } _ => { error!("Error while executing code: {:?}", e); @@ -48,12 +37,35 @@ async fn run( } } }, - }; + } +} + +#[post("/run")] +pub async fn post_run_route( + run_body: web::Json, + api_service: web::Data, +) -> Result> { + debug!( + "Received code execution request from http (language: {}, version: {})", + run_body.language, run_body.version + ); + trace!("Request body: {:?}", run_body); + + let service = api_service.get_ref(); + let result = run_code(run_body.into_inner(), service); - Ok(web::Json(response)) + Ok(web::Json(result.await)) } fn parse_response(response: ExecuteResponse) -> RunResponse { + if response.steps.is_empty() { + return RunResponse { + status: 1, + stdout: "".to_string(), + stderr: "Nothing was run".to_string(), + }; + } + let mut stdout = String::new(); let mut stderr = String::new(); for step in response.steps.as_slice() { @@ -67,8 +79,137 @@ fn parse_response(response: ExecuteResponse) -> RunResponse { status: response.steps[response.steps.len() - 1] .exit_code .try_into() - .unwrap(), + .unwrap_or(1), stdout, stderr, } } + +#[cfg(test)] +mod test { + use std::vec; + + use crate::{ + api::{parse_response, run_code}, + model::RunRequest, + vm_manager::grpc_definitions::{ExecuteResponse, ExecuteResponseStep, FileModel}, + }; + + use super::service::MockLambdoApiServiceTrait; + + #[test] + fn test_parse_response_stdout() { + let response = ExecuteResponse { + id: "test".to_string(), + steps: vec![ + ExecuteResponseStep { + command: "echo Hello".to_string(), + stdout: "Hello".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ExecuteResponseStep { + command: "echo World".to_string(), + stdout: "World".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ], + }; + + let parsed = parse_response(response); + + assert_eq!(parsed.stdout, "HelloWorld"); + assert_eq!(parsed.stderr, ""); + assert_eq!(parsed.status, 0); + } + + #[test] + fn test_parse_response_with_error() { + let response = ExecuteResponse { + id: "test".to_string(), + steps: vec![ + ExecuteResponseStep { + command: "echo Hello".to_string(), + stdout: "Hello".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ExecuteResponseStep { + command: "echo World".to_string(), + stdout: "".to_string(), + stderr: "Error".to_string(), + exit_code: 1, + }, + ], + }; + + let parsed = parse_response(response); + + assert_eq!(parsed.stdout, "Hello"); + assert_eq!(parsed.stderr, "Error"); + assert_eq!(parsed.status, 1); + } + + #[tokio::test] + async fn test_run_code_with_no_steps() { + let mut mock_service = MockLambdoApiServiceTrait::new(); + mock_service.expect_run_code().once().returning(|_| { + Ok(ExecuteResponse { + id: "test".to_string(), + steps: vec![], + }) + }); + + let run_request = RunRequest { + language: "Node".to_string(), + version: "1".to_string(), + code: vec![], + input: "".to_string(), + }; + + let response = run_code(run_request, &mock_service).await; + assert_eq!(response.status, 1); + assert_eq!(response.stdout, ""); + assert_eq!(response.stderr, "Nothing was run"); + } + + #[tokio::test] + async fn test_run_with_steps() { + let mut mock_service = MockLambdoApiServiceTrait::new(); + mock_service.expect_run_code().once().returning(|_| { + Ok(ExecuteResponse { + id: "test".to_string(), + steps: vec![ + ExecuteResponseStep { + command: "echo Hello".to_string(), + stdout: "Hello".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ExecuteResponseStep { + command: "echo World".to_string(), + stdout: "World".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ], + }) + }); + + let run_request = RunRequest { + language: "Node".to_string(), + version: "1".to_string(), + code: vec![FileModel { + filename: "test.js".to_string(), + content: "console.log('Hello World')".to_string(), + }], + input: "test.js".to_string(), + }; + + let response = run_code(run_request, &mock_service).await; + assert_eq!(response.status, 0); + assert_eq!(response.stdout, "HelloWorld"); + assert_eq!(response.stderr, ""); + } +} diff --git a/api/src/api/service.rs b/api/src/api/service.rs index c35cd87..429b2f6 100644 --- a/api/src/api/service.rs +++ b/api/src/api/service.rs @@ -1,39 +1,85 @@ use crate::{ config::{LambdoConfig, LambdoLanguageConfig}, - vm_manager::grpc_definitions::{ - ExecuteRequest, ExecuteRequestStep, ExecuteResponse, FileModel, + vm_manager::{ + grpc_definitions::{ExecuteRequest, ExecuteRequestStep, ExecuteResponse, FileModel}, + VMManagerTrait, }, vm_manager::{state::LambdoStateRef, Error, VMManager}, }; use log::{debug, trace}; +use mockall::automock; use uuid::Uuid; use crate::model::RunRequest; +#[automock] +#[async_trait::async_trait] +pub trait LambdoApiServiceTrait: Send + Sync { + async fn run_code(&self, request: RunRequest) -> Result; +} + pub struct LambdoApiService { pub config: LambdoConfig, - pub vm_manager: VMManager, + pub vm_manager: Box, } impl LambdoApiService { pub async fn new(config: LambdoConfig) -> Result { let state = crate::vm_manager::state::LambdoState::new(config.clone()); let vm_manager = - VMManager::new(std::sync::Arc::new(tokio::sync::Mutex::new(state))).await?; - Ok(LambdoApiService { config, vm_manager }) + VMManager::from_state(std::sync::Arc::new(tokio::sync::Mutex::new(state))).await?; + Ok(LambdoApiService { + config, + vm_manager: Box::new(vm_manager), + }) } pub async fn new_with_state(state: LambdoStateRef) -> Result { let config = state.lock().await.config.clone(); - let vm_manager = VMManager::new(state).await?; - Ok(LambdoApiService { config, vm_manager }) + let vm_manager = VMManager::from_state(state).await?; + Ok(LambdoApiService { + config, + vm_manager: Box::new(vm_manager), + }) + } + + fn find_language( + &self, + language: &String, + ) -> Result> { + let language_list = &self.config.languages; + for lang in language_list { + if &*lang.name == language { + return Ok(lang.clone()); + } + } + Err("Language not found".into()) + } + + fn generate_steps( + language_settings: &LambdoLanguageConfig, + entrypoint: &str, + ) -> Vec { + let mut steps: Vec = Vec::new(); + for step in &language_settings.steps { + let command = step.command.replace("{{filename}}", entrypoint); + + steps.push(ExecuteRequestStep { + command, + enable_output: step.output.enabled, + }); + } + steps } +} - pub async fn run_code(&self, request: RunRequest) -> Result { +#[async_trait::async_trait] +impl LambdoApiServiceTrait for LambdoApiService { + async fn run_code(&self, request: RunRequest) -> Result { let entrypoint = request.code[0].filename.clone(); let language_settings = self.find_language(&request.language).unwrap(); - let steps = self.generate_steps(&language_settings, &entrypoint); + let steps = Self::generate_steps(&language_settings, &entrypoint); let file = FileModel { filename: entrypoint.to_string(), content: request.code[0].content.clone(), @@ -60,34 +106,206 @@ impl LambdoApiService { response } +} - fn find_language( - &self, - language: &String, - ) -> Result> { - let language_list = &self.config.languages; - for lang in language_list { - if &*lang.name == language { - return Ok(lang.clone()); - } +#[cfg(test)] +mod test { + use std::sync::Arc; + + use mockall::predicate; + use tokio::sync::Mutex; + + use super::LambdoApiService; + use crate::{ + api::service::LambdoApiServiceTrait, + config::{ + LambdoAgentConfig, LambdoApiConfig, LambdoConfig, LambdoLanguageConfig, + LambdoLanguageStepConfig, LambdoLanguageStepOutputConfig, LambdoVMMConfig, + }, + model::{LanguageSettings, RunRequest}, + vm_manager::{ + grpc_definitions::{ExecuteRequest, ExecuteResponse, ExecuteResponseStep, FileModel}, + state::LambdoState, + MockVMManagerTrait, VMManager, + }, + }; + + fn generate_lambdo_test_config() -> LambdoConfig { + LambdoConfig { + apiVersion: "lambdo.io/v1alpha1".to_string(), + kind: "Config".to_string(), + api: LambdoApiConfig { + web_host: "0.0.0.0".to_string(), + web_port: 3000, + grpc_host: "0.0.0.0".to_string(), + gprc_port: 50051, + bridge: "lambdo0".to_string(), + bridge_address: "0.0.0.0".to_string(), + }, + vmm: LambdoVMMConfig { + kernel: "/var/lib/lambdo/kernel/vmlinux.bin".to_string(), + }, + agent: LambdoAgentConfig { + path: "/usr/local/bin/lambdo-agent".to_string(), + config: "/etc/lambdo/agent.yaml".to_string(), + }, + languages: vec![ + LambdoLanguageConfig { + name: "NODE".to_string(), + version: "1.0".to_string(), + initramfs: "test".to_string(), + steps: vec![ + LambdoLanguageStepConfig { + name: Some("step 1".to_string()), + command: "echo {{filename}}".to_string(), + output: LambdoLanguageStepOutputConfig { + enabled: true, + debug: false, + }, + }, + LambdoLanguageStepConfig { + name: Some("step 2".to_string()), + command: "echo hello".to_string(), + output: LambdoLanguageStepOutputConfig { + enabled: true, + debug: false, + }, + }, + LambdoLanguageStepConfig { + name: Some("step 3".to_string()), + command: "cat {{filename}} > {{filename}}".to_string(), + output: LambdoLanguageStepOutputConfig { + enabled: true, + debug: false, + }, + }, + ], + }, + LambdoLanguageConfig { + name: "PYTHON".to_string(), + version: "3.0".to_string(), + initramfs: "test".to_string(), + steps: vec![LambdoLanguageStepConfig { + name: Some("step".to_string()), + command: "echo {{filename}}".to_string(), + output: LambdoLanguageStepOutputConfig { + enabled: true, + debug: false, + }, + }], + }, + ], } - Err("Language not found".into()) } - fn generate_steps( - &self, - language_settings: &LambdoLanguageConfig, - entrypoint: &str, - ) -> Vec { - let mut steps: Vec = Vec::new(); - for step in &language_settings.steps { - let command = step.command.replace("{{filename}}", entrypoint); + #[test] + fn test_generate_steps() { + let language_settings = LambdoLanguageConfig { + name: "NODE".to_string(), + version: "1.0".to_string(), + initramfs: "test".to_string(), + steps: generate_lambdo_test_config().languages[0].steps.clone(), + }; + let entrypoint = "index.js"; - steps.push(ExecuteRequestStep { - command, - enable_output: step.output.enabled, - }); + let expected_steps = vec![ + "echo index.js".to_string(), + "echo hello".to_string(), + "cat index.js > index.js".to_string(), + ]; + + let steps = LambdoApiService::generate_steps(&language_settings, &entrypoint); + + assert_eq!(steps.len(), 3); + for (i, step) in steps.iter().enumerate() { + assert_eq!(step.command, expected_steps[i]); } - steps + } + + #[test] + fn test_find_language() { + let config = generate_lambdo_test_config(); + let service = LambdoApiService { + config: config.clone(), + vm_manager: Box::new(VMManager { + state: Arc::new(Mutex::new(LambdoState::new(config))), + }), + }; + + let language = "NODE".to_string(); + let language_settings = service.find_language(&language).unwrap(); + + assert_eq!(language_settings.name, language); + assert_eq!(language_settings.steps[0].name, Some("step 1".to_string())); + } + + #[tokio::test] + async fn test_run_code() { + let config = generate_lambdo_test_config(); + + let language = "NODE".to_string(); + let code = vec![FileModel { + filename: "index.js".to_string(), + content: "console.log('hello world')".to_string(), + }]; + let input = "hello".to_string(); + + let request = RunRequest { + version: "1.0".to_string(), + language: language.clone(), + code, + input, + }; + + let expected_language_settings = config.languages[0].clone(); + assert_eq!(expected_language_settings.name, language.clone()); + + let expected_response = ExecuteResponse { + id: "test".to_string(), + steps: vec![ + ExecuteResponseStep { + command: "echo index.js".to_string(), + stdout: "index.js\n".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ExecuteResponseStep { + command: "echo hello".to_string(), + stdout: "hello\n".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ExecuteResponseStep { + command: "cat index.js > index.js".to_string(), + stdout: "".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ], + }; + + let response = expected_response.clone(); + let mut mock_vm_manager = MockVMManagerTrait::new(); + mock_vm_manager + .expect_run_code() + .with( + predicate::function(|req: &ExecuteRequest| { + req.files[0].filename == "index.js" && req.steps[0].command == "echo index.js" + }), + predicate::function(move |lang: &LanguageSettings| { + lang.name == language && lang.version == expected_language_settings.version + }), + ) + .times(1) + .returning(move |_, _| Ok(response.clone())); + + let service = LambdoApiService { + config: config.clone(), + vm_manager: Box::new(mock_vm_manager), + }; + + let response = service.run_code(request).await.unwrap(); + + assert_eq!(response, expected_response); } } diff --git a/api/src/main.rs b/api/src/main.rs index d01a9aa..caf0c9e 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -9,7 +9,7 @@ use config::LambdoConfig; use thiserror::Error; use crate::{ - api::{run, service::LambdoApiService}, + api::{post_run_route, service::LambdoApiService}, vm_manager::grpc_definitions::lambdo_api_service_server::LambdoApiServiceServer, vm_manager::state::LambdoState, vm_manager::VMListener, @@ -87,8 +87,12 @@ async fn main() -> std::io::Result<()> { let http_port = config.api.web_port; let app_state = web::Data::new(api_service); info!("Starting web server on {}:{}", http_host, http_port); - HttpServer::new(move || App::new().app_data(app_state.clone()).service(run)) - .bind((http_host.clone(), http_port))? - .run() - .await + HttpServer::new(move || { + App::new() + .app_data(app_state.clone()) + .service(post_run_route) + }) + .bind((http_host.clone(), http_port))? + .run() + .await } diff --git a/api/src/vm_manager/mod.rs b/api/src/vm_manager/mod.rs index c66fd97..c24de4b 100644 --- a/api/src/vm_manager/mod.rs +++ b/api/src/vm_manager/mod.rs @@ -1,4 +1,5 @@ pub mod state; +use mockall::automock; use network_interface::{NetworkInterface, NetworkInterfaceConfig}; use tokio::process::Command; @@ -21,12 +22,27 @@ use self::{ mod vmm; +#[automock] +#[async_trait::async_trait] +pub trait VMManagerTrait: Sync + Send { + async fn from_state(state: LambdoStateRef) -> Result + where + Self: Sized; + + async fn run_code( + &self, + request: ExecuteRequest, + language_settings: LanguageSettings, + ) -> Result; +} + pub struct VMManager { pub state: LambdoStateRef, } -impl VMManager { - pub async fn new(state: LambdoStateRef) -> Result { +#[async_trait::async_trait] +impl VMManagerTrait for VMManager { + async fn from_state(state: LambdoStateRef) -> Result { let mut vmm_manager = VMManager { state }; { @@ -51,7 +67,7 @@ impl VMManager { Ok(vmm_manager) } - pub async fn run_code( + async fn run_code( &self, request: ExecuteRequest, language_settings: LanguageSettings, @@ -112,7 +128,9 @@ impl VMManager { Ok(response) } +} +impl VMManager { pub async fn event_listener(&mut self) { let mut receiver = self.state.lock().await.channel.1.resubscribe(); let state = self.state.clone();