diff --git a/Cargo.lock b/Cargo.lock index 0ea5e67f..1f08eb2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3450,7 +3450,7 @@ dependencies = [ "derive_builder 0.12.0", "docker-compose-types", "futures", - "futures-lite 1.13.0", + "futures-lite 2.2.0", "futures-util", "glob", "hmac", diff --git a/Cargo.toml b/Cargo.toml index f817a308..b7daae6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ derive_builder = "0.12.0" indexmap = { version = "2.0.0", features = ["serde"], optional = true } serde_yaml = "0.9" lapin = { version = "2.3.1", features = ["serde_json"] } -futures-lite = "1.13.0" +futures-lite = "2.2.0" clap = { version = "4.4.8", features = ["derive"] } brotli = "3.4.0" serde_path_to_error = "0.1.14" diff --git a/Dockerfile b/Dockerfile index c068fc29..34fa4b03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,6 +54,7 @@ COPY --from=builder /app/target/release/server . COPY --from=builder /app/.env . COPY --from=builder /app/configuration.yaml . COPY --from=builder /usr/local/cargo/bin/sqlx sqlx +COPY ./access_control.conf.dist /app EXPOSE 8000 diff --git a/README.md b/README.md index 53879ed3..09f466ff 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Stacker - is an application that helps users to create custom IT solutions based on dockerized open -source apps and user's custom applications docker containers. Users can build their own stack of applications, and +source apps and user's custom applications docker containers. Users can build their own project of applications, and deploy the final result to their favorite clouds using TryDirect API. Application development will include: @@ -36,9 +36,9 @@ Stacker (API) - Serves API clients Authentication made through TryDirect OAuth, here we have only client Database (Read only) Logging/Tracing (Files) / Quickwit for future -/stack (WebUI, as a result we have a JSON) -/stack/deploy -> sends deploy command to TryDirect Install service -/stack/deploy/status - get installation progress (rabbitmq client), +/project (WebUI, as a result we have a JSON) +/project/deploy -> sends deploy command to TryDirect Install service +/project/deploy/status - get installation progress (rabbitmq client), #### TODO Find out how to get user's token for queue @@ -76,7 +76,7 @@ sqlx migrate revert #### Deploy ``` -curl -X POST -H "Content-Type: application/json" -d @custom-stack-payload-2.json http://127.0.0.1:8000/stack +curl -X POST -H "Content-Type: application/json" -d @custom-stack-payload-2.json http://127.0.0.1:8000/project ``` diff --git a/docker-compose.yml b/docker-compose.yml index 9a66dde3..e9aee75a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,13 +8,14 @@ volumes: services: stacker: - image: trydirect/stacker:0.0.6 + image: trydirect/stacker:0.0.7 build: . container_name: stacker restart: always volumes: - ./files:/app/files - ./docker/local/configuration.yaml:/app/configuration.yaml + - ./access_control.conf:/app/access_control.conf - ./migrations:/app/migrations - ./docker/local/.env:/app/.env ports: diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index c1a022b2..6bf8eb55 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -13,13 +13,14 @@ networks: services: stacker: - image: trydirect/stacker:0.0.5 + image: trydirect/stacker:0.0.7 build: . container_name: stacker restart: always volumes: - ./stacker/files:/app/files - ./configuration.yaml:/app/configuration.yaml + - ./access_control.conf:/app/access_control.conf - ./migrations:/app/migrations - ./.env:/app/.env ports: diff --git a/docker/local/configuration.yaml b/docker/local/configuration.yaml index a0bb4c00..750f1cbb 100644 --- a/docker/local/configuration.yaml +++ b/docker/local/configuration.yaml @@ -1,6 +1,8 @@ app_host: 0.0.0.0 app_port: 8000 auth_url: https://dev.try.direct/server/user/oauth_server/api/me +max_clients_number: 2 + database: host: 172.17.0.2 port: 5432 diff --git a/migrations/20230903063840_creating_rating_tables.up.sql b/migrations/20230903063840_creating_rating_tables.up.sql index 28422706..156c7221 100644 --- a/migrations/20230903063840_creating_rating_tables.up.sql +++ b/migrations/20230903063840_creating_rating_tables.up.sql @@ -3,7 +3,7 @@ CREATE TYPE rate_category AS ENUM ( 'application', 'cloud', - 'stack', + 'project', 'deploymentSpeed', 'documentation', 'design', diff --git a/migrations/20230905145525_creating_stack_tables.down.sql b/migrations/20230905145525_creating_stack_tables.down.sql index 2f2a6e48..7f367dff 100644 --- a/migrations/20230905145525_creating_stack_tables.down.sql +++ b/migrations/20230905145525_creating_stack_tables.down.sql @@ -1,2 +1,2 @@ -- Add down migration script here -DROP TABLE user_stack; +DROP TABLE project; diff --git a/migrations/20230905145525_creating_stack_tables.up.sql b/migrations/20230905145525_creating_stack_tables.up.sql index b20b2cdb..c002beb0 100644 --- a/migrations/20230905145525_creating_stack_tables.up.sql +++ b/migrations/20230905145525_creating_stack_tables.up.sql @@ -1,13 +1,14 @@ -CREATE TABLE user_stack ( +CREATE TABLE project ( id serial4 NOT NULL, stack_id uuid NOT NULL, user_id VARCHAR(50) NOT NULL, - name TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, body JSON NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, - CONSTRAINT user_stack_pkey PRIMARY KEY (id) + CONSTRAINT project_pkey PRIMARY KEY (id) ); -CREATE INDEX idx_stack_id ON user_stack(stack_id); -CREATE INDEX idx_stack_user_id ON user_stack(user_id); \ No newline at end of file +CREATE INDEX idx_project_stack_id ON project(stack_id); +CREATE INDEX idx_project_user_id ON project(user_id); +CREATE INDEX idx_project_name ON project(name); diff --git a/migrations/20240228125751_creating_deployments.down.sql b/migrations/20240228125751_creating_deployments.down.sql new file mode 100644 index 00000000..228cc137 --- /dev/null +++ b/migrations/20240228125751_creating_deployments.down.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +DROP table deployment; \ No newline at end of file diff --git a/migrations/20240228125751_creating_deployments.up.sql b/migrations/20240228125751_creating_deployments.up.sql new file mode 100644 index 00000000..7a06d3b8 --- /dev/null +++ b/migrations/20240228125751_creating_deployments.up.sql @@ -0,0 +1,14 @@ +-- Add up migration script here +CREATE TABLE deployment ( + id serial4 NOT NULL, + project_id integer NOT NULL, + body JSON NOT NULL, + deleted BOOLEAN DEFAULT FALSE, + status VARCHAR(32) NOT NULL, + created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL, + CONSTRAINT fk_project FOREIGN KEY(project_id) REFERENCES project(id), + CONSTRAINT deployment_pkey PRIMARY KEY (id) +); + +CREATE INDEX idx_deployment_project_id ON deployment(project_id); diff --git a/migrations/20240229072555_creating_cloud.down.sql b/migrations/20240229072555_creating_cloud.down.sql new file mode 100644 index 00000000..2a04e928 --- /dev/null +++ b/migrations/20240229072555_creating_cloud.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP table cloud; diff --git a/migrations/20240229072555_creating_cloud.up.sql b/migrations/20240229072555_creating_cloud.up.sql new file mode 100644 index 00000000..c842d3f2 --- /dev/null +++ b/migrations/20240229072555_creating_cloud.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE cloud ( + id serial4 NOT NULL, + user_id VARCHAR(50) NOT NULL, + provider VARCHAR(50) NOT NULL, + cloud_token VARCHAR(255) , + cloud_key VARCHAR(255), + cloud_secret VARCHAR(255), + save_token BOOLEAN DEFAULT FALSE, + created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL, + CONSTRAINT user_cloud_pkey PRIMARY KEY (id) +); + +CREATE INDEX idx_deployment_user_cloud_user_id ON cloud(user_id); \ No newline at end of file diff --git a/migrations/20240229075843_creating_user_stack_cloud_relation.down.sql b/migrations/20240229075843_creating_user_stack_cloud_relation.down.sql new file mode 100644 index 00000000..02d2fe54 --- /dev/null +++ b/migrations/20240229075843_creating_user_stack_cloud_relation.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +ALTER table project DROP COLUMN cloud_id; \ No newline at end of file diff --git a/migrations/20240229075843_creating_user_stack_cloud_relation.up.sql b/migrations/20240229075843_creating_user_stack_cloud_relation.up.sql new file mode 100644 index 00000000..5f65c667 --- /dev/null +++ b/migrations/20240229075843_creating_user_stack_cloud_relation.up.sql @@ -0,0 +1,3 @@ +-- Add up migration script here +ALTER table project ADD COLUMN cloud_id INT CONSTRAINT project_cloud_id REFERENCES cloud(id) ON UPDATE CASCADE ON DELETE CASCADE; + diff --git a/migrations/20240229080559_creating_cloud_server.down.sql b/migrations/20240229080559_creating_cloud_server.down.sql new file mode 100644 index 00000000..f0fa9822 --- /dev/null +++ b/migrations/20240229080559_creating_cloud_server.down.sql @@ -0,0 +1,3 @@ +DROP INDEX idx_server_user_id; +DROP INDEX idx_server_cloud_id; +DROP table server; diff --git a/migrations/20240229080559_creating_cloud_server.up.sql b/migrations/20240229080559_creating_cloud_server.up.sql new file mode 100644 index 00000000..e4ed91bb --- /dev/null +++ b/migrations/20240229080559_creating_cloud_server.up.sql @@ -0,0 +1,22 @@ +-- Add up migration script here + +CREATE TABLE server ( + id serial4 NOT NULL, + user_id VARCHAR(50) NOT NULL, + cloud_id integer NOT NULL, + project_id integer NOT NULL, + region VARCHAR(50) NOT NULL, + zone VARCHAR(50), + server VARCHAR(255) NOT NULL, + os VARCHAR(100) NOT NULL, + disk_type VARCHAR(100), + created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL, + CONSTRAINT user_server_pkey PRIMARY KEY (id), + CONSTRAINT fk_server FOREIGN KEY(cloud_id) REFERENCES cloud(id), + CONSTRAINT fk_server_project FOREIGN KEY(project_id) REFERENCES project(id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE INDEX idx_server_user_id ON server(user_id); +CREATE INDEX idx_server_cloud_id ON server(cloud_id); +CREATE INDEX idx_server_project_id ON server(project_id); diff --git a/migrations/20240302081015_creating_original_request_column_project.down.sql b/migrations/20240302081015_creating_original_request_column_project.down.sql new file mode 100644 index 00000000..93549b57 --- /dev/null +++ b/migrations/20240302081015_creating_original_request_column_project.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +ALTER table project DROP COLUMN request_json; diff --git a/migrations/20240302081015_creating_original_request_column_project.up.sql b/migrations/20240302081015_creating_original_request_column_project.up.sql new file mode 100644 index 00000000..2c1ba74c --- /dev/null +++ b/migrations/20240302081015_creating_original_request_column_project.up.sql @@ -0,0 +1 @@ +ALTER table project ADD COLUMN request_json JSON NOT NULL DEFAULT '{}'; \ No newline at end of file diff --git a/migrations/20240307113718_alter_cloud_alter_project.down.sql b/migrations/20240307113718_alter_cloud_alter_project.down.sql new file mode 100644 index 00000000..06f51ab5 --- /dev/null +++ b/migrations/20240307113718_alter_cloud_alter_project.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +ALTER table project ADD COLUMN cloud_id INT CONSTRAINT project_cloud_id REFERENCES cloud(id) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER table cloud DROP COLUMN project_id; \ No newline at end of file diff --git a/migrations/20240307113718_alter_cloud_alter_project.up.sql b/migrations/20240307113718_alter_cloud_alter_project.up.sql new file mode 100644 index 00000000..554a24a8 --- /dev/null +++ b/migrations/20240307113718_alter_cloud_alter_project.up.sql @@ -0,0 +1,3 @@ +-- Add up migration script here +ALTER table project DROP COLUMN cloud_id; +ALTER table cloud ADD COLUMN project_id INT CONSTRAINT cloud_project_id REFERENCES project(id) ON UPDATE CASCADE ON DELETE CASCADE; diff --git a/migrations/20240315143712_remove_cloud_id_from_server.down.sql b/migrations/20240315143712_remove_cloud_id_from_server.down.sql new file mode 100644 index 00000000..72dd11e2 --- /dev/null +++ b/migrations/20240315143712_remove_cloud_id_from_server.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +DROP INDEX idx_server_cloud_id; +alter table server ADD column cloud_id integer NOT NULL; diff --git a/migrations/20240315143712_remove_cloud_id_from_server.up.sql b/migrations/20240315143712_remove_cloud_id_from_server.up.sql new file mode 100644 index 00000000..be9027c0 --- /dev/null +++ b/migrations/20240315143712_remove_cloud_id_from_server.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +alter table server drop column cloud_id; diff --git a/src/console/commands/mod.rs b/src/console/commands/mod.rs index 2cf75be7..585347d2 100644 --- a/src/console/commands/mod.rs +++ b/src/console/commands/mod.rs @@ -1,4 +1,6 @@ pub mod appclient; mod callable; +pub mod mq; pub use callable::*; +pub use mq::*; diff --git a/src/console/commands/mq/listener.rs b/src/console/commands/mq/listener.rs new file mode 100644 index 00000000..cfe329a3 --- /dev/null +++ b/src/console/commands/mq/listener.rs @@ -0,0 +1,89 @@ +use crate::configuration::get_configuration; +use actix_web::rt; +use actix_web::web; +use lapin::{Channel, Queue}; +use lapin::options::{BasicAckOptions, BasicConsumeOptions}; +use lapin::types::FieldTable; +use sqlx::PgPool; +use db::deployment; +use crate::{db, helpers}; +use crate::helpers::mq_manager; +use crate::helpers::mq_manager::MqManager; +use futures_lite::stream::StreamExt; + +pub struct ListenCommand { +} + +impl ListenCommand { + pub fn new() -> Self { + Self {} + } +} + +impl crate::console::commands::CallableTrait for ListenCommand { + + fn call(&self) -> Result<(), Box> { + rt::System::new().block_on(async { + let settings = get_configuration().expect("Failed to read configuration."); + let db_pool = PgPool::connect(&settings.database.connection_string()) + .await + .expect("Failed to connect to database."); + + let db_pool = web::Data::new(db_pool); + + let mq_manager = MqManager::try_new(settings.amqp.connection_string())?; + let consumer_channel= mq_manager + .consume( + "install_progress", + "install_progress_*******" + ) + .await?; + + + let mut consumer = consumer_channel + .basic_consume( + "install_progress", + "console_listener", + BasicConsumeOptions::default(), + FieldTable::default(), + ) + .await + .expect("Basic consume"); + + // .map_err(|err| format!("Error {:?}", err)); + + tracing::info!("will consume"); + // if let Ok(consumer) = consumer { + while let Some(delivery) = consumer.next().await { + let delivery = delivery.expect("error in consumer"); + delivery.ack(BasicAckOptions::default()).await.expect("ack"); + } + // } + + // while let Some(delivery) = consumer.next().await { + // tracing::debug!(message=?delivery, "received message"); + // if let Ok(delivery) = delivery { + // delivery + // .ack(BasicAckOptions::default()) + // .await + // .expect("basic_ack"); + // } + // } + + + // on_complete() + // let deployment = crate::models::deployment::Deployment { + // id: 0, + // project_id: 0, + // deleted: false, + // status: "".to_string(), + // body: Default::default(), + // created_at: Default::default(), + // updated_at: Default::default(), + // }; + // deployment::update(db_pool.get_ref(), deployment).await?; + + Ok(()) + }) + } +} diff --git a/src/console/commands/mq/mod.rs b/src/console/commands/mq/mod.rs new file mode 100644 index 00000000..0d4c7ef8 --- /dev/null +++ b/src/console/commands/mq/mod.rs @@ -0,0 +1,2 @@ +mod listener; +pub use listener::*; \ No newline at end of file diff --git a/src/console/main.rs b/src/console/main.rs index 09211286..752e10ae 100644 --- a/src/console/main.rs +++ b/src/console/main.rs @@ -12,6 +12,10 @@ enum Commands { #[command(subcommand)] command: AppClientCommands, }, + MQ { + #[command(subcommand)] + command: AppMqCommands, + } } #[derive(Debug, Subcommand)] @@ -22,6 +26,12 @@ enum AppClientCommands { }, } +#[derive(Debug, Subcommand)] +enum AppMqCommands { + Listen { + }, +} + //todo add documentation about how to add a new command //todo the helper from console should have a nicer display @@ -38,6 +48,11 @@ fn get_command(cli: Cli) -> Result match command { + AppMqCommands::Listen {} => Ok(Box::new( + stacker::console::commands::mq::ListenCommand::new(), + )), + }, _ => Err("command does not match".to_string()), } } diff --git a/src/db/client.rs b/src/db/client.rs index b8307a72..8f13d9af 100644 --- a/src/db/client.rs +++ b/src/db/client.rs @@ -19,7 +19,7 @@ pub async fn update(pool: &PgPool, client: models::Client) -> Result Result, String> { + tracing::info!("Fetch cloud {}", id); + sqlx::query_as!( + models::Cloud, + r#"SELECT * FROM cloud WHERE id=$1 LIMIT 1 "#, id + ) + .fetch_one(pool) + .await + .map(|cloud| Some(cloud)) + .or_else(|err| match err { + sqlx::Error::RowNotFound => Ok(None), + e => { + tracing::error!("Failed to fetch cloud, error: {:?}", e); + Err("Could not fetch data".to_string()) + } + }) +} + +pub async fn fetch_by_user(pool: &PgPool, user_id: &str) -> Result, String> { + let query_span = tracing::info_span!("Fetch clouds by user id."); + sqlx::query_as!( + models::Cloud, + r#" + SELECT + * + FROM cloud + WHERE user_id=$1 + "#, + user_id + ) + .fetch_all(pool) + .instrument(query_span) + .await + .map_err(|err| { + tracing::error!("Failed to fetch cloud, error: {:?}", err); + "".to_string() + }) +} + + +pub async fn insert(pool: &PgPool, mut cloud: models::Cloud) -> Result { + let query_span = tracing::info_span!("Saving user's cloud data into the database"); + sqlx::query!( + r#" + INSERT INTO cloud ( + user_id, + project_id, + provider, + cloud_token, + cloud_key, + cloud_secret, + save_token, + created_at, + updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING id; + "#, + cloud.user_id, + cloud.project_id, + cloud.provider, + cloud.cloud_token, + cloud.cloud_key, + cloud.cloud_secret, + cloud.save_token, + cloud.created_at, + cloud.updated_at, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + cloud.id = result.id; + cloud + }) + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} + +pub async fn update(pool: &PgPool, mut cloud: models::Cloud) -> Result { + let query_span = tracing::info_span!("Updating user cloud"); + sqlx::query_as!( + models::Cloud, + r#" + UPDATE cloud + SET + user_id=$2, + project_id=$3, + provider=$4, + cloud_token=$5, + cloud_key=$6, + cloud_secret=$7, + save_token=$8, + updated_at=NOW() at time zone 'utc' + WHERE id = $1 + RETURNING * + "#, + cloud.id, + cloud.user_id, + cloud.project_id, + cloud.provider, + cloud.cloud_token, + cloud.cloud_key, + cloud.cloud_secret, + cloud.save_token + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|result|{ + tracing::info!("Cloud info {} have been saved", cloud.id); + cloud.updated_at = result.updated_at; + cloud + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "".to_string() + }) +} + +#[tracing::instrument(name = "Delete cloud of a user.")] +pub async fn delete(pool: &PgPool, id: i32) -> Result { + tracing::info!("Delete cloud {}", id); + let mut tx = match pool.begin().await { + Ok(result) => result, + Err(err) => { + tracing::error!("Failed to begin transaction: {:?}", err); + return Err("".to_string()); + } + }; + + let delete_query = " DELETE FROM cloud WHERE id = $1; "; + + match sqlx::query(delete_query) + .bind(id) + .execute(&mut tx) + .await + .map_err(|err| { + println!("{:?}", err) + }) + { + Ok(_) => { + let _ = tx.commit().await.map_err(|err| { + tracing::error!("Failed to commit transaction: {:?}", err); + false + }); + Ok(true) + } + Err(err) => { + let _ = tx.rollback().await.map_err(|err| println!("{:?}", err)); + Ok(false) + } + } + +} diff --git a/src/db/deployment.rs b/src/db/deployment.rs new file mode 100644 index 00000000..11e7a2e3 --- /dev/null +++ b/src/db/deployment.rs @@ -0,0 +1,66 @@ +use crate::models; +use sqlx::PgPool; +use tracing::Instrument; + +pub async fn insert(pool: &PgPool, mut deployment: models::Deployment) -> Result { + let query_span = tracing::info_span!("Saving new deployment into the database"); + sqlx::query!( + r#" + INSERT INTO deployment (project_id, deleted, status, body, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id; + "#, + deployment.project_id, + deployment.deleted, + deployment.status, + deployment.body, + deployment.created_at, + deployment.updated_at, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + deployment.id = result.id; + deployment + }) + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} + +pub async fn update(pool: &PgPool, mut deployment: models::Deployment) -> Result { + let query_span = tracing::info_span!("Updating user deployment into the database"); + sqlx::query_as!( + models::Deployment, + r#" + UPDATE deployment + SET + project_id=$2, + deleted=$3, + status=$4, + body=$5, + updated_at=NOW() at time zone 'utc' + WHERE id = $1 + RETURNING * + "#, + deployment.id, + deployment.project_id, + deployment.deleted, + deployment.status, + deployment.body, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|result|{ + tracing::info!("Deployment {} has been updated", deployment.id); + deployment.updated_at = result.updated_at; + deployment + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "".to_string() + }) +} diff --git a/src/db/mod.rs b/src/db/mod.rs index a4fe3ae7..35853276 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,4 +1,7 @@ pub mod client; pub mod product; pub mod rating; -pub mod stack; +pub mod project; +pub(crate) mod deployment; +pub(crate) mod cloud; +pub(crate) mod server; diff --git a/src/db/project.rs b/src/db/project.rs new file mode 100644 index 00000000..d1f469f8 --- /dev/null +++ b/src/db/project.rs @@ -0,0 +1,183 @@ +use crate::models; +use sqlx::PgPool; +use tracing::Instrument; + +pub async fn fetch(pool: &PgPool, id: i32) -> Result, String> { + tracing::info!("Fetch project {}", id); + sqlx::query_as!( + models::Project, + r#" + SELECT + * + FROM project + WHERE id=$1 + LIMIT 1 + "#, + id + ) + .fetch_one(pool) + .await + .map(|project| Some(project)) + .or_else(|err| match err { + sqlx::Error::RowNotFound => Ok(None), + e => { + tracing::error!("Failed to fetch project, error: {:?}", e); + Err("Could not fetch data".to_string()) + } + }) +} + +pub async fn fetch_by_user(pool: &PgPool, user_id: &str) -> Result, String> { + let query_span = tracing::info_span!("Fetch projects by user id."); + sqlx::query_as!( + models::Project, + r#" + SELECT + * + FROM project + WHERE user_id=$1 + "#, + user_id + ) + .fetch_all(pool) + .instrument(query_span) + .await + .map_err(|err| { + tracing::error!("Failed to fetch project, error: {:?}", err); + "".to_string() + }) +} + +pub async fn fetch_one_by_name(pool: &PgPool, name: &str) -> Result, String> { + let query_span = tracing::info_span!("Fetch one project by name."); + sqlx::query_as!( + models::Project, + r#" + SELECT + * + FROM project + WHERE name=$1 + LIMIT 1 + "#, + name + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|project| Some(project)) + .or_else(|err| match err { + sqlx::Error::RowNotFound => Ok(None), + err => { + tracing::error!("Failed to fetch one project by name, error: {:?}", err); + Err("".to_string()) + } + }) +} + +pub async fn insert(pool: &PgPool, mut project: models::Project) -> Result { + let query_span = tracing::info_span!("Saving new project into the database"); + sqlx::query!( + r#" + INSERT INTO project (stack_id, user_id, name, body, created_at, updated_at, request_json) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id; + "#, + project.stack_id, + project.user_id, + project.name, + project.body, + project.created_at, + project.updated_at, + project.request_json, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + project.id = result.id; + project + }) + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} + +pub async fn update(pool: &PgPool, mut project: models::Project) -> Result { + let query_span = tracing::info_span!("Updating project"); + sqlx::query_as!( + models::Project, + r#" + UPDATE project + SET + stack_id=$2, + user_id=$3, + name=$4, + body=$5, + request_json=$6, + updated_at=NOW() at time zone 'utc' + WHERE id = $1 + RETURNING * + "#, + project.id, + project.stack_id, + project.user_id, + project.name, + project.body, + project.request_json + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|result|{ + tracing::info!("Project {} has been saved to database", project.id); + project.updated_at = result.updated_at; + project + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "".to_string() + }) +} + +#[tracing::instrument(name = "Delete user's project.")] +pub async fn delete(pool: &PgPool, id: i32) -> Result { + tracing::info!("Delete project {}", id); + let mut tx = match pool.begin().await { + Ok(result) => result, + Err(err) => { + tracing::error!("Failed to begin transaction: {:?}", err); + return Err("".to_string()); + } + }; + + // Combine delete queries into a single query + let delete_query = " + --DELETE FROM deployment WHERE project_id = $1; // on delete cascade + --DELETE FROM server WHERE project_id = $1; // on delete cascade + DELETE FROM project WHERE id = $1; + "; + + match sqlx::query(delete_query) + .bind(id) + .execute(&mut tx) + .await + .map_err(|err| { + println!("{:?}", err) + }) + { + Ok(_) => { + tx.commit().await.map_err(|err| { + tracing::error!("Failed to commit transaction: {:?}", err); + false + }); + Ok(true) + } + Err(err) => { + tx.rollback().await.map_err(|err| println!("{:?}", err)); + Ok(false) + } + // todo, when empty commit() + } +} + diff --git a/src/db/server.rs b/src/db/server.rs new file mode 100644 index 00000000..86f147b2 --- /dev/null +++ b/src/db/server.rs @@ -0,0 +1,191 @@ +use crate::models; +use sqlx::PgPool; +use tracing::Instrument; + +pub async fn fetch(pool: &PgPool, id: i32) -> Result, String> { + tracing::info!("Fetch server {}", id); + sqlx::query_as!( + models::Server, + r#"SELECT * FROM server WHERE id=$1 LIMIT 1 "#, id + ) + .fetch_one(pool) + .await + .map(|server| Some(server)) + .or_else(|err| match err { + sqlx::Error::RowNotFound => Ok(None), + e => { + tracing::error!("Failed to fetch server, error: {:?}", e); + Err("Could not fetch data".to_string()) + } + }) +} + +pub async fn fetch_by_user(pool: &PgPool, user_id: &str) -> Result, String> { + let query_span = tracing::info_span!("Fetch servers by user id."); + sqlx::query_as!( + models::Server, + r#" + SELECT + * + FROM server + WHERE user_id=$1 + "#, + user_id + ) + .fetch_all(pool) + .instrument(query_span) + .await + .map_err(|err| { + tracing::error!("Failed to fetch server, error: {:?}", err); + "".to_string() + }) +} + + +pub async fn fetch_by_project(pool: &PgPool, project_id: i32) -> Result, String> { + let query_span = tracing::info_span!("Fetch servers by project/project id."); + sqlx::query_as!( + models::Server, + r#" + SELECT + * + FROM server + WHERE project_id=$1 + "#, + project_id + ) + .fetch_all(pool) + .instrument(query_span) + .await + .map_err(|err| { + tracing::error!("Failed to fetch servers, error: {:?}", err); + "".to_string() + }) +} + + +pub async fn insert(pool: &PgPool, mut server: models::Server) -> Result { + let query_span = tracing::info_span!("Saving user's server data into the database"); + sqlx::query!( + r#" + INSERT INTO server ( + user_id, + project_id, + region, + zone, + server, + os, + disk_type, + created_at, + updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING id; + "#, + server.user_id, + server.project_id, + server.region, + server.zone, + server.server, + server.os, + server.disk_type, + server.created_at, + server.updated_at, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + server.id = result.id; + server + }) + .map_err(|e| { + + // match err { + // sqlx::error::ErrorKind::ForeignKeyViolation => { + // return JsonResponse::::build().bad_request(""); + // } + // _ => { + // return JsonResponse::::build().internal_server_error("Failed to insert"); + // } + // }) + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} + +pub async fn update(pool: &PgPool, mut server: models::Server) -> Result { + let query_span = tracing::info_span!("Updating user server"); + sqlx::query_as!( + models::Server, + r#" + UPDATE server + SET + user_id=$2, + project_id=$3, + region=$4, + zone=$5, + server=$6, + os=$7, + disk_type=$8, + updated_at=NOW() at time zone 'utc' + WHERE id = $1 + RETURNING * + "#, + server.id, + server.user_id, + server.project_id, + server.region, + server.zone, + server.server, + server.os, + server.disk_type, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|result|{ + tracing::info!("Server info {} have been saved", server.id); + server.updated_at = result.updated_at; + server + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "".to_string() + }) +} + +#[tracing::instrument(name = "Delete user's server.")] +pub async fn delete(pool: &PgPool, id: i32) -> Result { + tracing::info!("Delete server {}", id); + let mut tx = match pool.begin().await { + Ok(result) => result, + Err(err) => { + tracing::error!("Failed to begin transaction: {:?}", err); + return Err("".to_string()); + } + }; + + let delete_query = " DELETE FROM server WHERE id = $1; "; + + match sqlx::query(delete_query) + .bind(id) + .execute(&mut tx) + .await + .map_err(|err| { + println!("{:?}", err) + }) + { + Ok(_) => { + let _ = tx.commit().await.map_err(|err| { + tracing::error!("Failed to commit transaction: {:?}", err); + false + }); + Ok(true) + } + Err(err) => { + let _ = tx.rollback().await.map_err(|err| println!("{:?}", err)); + Ok(false) + } + } + +} diff --git a/src/db/stack.rs b/src/db/stack.rs deleted file mode 100644 index def7a367..00000000 --- a/src/db/stack.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::models; -use sqlx::PgPool; -use tracing::Instrument; - -pub async fn fetch(pool: &PgPool, id: i32) -> Result, String> { - tracing::info!("Fecth stack {}", id); - sqlx::query_as!( - models::Stack, - r#" - SELECT - * - FROM user_stack - WHERE id=$1 - LIMIT 1 - "#, - id - ) - .fetch_one(pool) - .await - .map(|stack| Some(stack)) - .or_else(|err| match err { - sqlx::Error::RowNotFound => Ok(None), - e => { - tracing::error!("Failed to fetch stack, error: {:?}", e); - Err("Could not fetch data".to_string()) - } - }) -} - -pub async fn fetch_by_user(pool: &PgPool, user_id: &str) -> Result, String> { - let query_span = tracing::info_span!("Fetch stacks by user id."); - sqlx::query_as!( - models::Stack, - r#" - SELECT - * - FROM user_stack - WHERE user_id=$1 - "#, - user_id - ) - .fetch_all(pool) - .instrument(query_span) - .await - .map_err(|err| { - tracing::error!("Failed to fetch stack, error: {:?}", err); - "".to_string() - }) -} - -pub async fn fetch_one_by_name(pool: &PgPool, name: &str) -> Result, String> { - let query_span = tracing::info_span!("Fetch one stack by name."); - sqlx::query_as!( - models::Stack, - r#" - SELECT - * - FROM user_stack - WHERE name=$1 - LIMIT 1 - "#, - name - ) - .fetch_one(pool) - .instrument(query_span) - .await - .map(|stack| Some(stack)) - .or_else(|err| match err { - sqlx::Error::RowNotFound => Ok(None), - err => { - tracing::error!("Failed to fetch one stack by name, error: {:?}", err); - Err("".to_string()) - } - }) -} - -pub async fn insert(pool: &PgPool, mut stack: models::Stack) -> Result { - let query_span = tracing::info_span!("Saving new stack into the database"); - sqlx::query!( - r#" - INSERT INTO user_stack (stack_id, user_id, name, body, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING id; - "#, - stack.stack_id, - stack.user_id, - stack.name, - stack.body, - stack.created_at, - stack.updated_at, - ) - .fetch_one(pool) - .instrument(query_span) - .await - .map(move |result| { - stack.id = result.id; - stack - }) - .map_err(|e| { - tracing::error!("Failed to execute query: {:?}", e); - "Failed to insert".to_string() - }) -} - -pub async fn update(pool: &PgPool, mut stack: models::Stack) -> Result { - let query_span = tracing::info_span!("Updating user stack into the database"); - sqlx::query_as!( - models::Stack, - r#" - UPDATE user_stack - SET - stack_id=$2, - user_id=$3, - name=$4, - body=$5, - created_at=$6, - updated_at=NOW() at time zone 'utc' - WHERE id = $1 - RETURNING * - "#, - stack.id, - stack.stack_id, - stack.user_id, - stack.name, - stack.body, - stack.created_at, - ) - .fetch_one(pool) - .instrument(query_span) - .await - .map(|result|{ - tracing::info!("Stack {} have been saved to database", stack.id); - stack.updated_at = result.updated_at; - stack - }) - .map_err(|err| { - tracing::error!("Failed to execute query: {:?}", err); - "".to_string() - }) -} diff --git a/src/forms/cloud.rs b/src/forms/cloud.rs new file mode 100644 index 00000000..138b2989 --- /dev/null +++ b/src/forms/cloud.rs @@ -0,0 +1,43 @@ +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; +use chrono::Utc; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Cloud { + pub project_id: Option, + #[validate(min_length = 2)] + #[validate(max_length = 50)] + pub provider: String, + pub cloud_token: Option, + pub cloud_key: Option, + pub cloud_secret: Option, + pub save_token: Option, +} + +impl Into for Cloud { + fn into(self) -> models::Cloud { + let mut cloud = models::Cloud::default(); + cloud.provider = self.provider; + cloud.cloud_token = self.cloud_token; + cloud.cloud_key = self.cloud_key; + cloud.cloud_secret = self.cloud_secret; + cloud.save_token = self.save_token; + + cloud + } +} + +impl Into for models::Cloud { + fn into(self) -> Cloud { + let mut form = Cloud::default(); + form.project_id = self.project_id; + form.provider = self.provider; + form.cloud_token = self.cloud_token; + form.cloud_key = self.cloud_key; + form.cloud_secret = self.cloud_secret; + form.save_token = self.save_token; + + form + } +} diff --git a/src/forms/mod.rs b/src/forms/mod.rs index 9647ea6e..f5fcc9cb 100644 --- a/src/forms/mod.rs +++ b/src/forms/mod.rs @@ -1,5 +1,9 @@ mod rating; -pub mod stack; +pub mod project; pub mod user; +pub(crate) mod cloud; +pub(crate) mod server; pub use rating::*; +pub use cloud::*; +pub use server::*; diff --git a/src/forms/stack/app.rs b/src/forms/project/app.rs similarity index 87% rename from src/forms/stack/app.rs rename to src/forms/project/app.rs index 93529a37..64598e61 100644 --- a/src/forms/stack/app.rs +++ b/src/forms/project/app.rs @@ -4,8 +4,8 @@ use indexmap::IndexMap; use serde_json::Value; use serde::{Deserialize, Serialize}; use serde_valid::Validate; -use crate::forms::stack::network::Network; -use crate::forms::stack::{DockerImage, replace_id_with_name}; +use crate::forms::project::network::Network; +use crate::forms::project::{DockerImage, replace_id_with_name}; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct App { @@ -30,15 +30,15 @@ pub struct App { #[serde(rename = "type")] pub type_field: String, #[serde(flatten)] - pub role: forms::stack::Role, + pub role: forms::project::Role, pub default: Option, - pub versions: Option>, + pub versions: Option>, #[serde(flatten)] #[validate] pub docker_image: DockerImage, #[serde(flatten)] #[validate] - pub requirements: forms::stack::Requirements, + pub requirements: forms::project::Requirements, #[validate(minimum = 1)] pub popularity: Option, pub commercial: Option, @@ -47,8 +47,8 @@ pub struct App { pub suggested: Option, pub dependency: Option, pub avoid_render: Option, - pub price: Option, - pub icon: Option, + pub price: Option, + pub icon: Option, pub domain: Option, pub category_id: Option, pub parent_app_id: Option, @@ -63,13 +63,13 @@ pub struct App { pub url_git: Option, #[validate(enumerate("always", "no", "unless-stopped", "on-failure"))] pub restart: String, - pub volumes: Option>, + pub volumes: Option>, #[serde(flatten)] - pub environment: forms::stack::Environment, + pub environment: forms::project::Environment, #[serde(flatten)] - pub network: forms::stack::ServiceNetworks, + pub network: forms::project::ServiceNetworks, #[validate] - pub shared_ports: Option>, + pub shared_ports: Option>, } impl App { @@ -151,8 +151,8 @@ impl App { } } -impl AsRef for App { - fn as_ref(&self) -> &forms::stack::DockerImage { +impl AsRef for App { + fn as_ref(&self) -> &forms::project::DockerImage { &self.docker_image } } diff --git a/src/forms/stack/compose_networks.rs b/src/forms/project/compose_networks.rs similarity index 94% rename from src/forms/stack/compose_networks.rs rename to src/forms/project/compose_networks.rs index afbd6f81..bb45a365 100644 --- a/src/forms/stack/compose_networks.rs +++ b/src/forms/project/compose_networks.rs @@ -1,8 +1,8 @@ use serde::{Deserialize, Serialize}; use docker_compose_types as dctypes; use indexmap::IndexMap; -use crate::forms::stack; -use crate::forms::stack::network::Network; +use crate::forms::project; +use crate::forms::project::network::Network; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ComposeNetworks { diff --git a/src/forms/stack/custom.rs b/src/forms/project/custom.rs similarity index 88% rename from src/forms/stack/custom.rs rename to src/forms/project/custom.rs index 4e6bd871..0a4eac7e 100644 --- a/src/forms/stack/custom.rs +++ b/src/forms/project/custom.rs @@ -2,20 +2,17 @@ use serde::{Deserialize, Serialize}; use crate::forms; use indexmap::IndexMap; use docker_compose_types as dctypes; -use crate::forms::stack::Network; +use crate::forms::project::Network; use serde_valid::Validate; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct Custom { #[validate] - pub web: Vec, + pub web: Vec, #[validate] - pub feature: Option>, + pub feature: Option>, #[validate] - pub service: Option>, - #[validate(minimum = 0)] - #[validate(maximum = 10)] - pub servers_count: u32, + pub service: Option>, #[validate(min_length = 3)] #[validate(max_length = 50)] pub custom_stack_code: String, @@ -25,13 +22,13 @@ pub struct Custom { pub custom_stack_category: Option>, pub custom_stack_short_description: Option, pub custom_stack_description: Option, - #[validate(min_length = 3)] - #[validate(max_length = 255)] - pub project_name: String, + // #[validate(min_length = 3)] + // #[validate(max_length = 255)] + pub project_name: Option, pub project_overview: Option, pub project_description: Option, #[serde(flatten)] - pub networks: forms::stack::ComposeNetworks, // all networks + pub networks: forms::project::ComposeNetworks, // all networks } diff --git a/src/forms/project/deploy.rs b/src/forms/project/deploy.rs new file mode 100644 index 00000000..ae4c5e8e --- /dev/null +++ b/src/forms/project/deploy.rs @@ -0,0 +1,27 @@ +use serde_derive::{Deserialize, Serialize}; +use serde_json::Value; +use serde_valid::Validate; +use crate::forms; +use crate::forms::{Cloud, Server}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Deploy { + #[validate] + pub(crate) stack: Stack, + #[validate] + pub(crate) server: Server, + #[validate] + pub(crate) cloud: Cloud +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Stack { + #[validate(min_length = 2)] + #[validate(max_length = 255)] + pub stack_code: Option, + pub vars: Option>, + pub integrated_features: Option>, + pub extended_features: Option>, + pub subscriptions: Option>, + pub form_app: Option>, +} \ No newline at end of file diff --git a/src/forms/stack/docker_image.rs b/src/forms/project/docker_image.rs similarity index 100% rename from src/forms/stack/docker_image.rs rename to src/forms/project/docker_image.rs diff --git a/src/forms/stack/domain_list.rs b/src/forms/project/domain_list.rs similarity index 100% rename from src/forms/stack/domain_list.rs rename to src/forms/project/domain_list.rs diff --git a/src/forms/stack/environment.rs b/src/forms/project/environment.rs similarity index 100% rename from src/forms/stack/environment.rs rename to src/forms/project/environment.rs diff --git a/src/forms/stack/feature.rs b/src/forms/project/feature.rs similarity index 86% rename from src/forms/stack/feature.rs rename to src/forms/project/feature.rs index 147aa033..d5405725 100644 --- a/src/forms/stack/feature.rs +++ b/src/forms/project/feature.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_valid::Validate; -use crate::forms::stack::*; +use crate::forms::project::*; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct Feature { @@ -10,4 +10,5 @@ pub struct Feature { // pub shared_ports: Option>, #[serde(flatten)] pub app: App, + pub custom: Option, } diff --git a/src/forms/project/form.rs b/src/forms/project/form.rs new file mode 100644 index 00000000..d2498cf4 --- /dev/null +++ b/src/forms/project/form.rs @@ -0,0 +1,77 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use serde_valid::Validate; +use actix_web::Error; +use actix_web::web::Bytes; +use crate::models; +use crate::forms; +use crate::helpers::JsonResponse; +use std::str; + + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct ProjectForm { + pub custom: forms::project::Custom +} + +impl TryFrom<&models::Project> for ProjectForm { + type Error = String; + + fn try_from(project: &models::Project) -> Result { + serde_json::from_value::(project.body.clone()).map_err(|err| format!("{:?}", err)) + } +} + +impl ProjectForm { + pub async fn is_readable_docker_image(&self) -> Result { + let mut is_active = true; + for app in &self.custom.web { + if !app.app.docker_image.is_active().await? { + is_active = false; + break; + } + } + + if let Some(service) = &self.custom.service { + for app in service { + if !app.app.docker_image.is_active().await? { + is_active = false; + break; + } + } + } + + if let Some(features) = &self.custom.feature { + for app in features { + if !app.app.docker_image.is_active().await? { + is_active = false; + break; + } + } + } + Ok(is_active) + } +} + +pub(crate) async fn body_into_form(body: Bytes) -> actix_web::Result { + let body_bytes = actix_web::body::to_bytes(body).await.unwrap(); + let body_str = str::from_utf8(&body_bytes) + .map_err(|err| JsonResponse::::build().internal_server_error(err.to_string()))?; + let deserializer = &mut serde_json::Deserializer::from_str(body_str); + serde_path_to_error::deserialize(deserializer) + .map_err(|err| { + let msg = format!("{}:{:?}", err.path().to_string(), err); + JsonResponse::::build().bad_request(msg) + }) + .and_then(|mut form: forms::project::ProjectForm| { + if !form.validate().is_ok() { + let errors = form.validate().unwrap_err().to_string(); + let err_msg = format!("Invalid data received {:?}", &errors); + tracing::debug!(err_msg); + + return Err(JsonResponse::::build().form_error(errors)); + } + + Ok(form) + }) +} diff --git a/src/forms/stack/icon.rs b/src/forms/project/icon.rs similarity index 85% rename from src/forms/stack/icon.rs rename to src/forms/project/icon.rs index 0bcae010..2f1c83c1 100644 --- a/src/forms/stack/icon.rs +++ b/src/forms/project/icon.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::forms::stack::*; +use crate::forms::project::*; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Icon { diff --git a/src/forms/stack/icon_dark.rs b/src/forms/project/icon_dark.rs similarity index 51% rename from src/forms/stack/icon_dark.rs rename to src/forms/project/icon_dark.rs index 73c543e4..d488f6a6 100644 --- a/src/forms/stack/icon_dark.rs +++ b/src/forms/project/icon_dark.rs @@ -1,4 +1,8 @@ use serde::{Deserialize, Serialize}; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct IconDark {} +pub struct IconDark { + width: Option, + height: Option, + image: Option +} diff --git a/src/forms/stack/icon_light.rs b/src/forms/project/icon_light.rs similarity index 59% rename from src/forms/stack/icon_light.rs rename to src/forms/project/icon_light.rs index 8e9e4b81..90b2c6af 100644 --- a/src/forms/stack/icon_light.rs +++ b/src/forms/project/icon_light.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct IconLight { - pub width: i64, - pub height: i64, - pub image: String, + pub width: Option, + pub height: Option, + pub image: Option, } diff --git a/src/forms/stack/mod.rs b/src/forms/project/mod.rs similarity index 94% rename from src/forms/stack/mod.rs rename to src/forms/project/mod.rs index 29799fb1..d83fecca 100644 --- a/src/forms/stack/mod.rs +++ b/src/forms/project/mod.rs @@ -1,6 +1,6 @@ mod app; mod custom; -mod form; +pub(crate) mod form; mod port; mod payload; mod volumes; @@ -24,6 +24,7 @@ mod icon_dark; mod version; mod network_driver; +mod deploy; pub use app::*; pub use custom::*; @@ -50,3 +51,4 @@ pub use icon::*; pub use icon_light::*; pub use icon_dark::*; pub use version::*; +pub use deploy::*; \ No newline at end of file diff --git a/src/forms/stack/network.rs b/src/forms/project/network.rs similarity index 96% rename from src/forms/stack/network.rs rename to src/forms/project/network.rs index a05fd540..9b848ff9 100644 --- a/src/forms/stack/network.rs +++ b/src/forms/project/network.rs @@ -1,9 +1,9 @@ use serde::{Deserialize, Serialize}; use serde_valid::Validate; -use crate::forms::stack; +use crate::forms::project; use docker_compose_types as dctypes; use indexmap::IndexMap; -use crate::forms::stack::NetworkDriver; +use crate::forms::project::NetworkDriver; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] diff --git a/src/forms/stack/network_driver.rs b/src/forms/project/network_driver.rs similarity index 100% rename from src/forms/stack/network_driver.rs rename to src/forms/project/network_driver.rs diff --git a/src/forms/project/payload.rs b/src/forms/project/payload.rs new file mode 100644 index 00000000..7108a1f5 --- /dev/null +++ b/src/forms/project/payload.rs @@ -0,0 +1,38 @@ +use std::convert::TryFrom; +use crate::models; +use crate::forms; +use serde_json::Value; +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +#[serde(rename_all = "snake_case")] +pub struct Payload { + pub(crate) id: Option, + pub(crate) user_token: Option, + pub(crate) user_email: Option, + #[serde(flatten)] + pub cloud: Option, + #[serde(flatten)] + pub server: Option, + #[serde(flatten)] + pub stack: forms::project::Stack, + pub custom: forms::project::Custom, + pub docker_compose: Option>, +} + +impl TryFrom<&models::Project> for Payload { + type Error = String; + + fn try_from(project: &models::Project) -> Result { + // tracing::debug!("project body: {:?}", project.body.clone()); + let mut project_data = serde_json::from_value::(project.body.clone()) + .map_err(|err| { + format!("{:?}", err) + })?; + + project_data.id = Some(project.id.clone()); + + Ok(project_data) + } +} diff --git a/src/forms/stack/port.rs b/src/forms/project/port.rs similarity index 100% rename from src/forms/stack/port.rs rename to src/forms/project/port.rs diff --git a/src/forms/stack/price.rs b/src/forms/project/price.rs similarity index 100% rename from src/forms/stack/price.rs rename to src/forms/project/price.rs diff --git a/src/forms/stack/requirements.rs b/src/forms/project/requirements.rs similarity index 100% rename from src/forms/stack/requirements.rs rename to src/forms/project/requirements.rs diff --git a/src/forms/stack/role.rs b/src/forms/project/role.rs similarity index 100% rename from src/forms/stack/role.rs rename to src/forms/project/role.rs diff --git a/src/forms/stack/service.rs b/src/forms/project/service.rs similarity index 87% rename from src/forms/stack/service.rs rename to src/forms/project/service.rs index 1470233e..706e0be2 100644 --- a/src/forms/stack/service.rs +++ b/src/forms/project/service.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_valid::Validate; -use crate::forms::stack::*; +use crate::forms::project::*; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct Service { @@ -10,4 +10,5 @@ pub struct Service { // pub shared_ports: Option>, #[serde(flatten)] pub(crate) app: App, + pub custom: Option, } diff --git a/src/forms/stack/service_networks.rs b/src/forms/project/service_networks.rs similarity index 98% rename from src/forms/stack/service_networks.rs rename to src/forms/project/service_networks.rs index 17b51c53..39f03b08 100644 --- a/src/forms/stack/service_networks.rs +++ b/src/forms/project/service_networks.rs @@ -24,7 +24,7 @@ impl TryFrom<&ServiceNetworks> for dctypes::Networks { // IndexMap // -// impl Into>> for stack::ComposeNetworks { +// impl Into>> for project::ComposeNetworks { // fn into(self) -> IndexMap> { // // // let mut default_networks = vec![Network::default()]; diff --git a/src/forms/stack/var.rs b/src/forms/project/var.rs similarity index 100% rename from src/forms/stack/var.rs rename to src/forms/project/var.rs diff --git a/src/forms/stack/version.rs b/src/forms/project/version.rs similarity index 100% rename from src/forms/stack/version.rs rename to src/forms/project/version.rs diff --git a/src/forms/stack/volume.rs b/src/forms/project/volume.rs similarity index 91% rename from src/forms/stack/volume.rs rename to src/forms/project/volume.rs index ab4d3f84..2b30a594 100644 --- a/src/forms/stack/volume.rs +++ b/src/forms/project/volume.rs @@ -4,8 +4,8 @@ use indexmap::IndexMap; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Volume { - pub(crate) host_path: Option, - pub(crate) container_path: Option, + pub host_path: Option, + pub container_path: Option, } impl Volume { @@ -58,8 +58,8 @@ impl Into for &Volume { // @todo check if host_path is required argument driver_opts.insert(String::from("type"), Some(dctypes::SingleValue::String("none".to_string()))); driver_opts.insert(String::from("o"), Some(dctypes::SingleValue::String("bind".to_string()))); - // @todo move to config stack docroot on host - let path = format!("/root/stack/{}", &host_path); + // @todo move to config project docroot on host + let path = format!("/root/project/{}", &host_path); driver_opts.insert(String::from("device"), Some(dctypes::SingleValue::String(path))); dctypes::ComposeVolume { diff --git a/src/forms/stack/volumes.rs b/src/forms/project/volumes.rs similarity index 83% rename from src/forms/stack/volumes.rs rename to src/forms/project/volumes.rs index 39ec33ea..27548a72 100644 --- a/src/forms/stack/volumes.rs +++ b/src/forms/project/volumes.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::forms::stack::*; +use crate::forms::project::*; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Volumes { diff --git a/src/forms/stack/web.rs b/src/forms/project/web.rs similarity index 88% rename from src/forms/stack/web.rs rename to src/forms/project/web.rs index 87e70cff..2d80cd51 100644 --- a/src/forms/stack/web.rs +++ b/src/forms/project/web.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_valid::Validate; -use crate::forms::stack::*; +use crate::forms::project::*; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] pub struct Web { diff --git a/src/forms/rating.rs b/src/forms/rating.rs index 04b9b6b7..e3f82810 100644 --- a/src/forms/rating.rs +++ b/src/forms/rating.rs @@ -17,7 +17,7 @@ impl Into for Rating { fn into(self) -> models::Rating { let mut rating = models::Rating::default(); rating.obj_id = self.obj_id; - rating.category = self.category.into(); //todo change the type of category field to the RateCategory + rating.category = self.category.into(); rating.hidden = Some(false); rating.rate = Some(self.rate); rating.comment = self.comment; diff --git a/src/forms/server.rs b/src/forms/server.rs new file mode 100644 index 00000000..1e55a895 --- /dev/null +++ b/src/forms/server.rs @@ -0,0 +1,46 @@ +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; +use chrono::{Utc}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Server { + // pub cloud_id: i32, + // pub project_id: i32, + pub region: String, + pub zone: Option, + pub server: String, + pub os: String, + pub disk_type: Option, +} + +impl Into for Server { + fn into(self) -> models::Server { + let mut server = models::Server::default(); + server.disk_type = self.disk_type; + server.region = self.region; + server.server = self.server; + server.zone = self.zone; + server.os = self.os; + server.created_at = Utc::now(); + server.updated_at = Utc::now(); + + server + } +} + +impl Into for models::Server { + + fn into(self) -> Server { + let mut form = Server::default(); + // form.cloud_id = self.cloud_id; + // form.project_id = self.project_id; + form.disk_type = self.disk_type; + form.region = self.region; + form.server = self.server; + form.zone = self.zone; + form.os = self.os; + + form + } +} diff --git a/src/forms/stack/form.rs b/src/forms/stack/form.rs deleted file mode 100644 index 3a9d8b70..00000000 --- a/src/forms/stack/form.rs +++ /dev/null @@ -1,94 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use serde_valid::Validate; -use std::collections::HashMap; -use std::fmt; -use crate::models; -use crate::forms; - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] -pub struct Stack { - // #[validate(min_length=2)] - // #[validate(max_length=255)] - #[serde(rename = "commonDomain")] - pub common_domain: Option, - pub domain_list: Option, - #[validate(min_length = 2)] - #[validate(max_length = 255)] - pub stack_code: Option, - #[validate(min_length = 2)] - #[validate(max_length = 50)] - pub region: String, - #[validate(min_length = 2)] - #[validate(max_length = 50)] - pub zone: Option, - #[validate(min_length = 2)] - #[validate(max_length = 50)] - pub server: String, - #[validate(min_length = 2)] - #[validate(max_length = 50)] - pub os: String, - #[validate(min_length = 3)] - #[validate(max_length = 50)] - pub ssl: String, - pub vars: Option>, - pub integrated_features: Option>, - pub extended_features: Option>, - pub subscriptions: Option>, - pub form_app: Option>, - #[validate(min_length = 3)] - #[validate(max_length = 50)] - pub disk_type: Option, - pub save_token: bool, - #[validate(min_length = 10)] - #[validate(max_length = 255)] - pub cloud_token: String, - #[validate(min_length = 2)] - #[validate(max_length = 50)] - pub provider: String, - #[validate(min_length = 3)] - #[validate(max_length = 50)] - pub selected_plan: String, - pub custom: forms::stack::Custom, -} - -impl TryFrom<&models::Stack> for Stack { - type Error = String; - - fn try_from(stack: &models::Stack) -> Result { - serde_json::from_value::(stack.body.clone()).map_err(|err| format!("{:?}", err)) - } -} - -impl Stack { - pub async fn is_readable_docker_image(&self) -> Result { - let mut is_active = true; - for app in &self.custom.web { - if !app.app.docker_image.is_active().await? { - is_active = false; - break; - } - } - - // temporarily disabled - // if let Some(service) = &self.custom.service { - // for app in service { - // if !app.app.docker_image.is_active().await? { - // is_active = false; - // break; - // } - // } - // } - // - // if let Some(features) = &self.custom.feature { - // for app in features { - // if !app.app.docker_image.is_active().await? { - // is_active = false; - // break; - // } - // } - // } - Ok(is_active) - } -} - diff --git a/src/forms/stack/payload.rs b/src/forms/stack/payload.rs deleted file mode 100644 index f89b3c6a..00000000 --- a/src/forms/stack/payload.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::convert::TryFrom; -use crate::models; -use crate::forms; -use serde_json::Value; -use serde::{Deserialize, Serialize}; -use serde_valid::Validate; - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] -#[serde(rename_all = "snake_case")] -pub struct Payload { - pub(crate) id: Option, - pub(crate) user_token: Option, - pub(crate) user_email: Option, - #[serde(rename = "commonDomain")] - pub common_domain: String, - pub domain_list: Option, - pub region: String, - pub zone: Option, - pub server: String, - pub os: String, - pub ssl: String, - pub vars: Option>, - #[serde(rename = "integrated_features")] - pub integrated_features: Option>, - #[serde(rename = "extended_features")] - pub extended_features: Option>, - pub subscriptions: Option>, - pub form_app: Option>, - pub disk_type: Option, - pub save_token: bool, - pub cloud_token: Option, - pub cloud_key: Option, - pub cloud_secret: Option, - pub provider: String, - pub stack_code: String, - #[serde(rename = "selected_plan")] - pub selected_plan: String, - pub custom: forms::stack::Custom, - pub docker_compose: Option>, -} - -impl TryFrom<&models::Stack> for Payload { - type Error = String; - - fn try_from(stack: &models::Stack) -> Result { - let mut stack_data = serde_json::from_value::(stack.body.clone()).map_err(|err| { - format!("{:?}", err) - })?; - - stack_data.id = Some(stack.id.clone()); - stack_data.stack_code = stack_data.custom.custom_stack_code.clone(); - - Ok(stack_data) - } -} diff --git a/src/helpers/dockerhub.rs b/src/helpers/dockerhub.rs index e3f17207..d5e4ad67 100644 --- a/src/helpers/dockerhub.rs +++ b/src/helpers/dockerhub.rs @@ -1,4 +1,4 @@ -use crate::forms::stack::DockerImage; +use crate::forms::project::DockerImage; use reqwest::RequestBuilder; use serde_derive::{Deserialize, Serialize}; use serde_json::Value; diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs index 17f75f10..0640df84 100644 --- a/src/helpers/mod.rs +++ b/src/helpers/mod.rs @@ -1,10 +1,10 @@ pub mod client; pub(crate) mod json; -mod mq_manager; -pub(crate) mod stack; +pub mod mq_manager; +pub mod project; pub use json::*; -pub use mq_manager::MqManager; +pub use mq_manager::*; pub mod dockerhub; pub(crate) mod compressor; diff --git a/src/helpers/mq_manager.rs b/src/helpers/mq_manager.rs index 5b058ea4..c2658251 100644 --- a/src/helpers/mq_manager.rs +++ b/src/helpers/mq_manager.rs @@ -1,9 +1,8 @@ +use actix_web::web; use deadpool_lapin::{Config, CreatePoolError, Object, Pool, Runtime}; -use lapin::{ - options::*, - publisher_confirm::{Confirmation, PublisherConfirm}, - BasicProperties, Channel, -}; +use lapin::{options::*, publisher_confirm::{Confirmation, PublisherConfirm}, BasicProperties, Channel, ExchangeKind, Queue}; +use lapin::types::AMQPType::ShortString; +use lapin::types::{AMQPValue, FieldTable}; use serde::ser::Serialize; #[derive(Debug)] @@ -33,8 +32,9 @@ impl MqManager { async fn get_connection(&self) -> Result { self.pool.get().await.map_err(|err| { - tracing::error!("getting connection from pool {:?}", err); - format!("getting connection from pool {:?}", err) + let msg = format!("getting connection from pool {:?}", err); + tracing::error!(msg); + msg }) } @@ -44,8 +44,9 @@ impl MqManager { .create_channel() .await .map_err(|err| { - tracing::error!("creating RabbitMQ channel {:?}", err); - format!("creating RabbitMQ channel {:?}", err) + let msg = format!("creating RabbitMQ channel {:?}", err); + tracing::error!(msg); + msg }) } @@ -85,15 +86,67 @@ impl MqManager { .await? .await .map_err(|err| { - tracing::error!("confirming the publication {:?}", err); - format!("confirming the publication {:?}", err) + let msg = format!("confirming the publication {:?}", err); + tracing::error!(msg); + msg + }) .and_then(|confirm| match confirm { Confirmation::NotRequested => { - tracing::error!("confirmation is NotRequested"); - Err(format!("confirmation is NotRequested")) + let msg = format!("confirmation is NotRequested"); + tracing::error!(msg); + Err(msg) } _ => Ok(()), }) } + + pub async fn consume( + &self, + exchange_name: &str, + routing_key: &str, + ) -> Result { + + let mut args = FieldTable::default(); + args.insert("x-expires".into(), AMQPValue::LongUInt(180000)); + let channel = self.create_channel().await?; + + channel + .exchange_declare( + exchange_name, + ExchangeKind::Topic, + ExchangeDeclareOptions { + passive: false, + durable: true, + auto_delete: false, + internal: false, + nowait: false, + }, + args + ) + .await + .expect("Exchange declare failed"); + + let queue = channel.queue_declare( + routing_key, + QueueDeclareOptions::default(), + Default::default(), + ) + .await + .expect("Queue declare failed"); + + let _ = channel + .queue_bind( + queue.name().as_str(), + exchange_name, + routing_key, + QueueBindOptions::default(), + FieldTable::default(), + ) + .await + .map_err(|err| format!("error {:?}", err)); + + let channel = self.create_channel().await?; + Ok(channel) + } } diff --git a/src/helpers/stack/builder.rs b/src/helpers/project/builder.rs similarity index 76% rename from src/helpers/stack/builder.rs rename to src/helpers/project/builder.rs index e17d3642..7112fe30 100644 --- a/src/helpers/stack/builder.rs +++ b/src/helpers/project/builder.rs @@ -2,7 +2,7 @@ use crate::forms; use docker_compose_types as dctypes; use crate::models; use serde_yaml; -use crate::helpers::stack::*; +use crate::helpers::project::*; use tracing::Value; @@ -10,29 +10,32 @@ use tracing::Value; #[derive(Clone, Debug)] pub struct DcBuilder { config: Config, - pub(crate) stack: models::Stack, + pub(crate) project: models::Project, } impl DcBuilder { - pub fn new(stack: models::Stack) -> Self { + pub fn new(project: models::Project) -> Self { DcBuilder { config: Config::default(), - stack, + project, } } - #[tracing::instrument(name = "building stack")] + #[tracing::instrument(name = "building project")] pub fn build(&self) -> Result { let mut compose_content = dctypes::Compose { version: Some("3.8".to_string()), ..Default::default() }; - let apps = forms::stack::Stack::try_from(&self.stack)?; + let apps = forms::project::ProjectForm::try_from(&self.project)?; + tracing::debug!("apps {:?}", &apps); let services = apps.custom.services()?; + tracing::debug!("services {:?}", &services); let named_volumes = apps.custom.named_volumes()?; + tracing::debug!("named volumes {:?}", &named_volumes); // let all_networks = &apps.custom.networks.networks.clone().unwrap_or(vec![]); let networks = apps.custom.networks.clone(); compose_content.networks = dctypes::ComposeNetworks(networks.into()); @@ -41,10 +44,9 @@ impl DcBuilder { compose_content.volumes = dctypes::TopLevelVolumes(named_volumes); } - tracing::debug!("services {:?}", &services); compose_content.services = dctypes::Services(services); - let fname = format!("./files/{}.yml", self.stack.stack_id); + let fname = format!("./files/{}.yml", self.project.stack_id); tracing::debug!("Saving docker compose to file {:?}", fname); let target_file = std::path::Path::new(fname.as_str()); let serialized = serde_yaml::to_string(&compose_content) diff --git a/src/helpers/stack/builder_config.rs b/src/helpers/project/builder_config.rs similarity index 100% rename from src/helpers/stack/builder_config.rs rename to src/helpers/project/builder_config.rs diff --git a/src/helpers/stack/mod.rs b/src/helpers/project/mod.rs similarity index 100% rename from src/helpers/stack/mod.rs rename to src/helpers/project/mod.rs diff --git a/src/helpers/stack/dctypes/advanced_build_step.rs b/src/helpers/stack/dctypes/advanced_build_step.rs deleted file mode 100644 index c0e6e060..00000000 --- a/src/helpers/stack/dctypes/advanced_build_step.rs +++ /dev/null @@ -1,23 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes::*; - -#[derive(Builder, Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)] -#[serde(deny_unknown_fields)] -#[builder(setter(into), default)] -pub struct AdvancedBuildStep { - pub context: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub dockerfile: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub args: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub shm_size: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub target: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub network: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub cache_from: Vec, - #[serde(default, skip_serializing_if = "Labels::is_empty")] - pub labels: Labels, -} diff --git a/src/helpers/stack/dctypes/advanced_network_settings.rs b/src/helpers/stack/dctypes/advanced_network_settings.rs deleted file mode 100644 index 730c9cb5..00000000 --- a/src/helpers/stack/dctypes/advanced_network_settings.rs +++ /dev/null @@ -1,12 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct AdvancedNetworkSettings { - #[serde(skip_serializing_if = "Option::is_none")] - pub ipv4_address: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ipv6_address: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub aliases: Vec, -} diff --git a/src/helpers/stack/dctypes/advanced_networks.rs b/src/helpers/stack/dctypes/advanced_networks.rs deleted file mode 100644 index d26aba1f..00000000 --- a/src/helpers/stack/dctypes/advanced_networks.rs +++ /dev/null @@ -1,13 +0,0 @@ -use serde::{Deserialize, Serialize}; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -#[cfg(not(feature = "indexmap"))] -use std::collections::HashMap; -use crate::helpers::stack::dctypes::*; - -#[cfg(feature = "indexmap")] -#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct AdvancedNetworks(pub IndexMap>); -#[cfg(not(feature = "indexmap"))] -#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct AdvancedNetworks(pub HashMap>); diff --git a/src/helpers/stack/dctypes/build_args.rs b/src/helpers/stack/dctypes/build_args.rs deleted file mode 100644 index 8f629662..00000000 --- a/src/helpers/stack/dctypes/build_args.rs +++ /dev/null @@ -1,16 +0,0 @@ -use serde::{Deserialize, Serialize}; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -#[cfg(not(feature = "indexmap"))] -use std::collections::HashMap; - -#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum BuildArgs { - Simple(String), - List(Vec), - #[cfg(feature = "indexmap")] - KvPair(IndexMap), - #[cfg(not(feature = "indexmap"))] - KvPair(HashMap), -} diff --git a/src/helpers/stack/dctypes/build_step.rs b/src/helpers/stack/dctypes/build_step.rs deleted file mode 100644 index 25a468d6..00000000 --- a/src/helpers/stack/dctypes/build_step.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes; - -#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum BuildStep { - Simple(String), - Advanced(dctypes::AdvancedBuildStep), -} diff --git a/src/helpers/stack/dctypes/compose.rs b/src/helpers/stack/dctypes/compose.rs deleted file mode 100644 index 46d43c27..00000000 --- a/src/helpers/stack/dctypes/compose.rs +++ /dev/null @@ -1,25 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes::*; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -use serde_yaml::Value; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] -pub struct Compose { - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, - #[serde(default, skip_serializing_if = "Services::is_empty")] - pub services: Services, - #[serde(default, skip_serializing_if = "TopLevelVolumes::is_empty")] - pub volumes: TopLevelVolumes, - #[serde(default, skip_serializing_if = "ComposeNetworks::is_empty")] - pub networks: ComposeNetworks, - #[serde(skip_serializing_if = "Option::is_none")] - pub service: Option, - #[cfg(feature = "indexmap")] - #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")] - pub extensions: IndexMap, - #[cfg(not(feature = "indexmap"))] - #[serde(flatten, skip_serializing_if = "HashMap::is_empty")] - pub extensions: HashMap, -} diff --git a/src/helpers/stack/dctypes/compose_file.rs b/src/helpers/stack/dctypes/compose_file.rs deleted file mode 100644 index 1131f675..00000000 --- a/src/helpers/stack/dctypes/compose_file.rs +++ /dev/null @@ -1,18 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes; - -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; - -#[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum ComposeFile { - V2Plus(dctypes::Compose), - #[cfg(feature = "indexmap")] - V1(IndexMap), - #[cfg(not(feature = "indexmap"))] - V1(HashMap), - Single(dctypes::SingleService), -} - diff --git a/src/helpers/stack/dctypes/compose_network.rs b/src/helpers/stack/dctypes/compose_network.rs deleted file mode 100644 index da364349..00000000 --- a/src/helpers/stack/dctypes/compose_network.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes::*; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum ComposeNetwork { - Detailed(ComposeNetworkSettingDetails), - Bool(bool), -} diff --git a/src/helpers/stack/dctypes/compose_network_setting_details.rs b/src/helpers/stack/dctypes/compose_network_setting_details.rs deleted file mode 100644 index 59d0348c..00000000 --- a/src/helpers/stack/dctypes/compose_network_setting_details.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct ComposeNetworkSettingDetails { - pub name: String, -} diff --git a/src/helpers/stack/dctypes/compose_networks.rs b/src/helpers/stack/dctypes/compose_networks.rs deleted file mode 100644 index f1374f0c..00000000 --- a/src/helpers/stack/dctypes/compose_networks.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -#[cfg(not(feature = "indexmap"))] -use std::collections::HashMap; -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes::*; - -#[cfg(feature = "indexmap")] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct ComposeNetworks(pub IndexMap>); -#[cfg(not(feature = "indexmap"))] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct ComposeNetworks(pub HashMap>); - -impl ComposeNetworks { - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} diff --git a/src/helpers/stack/dctypes/compose_volume.rs b/src/helpers/stack/dctypes/compose_volume.rs deleted file mode 100644 index 4d40b23f..00000000 --- a/src/helpers/stack/dctypes/compose_volume.rs +++ /dev/null @@ -1,25 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes::*; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -#[cfg(not(feature = "indexmap"))] -use std::collections::HashMap; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct ComposeVolume { - #[serde(skip_serializing_if = "Option::is_none")] - pub driver: Option, - #[cfg(feature = "indexmap")] - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub driver_opts: IndexMap>, - #[cfg(not(feature = "indexmap"))] - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub driver_opts: HashMap>, - #[serde(skip_serializing_if = "Option::is_none")] - pub external: Option, - #[serde(default, skip_serializing_if = "Labels::is_empty")] - pub labels: Labels, - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, -} - diff --git a/src/helpers/stack/dctypes/depends_condition.rs b/src/helpers/stack/dctypes/depends_condition.rs deleted file mode 100644 index 863c7b5c..00000000 --- a/src/helpers/stack/dctypes/depends_condition.rs +++ /dev/null @@ -1,6 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -pub struct DependsCondition { - pub condition: String, -} diff --git a/src/helpers/stack/dctypes/depends_on_options.rs b/src/helpers/stack/dctypes/depends_on_options.rs deleted file mode 100644 index 6e12300b..00000000 --- a/src/helpers/stack/dctypes/depends_on_options.rs +++ /dev/null @@ -1,30 +0,0 @@ -use serde::{Deserialize, Serialize}; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -use crate::helpers::stack::dctypes; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum DependsOnOptions { - Simple(Vec), - #[cfg(feature = "indexmap")] - Conditional(IndexMap), - #[cfg(not(feature = "indexmap"))] - Conditional(HashMap), -} - -impl Default for DependsOnOptions { - fn default() -> Self { - Self::Simple(Vec::new()) - } -} - -impl DependsOnOptions { - pub fn is_empty(&self) -> bool { - match self { - Self::Simple(v) => v.is_empty(), - Self::Conditional(m) => m.is_empty(), - } - } -} - diff --git a/src/helpers/stack/dctypes/env_file.rs b/src/helpers/stack/dctypes/env_file.rs deleted file mode 100644 index 643d6b99..00000000 --- a/src/helpers/stack/dctypes/env_file.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum EnvFile { - Simple(String), - List(Vec), -} - diff --git a/src/helpers/stack/dctypes/environment.rs b/src/helpers/stack/dctypes/environment.rs deleted file mode 100644 index 3afafed6..00000000 --- a/src/helpers/stack/dctypes/environment.rs +++ /dev/null @@ -1,30 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_yaml::Value; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -use crate::helpers::stack::dctypes; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum Environment { - List(Vec), - #[cfg(feature = "indexmap")] - KvPair(IndexMap>), - #[cfg(not(feature = "indexmap"))] - KvPair(HashMap>), -} - -impl Default for Environment { - fn default() -> Self { - Self::List(Vec::new()) - } -} - -impl Environment { - pub fn is_empty(&self) -> bool { - match self { - Self::List(v) => v.is_empty(), - Self::KvPair(m) => m.is_empty(), - } - } -} diff --git a/src/helpers/stack/dctypes/extension.rs b/src/helpers/stack/dctypes/extension.rs deleted file mode 100644 index f70871c5..00000000 --- a/src/helpers/stack/dctypes/extension.rs +++ /dev/null @@ -1,28 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes; -use std::str::FromStr; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default, Ord, PartialOrd)] -#[serde(try_from = "String")] -pub struct Extension(String); - -impl FromStr for Extension { - type Err = dctypes::ExtensionParseError; - - fn from_str(s: &str) -> Result { - let owned = s.to_owned(); - Extension::try_from(owned) - } -} - -impl TryFrom for Extension { - type Error = dctypes::ExtensionParseError; - - fn try_from(s: String) -> Result { - if s.starts_with("x-") { - Ok(Self(s)) - } else { - Err(dctypes::ExtensionParseError(s)) - } - } -} diff --git a/src/helpers/stack/dctypes/extension_parse_error.rs b/src/helpers/stack/dctypes/extension_parse_error.rs deleted file mode 100644 index 9fcec518..00000000 --- a/src/helpers/stack/dctypes/extension_parse_error.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::fmt; - -/// The result of a failed TryFrom conversion for [`Extension`] -/// -/// Contains the string that was being converted -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct ExtensionParseError(pub String); - -impl fmt::Display for ExtensionParseError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "unknown attribute {:?}, extensions must start with 'x-' (see https://docs.docker.com/compose/compose-file/#extension)", self.0) - } -} - -impl std::error::Error for ExtensionParseError {} diff --git a/src/helpers/stack/dctypes/external_network_setting_bool.rs b/src/helpers/stack/dctypes/external_network_setting_bool.rs deleted file mode 100644 index 1d1ad3d3..00000000 --- a/src/helpers/stack/dctypes/external_network_setting_bool.rs +++ /dev/null @@ -1,5 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct ExternalNetworkSettingBool(bool); diff --git a/src/helpers/stack/dctypes/external_volume.rs b/src/helpers/stack/dctypes/external_volume.rs deleted file mode 100644 index ba03e26b..00000000 --- a/src/helpers/stack/dctypes/external_volume.rs +++ /dev/null @@ -1,8 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum ExternalVolume { - Bool(bool), - Name { name: String }, -} diff --git a/src/helpers/stack/dctypes/ipam.rs b/src/helpers/stack/dctypes/ipam.rs deleted file mode 100644 index 592c2a6d..00000000 --- a/src/helpers/stack/dctypes/ipam.rs +++ /dev/null @@ -1,12 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes::*; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct Ipam { - #[serde(skip_serializing_if = "Option::is_none")] - pub driver: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub config: Vec, -} - diff --git a/src/helpers/stack/dctypes/ipam_config.rs b/src/helpers/stack/dctypes/ipam_config.rs deleted file mode 100644 index f40c2f59..00000000 --- a/src/helpers/stack/dctypes/ipam_config.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct IpamConfig { - pub subnet: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub gateway: Option, -} diff --git a/src/helpers/stack/dctypes/labels.rs b/src/helpers/stack/dctypes/labels.rs deleted file mode 100644 index e4296afa..00000000 --- a/src/helpers/stack/dctypes/labels.rs +++ /dev/null @@ -1,31 +0,0 @@ -use serde::{Deserialize, Serialize}; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -#[cfg(not(feature = "indexmap"))] -use std::collections::HashMap; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum Labels { - List(Vec), - #[cfg(feature = "indexmap")] - Map(IndexMap), - #[cfg(not(feature = "indexmap"))] - Map(HashMap), -} - -impl Default for Labels { - fn default() -> Self { - Self::List(Vec::new()) - } -} - -impl Labels { - pub fn is_empty(&self) -> bool { - match self { - Self::List(v) => v.is_empty(), - Self::Map(m) => m.is_empty(), - } - } -} - diff --git a/src/helpers/stack/dctypes/logging_parameters.rs b/src/helpers/stack/dctypes/logging_parameters.rs deleted file mode 100644 index bae66fcb..00000000 --- a/src/helpers/stack/dctypes/logging_parameters.rs +++ /dev/null @@ -1,15 +0,0 @@ -use serde::{Deserialize, Serialize}; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -use crate::helpers::stack::dctypes; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct LoggingParameters { - pub driver: String, - #[cfg(feature = "indexmap")] - #[serde(skip_serializing_if = "Option::is_none")] - pub options: Option>, - #[cfg(not(feature = "indexmap"))] - #[serde(skip_serializing_if = "Option::is_none")] - pub options: Option>, -} diff --git a/src/helpers/stack/dctypes/mod.rs b/src/helpers/stack/dctypes/mod.rs deleted file mode 100644 index 66bda096..00000000 --- a/src/helpers/stack/dctypes/mod.rs +++ /dev/null @@ -1,326 +0,0 @@ -mod port; -mod published_port; -mod compose_file; -mod single_service; -mod service; -mod sys_ctls; -mod compose; -mod env_file; -mod depends_on_options; -mod depends_condition; -mod logging_parameters; -mod ports; -mod environment; -mod extension; -mod extension_parse_error; -mod services; -mod labels; -mod tmpfs; -mod ulimit; -mod ulimits; -mod networks; -mod build_step; -mod advanced_build_step; -mod build_args; -mod advanced_networks; -mod advanced_network_settings; -mod top_level_volumes; -mod compose_volume; -mod external_volume; -mod compose_network; -mod compose_networks; -mod compose_network_setting_details; -mod external_network_setting_bool; -mod network_settings; -mod ipam; -mod ipam_config; - -pub use port::*; -pub use published_port::*; -pub use compose_file::*; -pub use single_service::*; -pub use service::*; -pub use sys_ctls::*; -pub use compose::*; -pub use env_file::*; -pub use depends_on_options::*; -pub use depends_condition::*; -pub use logging_parameters::*; -pub use ports::*; -pub use environment::*; -pub use extension::*; -pub use extension_parse_error::*; -pub use services::*; -pub use labels::*; -pub use tmpfs::*; -pub use ulimit::*; -pub use ulimits::*; -pub use networks::*; -pub use build_step::*; -pub use advanced_build_step::*; -pub use build_args::*; -pub use advanced_networks::*; -pub use advanced_network_settings::*; -pub use top_level_volumes::*; -pub use compose_volume::*; -pub use external_volume::*; -pub use compose_networks::*; -pub use compose_network::*; -pub use compose_network_setting_details::*; -pub use external_network_setting_bool::*; -pub use network_settings::*; -pub use ipam::*; -pub use ipam_config::*; - -use crate::helpers::stack::dctypes; - -use derive_builder::*; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -use serde::{Deserialize, Serialize}; -use serde_yaml::Value; -#[cfg(not(feature = "indexmap"))] -use std::collections::HashMap; -use std::convert::TryFrom; -use std::fmt; - - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] -#[serde(deny_unknown_fields)] -pub struct Deploy { - #[serde(skip_serializing_if = "Option::is_none")] - pub mode: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub replicas: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub labels: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub update_config: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub resources: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub restart_policy: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub placement: Option, -} - -fn is_zero(val: &i64) -> bool { - *val == 0 -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct Healthcheck { - #[serde(skip_serializing_if = "Option::is_none")] - pub test: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub interval: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub timeout: Option, - #[serde(default, skip_serializing_if = "is_zero")] - pub retries: i64, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_period: Option, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub disable: bool, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum HealthcheckTest { - Single(String), - Multiple(Vec), -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] -#[serde(deny_unknown_fields)] -pub struct Limits { - #[serde(skip_serializing_if = "Option::is_none")] - pub cpus: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub memory: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] -#[serde(deny_unknown_fields)] -pub struct Placement { - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub constraints: Vec, - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub preferences: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct Preferences { - pub spread: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] -#[serde(deny_unknown_fields)] -pub struct Resources { - pub limits: Option, - pub reservations: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] -#[serde(deny_unknown_fields)] -pub struct RestartPolicy { - #[serde(skip_serializing_if = "Option::is_none")] - pub condition: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub delay: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_attempts: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub window: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] -#[serde(deny_unknown_fields)] -pub struct UpdateConfig { - #[serde(skip_serializing_if = "Option::is_none")] - pub parallelism: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub delay: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub failure_action: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub monitor: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_failure_ratio: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum Volumes { - Simple(Vec), - Advanced(Vec), -} - -impl Default for Volumes { - fn default() -> Self { - Self::Simple(Vec::new()) - } -} - -impl Volumes { - pub fn is_empty(&self) -> bool { - match self { - Self::Simple(v) => v.is_empty(), - Self::Advanced(v) => v.is_empty(), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(deny_unknown_fields)] -pub struct AdvancedVolumes { - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - pub target: String, - #[serde(rename = "type")] - pub _type: String, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub read_only: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub bind: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub volume: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tmpfs: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] -#[serde(deny_unknown_fields)] -pub struct Bind { - pub propagation: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] -#[serde(deny_unknown_fields)] -pub struct Volume { - pub nocopy: bool, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] -#[serde(deny_unknown_fields)] -pub struct TmpfsSettings { - pub size: u64, -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum Command { - Simple(String), - Args(Vec), -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum Entrypoint { - Simple(String), - List(Vec), -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd)] -#[serde(untagged)] -pub enum SingleValue { - String(String), - Bool(bool), - Unsigned(u64), - Signed(i64), - Float(f64), -} - -impl fmt::Display for SingleValue { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::String(s) => f.write_str(s), - Self::Bool(b) => write!(f, "{b}"), - Self::Unsigned(u) => write!(f, "{u}"), - Self::Signed(i) => write!(f, "{i}"), - Self::Float(fl) => write!(f, "{fl}"), - } - } -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Hash)] -#[serde(untagged)] -pub enum MapOrEmpty { - Map(T), - Empty, -} - -impl Default for MapOrEmpty { - fn default() -> Self { - Self::Empty - } -} - -impl From> for Option { - fn from(value: MapOrEmpty) -> Self { - match value { - MapOrEmpty::Map(t) => Some(t), - MapOrEmpty::Empty => None, - } - } -} - -impl Serialize for MapOrEmpty - where - T: Serialize, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - Self::Map(t) => t.serialize(serializer), - Self::Empty => { - use serde::ser::SerializeMap; - serializer.serialize_map(None)?.end() - } - } - } -} diff --git a/src/helpers/stack/dctypes/network_settings.rs b/src/helpers/stack/dctypes/network_settings.rs deleted file mode 100644 index 9c45bc15..00000000 --- a/src/helpers/stack/dctypes/network_settings.rs +++ /dev/null @@ -1,29 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes::*; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] -#[serde(deny_unknown_fields)] -pub struct NetworkSettings { - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub attachable: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub driver: Option, - #[cfg(feature = "indexmap")] - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub driver_opts: IndexMap>, - #[cfg(not(feature = "indexmap"))] - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub driver_opts: HashMap>, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub enable_ipv6: bool, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub internal: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub external: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ipam: Option, - #[serde(default, skip_serializing_if = "Labels::is_empty")] - pub labels: Labels, - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, -} diff --git a/src/helpers/stack/dctypes/networks.rs b/src/helpers/stack/dctypes/networks.rs deleted file mode 100644 index bbcfdb14..00000000 --- a/src/helpers/stack/dctypes/networks.rs +++ /dev/null @@ -1,24 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum Networks { - Simple(Vec), - Advanced(dctypes::AdvancedNetworks), -} - -impl Default for Networks { - fn default() -> Self { - Self::Simple(Vec::new()) - } -} - -impl Networks { - pub fn is_empty(&self) -> bool { - match self { - Self::Simple(n) => n.is_empty(), - Self::Advanced(n) => n.0.is_empty(), - } - } -} diff --git a/src/helpers/stack/dctypes/port.rs b/src/helpers/stack/dctypes/port.rs deleted file mode 100644 index 25557581..00000000 --- a/src/helpers/stack/dctypes/port.rs +++ /dev/null @@ -1,53 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes; -use crate::forms; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct Port { - pub target: u16, - #[serde(skip_serializing_if = "Option::is_none")] - pub host_ip: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub published: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub protocol: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub mode: Option, -} - -impl Default for Port { - fn default() -> Self { - Port { - target: 80, - host_ip: None, - published: None, - protocol: None, - mode: None, - } - } -} - -impl TryInto for &forms::stack::Port { - type Error = String; - fn try_into(self) -> Result { - let cp = self - .container_port - .as_ref() - .map_or(Ok(0u16), |s| s.parse::()) - .map_err(|_| "Could not parse port".to_string())?; - - let hp = self - .host_port - .as_ref() - .map_or(Ok(0u16), |s| s.parse::()) - .map_err(|_| "Could not parse port".to_string())?; - - Ok(Port { - target: cp, - host_ip: None, - published: Some(dctypes::PublishedPort::Single(hp)), - protocol: None, - mode: None, - }) - } -} diff --git a/src/helpers/stack/dctypes/ports.rs b/src/helpers/stack/dctypes/ports.rs deleted file mode 100644 index bdcc6a81..00000000 --- a/src/helpers/stack/dctypes/ports.rs +++ /dev/null @@ -1,25 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum Ports { - Short(Vec), - Long(Vec), -} - -impl Default for Ports { - fn default() -> Self { - Self::Short(Vec::default()) - } -} - -impl Ports { - pub fn is_empty(&self) -> bool { - match self { - Self::Short(v) => v.is_empty(), - Self::Long(v) => v.is_empty(), - } - } -} - diff --git a/src/helpers/stack/dctypes/published_port.rs b/src/helpers/stack/dctypes/published_port.rs deleted file mode 100644 index 57a24ab3..00000000 --- a/src/helpers/stack/dctypes/published_port.rs +++ /dev/null @@ -1,8 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum PublishedPort { - Single(u16), - Range(String), -} diff --git a/src/helpers/stack/dctypes/service.rs b/src/helpers/stack/dctypes/service.rs deleted file mode 100644 index 4cebefcf..00000000 --- a/src/helpers/stack/dctypes/service.rs +++ /dev/null @@ -1,120 +0,0 @@ -use serde::{Deserialize, Serialize}; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -use crate::helpers::stack::dctypes; -use serde_json::Value; -use derive_builder::*; - -#[derive(Builder, Clone, Debug, Deserialize, Serialize, PartialEq, Default)] -#[builder(setter(into), default)] -pub struct Service { - #[serde(skip_serializing_if = "Option::is_none")] - pub hostname: Option, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub privileged: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub healthcheck: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub deploy: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub image: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub container_name: Option, - #[serde(skip_serializing_if = "Option::is_none", rename = "build")] - pub build_: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub pid: Option, - #[serde(default, skip_serializing_if = "dctypes::Ports::is_empty")] - pub ports: dctypes::Ports, - #[serde(default, skip_serializing_if = "dctypes::Environment::is_empty")] - pub environment: dctypes::Environment, - #[serde(skip_serializing_if = "Option::is_none")] - pub network_mode: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub devices: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub restart: Option, - #[serde(default, skip_serializing_if = "dctypes::Labels::is_empty")] - pub labels: dctypes::Labels, - #[serde(skip_serializing_if = "Option::is_none")] - pub tmpfs: Option, - #[serde(default, skip_serializing_if = "dctypes::Ulimits::is_empty")] - pub ulimits: dctypes::Ulimits, - #[serde(default, skip_serializing_if = "dctypes::Volumes::is_empty")] - pub volumes: dctypes::Volumes, - #[serde(default, skip_serializing_if = "dctypes::Networks::is_empty")] - pub networks: dctypes::Networks, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub cap_add: Vec, - #[serde(default, skip_serializing_if = "dctypes::DependsOnOptions::is_empty")] - pub depends_on: dctypes::DependsOnOptions, - #[serde(skip_serializing_if = "Option::is_none")] - pub command: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub entrypoint: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub env_file: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub stop_grace_period: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub profiles: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub links: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub dns: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub ipc: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub net: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub stop_signal: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub user: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub working_dir: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub expose: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub volumes_from: Vec, - #[cfg(feature = "indexmap")] - #[serde(default, skip_serializing_if = "IndexMap::is_empty")] - pub extends: IndexMap, - #[cfg(not(feature = "indexmap"))] - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub extends: HashMap, - #[serde(skip_serializing_if = "Option::is_none")] - pub logging: Option, - #[serde(default, skip_serializing_if = "dctypes::is_zero")] - pub scale: i64, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub init: bool, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub stdin_open: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub shm_size: Option, - #[cfg(feature = "indexmap")] - #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")] - pub extensions: IndexMap, - #[cfg(not(feature = "indexmap"))] - #[serde(flatten, skip_serializing_if = "HashMap::is_empty")] - pub extensions: HashMap, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub extra_hosts: Vec, - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub tty: bool, - #[serde(default, skip_serializing_if = "dctypes::SysCtls::is_empty")] - pub sysctls: dctypes::SysCtls, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub security_opt: Vec, -} - -impl Service { - pub fn image(&self) -> &str { - self.image.as_deref().unwrap_or_default() - } - - pub fn network_mode(&self) -> &str { - self.network_mode.as_deref().unwrap_or_default() - } -} - diff --git a/src/helpers/stack/dctypes/services.rs b/src/helpers/stack/dctypes/services.rs deleted file mode 100644 index b41ff10d..00000000 --- a/src/helpers/stack/dctypes/services.rs +++ /dev/null @@ -1,19 +0,0 @@ -use serde::{Deserialize, Serialize}; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -#[cfg(not(feature = "indexmap"))] -use std::collections::HashMap; -use crate::helpers::stack::dctypes; - -#[cfg(feature = "indexmap")] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct Services(pub IndexMap>); -#[cfg(not(feature = "indexmap"))] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct Services(pub HashMap>); - -impl Services { - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} diff --git a/src/helpers/stack/dctypes/single_service.rs b/src/helpers/stack/dctypes/single_service.rs deleted file mode 100644 index 489a3063..00000000 --- a/src/helpers/stack/dctypes/single_service.rs +++ /dev/null @@ -1,8 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] -pub struct SingleService { - pub service: dctypes::Service, -} - diff --git a/src/helpers/stack/dctypes/sys_ctls.rs b/src/helpers/stack/dctypes/sys_ctls.rs deleted file mode 100644 index 45d0a7c3..00000000 --- a/src/helpers/stack/dctypes/sys_ctls.rs +++ /dev/null @@ -1,30 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::helpers::stack::dctypes; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(untagged)] -pub enum SysCtls { - List(Vec), - #[cfg(feature = "indexmap")] - Map(IndexMap>), - #[cfg(not(feature = "indexmap"))] - Map(HashMap>), -} - -impl Default for SysCtls { - fn default() -> Self { - Self::List(Vec::new()) - } -} - -impl SysCtls { - pub fn is_empty(&self) -> bool { - match self { - Self::List(v) => v.is_empty(), - Self::Map(m) => m.is_empty(), - } - } -} - diff --git a/src/helpers/stack/dctypes/tmpfs.rs b/src/helpers/stack/dctypes/tmpfs.rs deleted file mode 100644 index 1bea9b6e..00000000 --- a/src/helpers/stack/dctypes/tmpfs.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum Tmpfs { - Simple(String), - List(Vec), -} - diff --git a/src/helpers/stack/dctypes/top_level_volumes.rs b/src/helpers/stack/dctypes/top_level_volumes.rs deleted file mode 100644 index 7216953d..00000000 --- a/src/helpers/stack/dctypes/top_level_volumes.rs +++ /dev/null @@ -1,19 +0,0 @@ -use serde::{Deserialize, Serialize}; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -#[cfg(not(feature = "indexmap"))] -use std::collections::HashMap; -use crate::helpers::stack::dctypes::*; - -#[cfg(feature = "indexmap")] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct TopLevelVolumes(pub IndexMap>); -#[cfg(not(feature = "indexmap"))] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct TopLevelVolumes(pub HashMap>); - -impl TopLevelVolumes { - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} diff --git a/src/helpers/stack/dctypes/ulimit.rs b/src/helpers/stack/dctypes/ulimit.rs deleted file mode 100644 index 65899310..00000000 --- a/src/helpers/stack/dctypes/ulimit.rs +++ /dev/null @@ -1,8 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(untagged)] -pub enum Ulimit { - Single(i64), - SoftHard { soft: i64, hard: i64 }, -} diff --git a/src/helpers/stack/dctypes/ulimits.rs b/src/helpers/stack/dctypes/ulimits.rs deleted file mode 100644 index 721eaf96..00000000 --- a/src/helpers/stack/dctypes/ulimits.rs +++ /dev/null @@ -1,20 +0,0 @@ -use serde::{Deserialize, Serialize}; -#[cfg(feature = "indexmap")] -use indexmap::IndexMap; -#[cfg(not(feature = "indexmap"))] -use std::collections::HashMap; -use crate::helpers::stack::dctypes; - -#[cfg(feature = "indexmap")] -#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct Ulimits(pub IndexMap); -#[cfg(not(feature = "indexmap"))] -#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct Ulimits(pub HashMap); - -impl Ulimits { - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - diff --git a/src/models/cloud.rs b/src/models/cloud.rs new file mode 100644 index 00000000..91b494c1 --- /dev/null +++ b/src/models/cloud.rs @@ -0,0 +1,16 @@ +use chrono::{DateTime, Utc}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Cloud { + pub id: i32, + pub user_id: String, + pub project_id: Option, + pub provider: String, + pub cloud_token: Option, + pub cloud_key: Option, + pub cloud_secret: Option, + pub save_token: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} diff --git a/src/models/deployment.rs b/src/models/deployment.rs new file mode 100644 index 00000000..bfdd72b0 --- /dev/null +++ b/src/models/deployment.rs @@ -0,0 +1,38 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +// Store user deployment attempts for a specific project +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Deployment { + pub id: i32, // id - is a unique identifier for the app project + pub project_id: i32, // external project ID + pub deleted: Option, + pub status: String, + pub body: Value, //json type + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl Deployment { + pub fn new(project_id: i32, status: String, body: Value) -> Self { + Self { + id: 0, + project_id, + deleted: Some(false), + status, + body, + created_at: Utc::now(), + updated_at: Utc::now(), + } + } +} + +impl Default for Deployment { + fn default() -> Self { + Deployment { + status: "pending".to_string(), + ..Default::default() + } + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index a17ea327..c1c375bd 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -3,13 +3,19 @@ mod product; mod ratecategory; mod rules; pub mod rating; -pub mod stack; +pub mod project; pub mod user; +pub(crate) mod deployment; +mod cloud; +mod server; pub use client::*; pub use rating::*; -pub use stack::*; +pub use project::*; pub use user::*; pub use product::*; pub use ratecategory::*; pub use rules::*; +pub use deployment::*; +pub use cloud::*; +pub use server::*; diff --git a/src/models/product.rs b/src/models/product.rs index 992818db..8fde4f3d 100644 --- a/src/models/product.rs +++ b/src/models/product.rs @@ -2,15 +2,15 @@ use chrono::{DateTime, Utc}; pub struct Product { // Product - is an external object that we want to store in the database, - // that can be a stack or an app in the stack. feature, service, web app etc. + // that can be a project or an app in the project. feature, service, web app etc. // id - is a unique identifier for the product // user_id - is a unique identifier for the user // rating - is a rating of the product - // product type stack & app, + // product type project & app, // id is generated based on the product type and external obj_id pub id: i32, //primary key, for better data management pub obj_id: i32, // external product ID db, no autoincrement, example: 100 - pub obj_type: String, // stack | app, unique index + pub obj_type: String, // project | app, unique index pub created_at: DateTime, pub updated_at: DateTime, } diff --git a/src/models/stack.rs b/src/models/project.rs similarity index 69% rename from src/models/stack.rs rename to src/models/project.rs index a808f130..77422675 100644 --- a/src/models/stack.rs +++ b/src/models/project.rs @@ -4,34 +4,36 @@ use serde_json::Value; use uuid::Uuid; #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Stack { - pub id: i32, // id - is a unique identifier for the app stack - pub stack_id: Uuid, // external stack ID +pub struct Project { + pub id: i32, // id - is a unique identifier for the app project + pub stack_id: Uuid, // external project ID pub user_id: String, // external unique identifier for the user pub name: String, // pub body: sqlx::types::Json, pub body: Value, //json type + pub request_json: Value, pub created_at: DateTime, pub updated_at: DateTime, } -impl Stack { - pub fn new(user_id: String, name: String, body: Value) -> Self { +impl Project { + pub fn new(user_id: String, name: String, body: Value, request_json: Value) -> Self { Self { id: 0, stack_id: Uuid::new_v4(), - user_id: user_id, - name: name, - body: body, + user_id, + name, + body, + request_json, created_at: Utc::now(), updated_at: Utc::now(), } } } -impl Default for Stack { +impl Default for Project { fn default() -> Self { - Stack { + Project { user_id: "".to_string(), name: "".to_string(), ..Default::default() diff --git a/src/models/ratecategory.rs b/src/models/ratecategory.rs index 8228ae0e..352bedbc 100644 --- a/src/models/ratecategory.rs +++ b/src/models/ratecategory.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; pub enum RateCategory { Application, // app, feature, extension Cloud, // is user satisfied working with this cloud - Stack, // app stack + Project, // app project DeploymentSpeed, Documentation, Design, diff --git a/src/models/server.rs b/src/models/server.rs new file mode 100644 index 00000000..3827fcf3 --- /dev/null +++ b/src/models/server.rs @@ -0,0 +1,27 @@ +use chrono::{DateTime, Utc}; +use serde_derive::{Deserialize, Serialize}; +use serde_valid::Validate; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Server { + pub id: i32, + pub user_id: String, + pub project_id: i32, + #[validate(min_length = 2)] + #[validate(max_length = 50)] + pub region: String, + #[validate(min_length = 2)] + #[validate(max_length = 50)] + pub zone: Option, + #[validate(min_length = 2)] + #[validate(max_length = 50)] + pub server: String, + #[validate(min_length = 2)] + #[validate(max_length = 50)] + pub os: String, + #[validate(min_length = 3)] + #[validate(max_length = 50)] + pub disk_type: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} diff --git a/src/routes/cloud/add.rs b/src/routes/cloud/add.rs new file mode 100644 index 00000000..cb572096 --- /dev/null +++ b/src/routes/cloud/add.rs @@ -0,0 +1,42 @@ +use crate::forms; +use crate::helpers::JsonResponse; +use crate::models; +use crate::db; +use actix_web::{post, web, Responder, Result}; +use sqlx::PgPool; +use tracing::Instrument; +use std::sync::Arc; +use serde_valid::Validate; + +// workflow +// add, update, list, get(user_id), ACL, +// ACL - access to func for a user +// ACL - access to objects for a user + +#[tracing::instrument(name = "Add cloud.")] +#[post("")] +pub async fn add( + user: web::ReqData>, + form: web::Json, + pg_pool: web::Data, +) -> Result { + + if !form.validate().is_ok() { + let errors = form.validate().unwrap_err().to_string(); + let err_msg = format!("Invalid data received {:?}", &errors); + tracing::debug!(err_msg); + + return Err(JsonResponse::::build().form_error(errors)); + } + + let mut cloud: models::Cloud = form.into_inner().into(); + cloud.user_id = user.id.clone(); + + db::cloud::insert(pg_pool.get_ref(), cloud) + .await + .map(|cloud| JsonResponse::build() + .set_item(cloud) + .ok("success")) + .map_err(|_err| JsonResponse::::build() + .internal_server_error("Failed to insert")) +} diff --git a/src/routes/cloud/delete.rs b/src/routes/cloud/delete.rs new file mode 100644 index 00000000..b69aec4e --- /dev/null +++ b/src/routes/cloud/delete.rs @@ -0,0 +1,51 @@ +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{delete, web, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use futures_util::FutureExt; +use tracing::Instrument; +use crate::db; +use crate::models::Cloud; + +#[tracing::instrument(name = "Delete cloud record of a user.")] +#[delete("/{id}")] +pub async fn item( + user: web::ReqData>, + path: web::Path<(i32,)>, + pg_pool: web::Data, +) -> Result { + /// Get cloud apps of logged user only + let (id,) = path.into_inner(); + + let cloud = db::cloud::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|cloud| { + match cloud { + Some(cloud) if cloud.user_id != user.id => { + Err(JsonResponse::::build().bad_request("Delete is forbidden")) + } + Some(cloud) => { + Ok(cloud) + }, + None => Err(JsonResponse::::build().not_found("")) + } + })?; + + db::cloud::delete(pg_pool.get_ref(), cloud.id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|result| { + match result + { + true => { + Ok(JsonResponse::::build().ok("Deleted")) + } + _ => { + Err(JsonResponse::::build().bad_request("Could not delete")) + } + } + }) + +} diff --git a/src/routes/cloud/get.rs b/src/routes/cloud/get.rs new file mode 100644 index 00000000..62f2a472 --- /dev/null +++ b/src/routes/cloud/get.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; +use crate::db; +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{get, web, Responder, Result}; +use sqlx::PgPool; +use tracing::Instrument; + +// workflow +// add, update, list, get(user_id), ACL, +// ACL - access to func for a user +// ACL - access to objects for a user + +#[tracing::instrument(name = "Get cloud.")] +#[get("/{id}")] +pub async fn item( + path: web::Path<(i32,)>, + pg_pool: web::Data, +) -> Result { + let id = path.0; + let cloud = db::cloud::fetch(pg_pool.get_ref(), id) + .await + .map_err(|_err| JsonResponse::::build() + .internal_server_error("")) + .and_then(|cloud| { + match cloud { + Some(cloud) => { Ok(cloud) }, + None => Err(JsonResponse::::build().not_found("object not found")) + } + })?; + + Ok(JsonResponse::build().set_item(cloud).ok("OK")) +} + +#[tracing::instrument(name = "Get all clouds.")] +#[get("")] +pub async fn list( + path: web::Path<()>, + user: web::ReqData>, + pg_pool: web::Data, +) -> Result { + db::cloud::fetch_by_user(pg_pool.get_ref(), user.id.as_ref()) + .await + .map(|clouds| JsonResponse::build().set_list(clouds).ok("OK")) + .map_err(|_err| JsonResponse::::build().internal_server_error("")) +} diff --git a/src/routes/cloud/mod.rs b/src/routes/cloud/mod.rs new file mode 100644 index 00000000..bc45c2a7 --- /dev/null +++ b/src/routes/cloud/mod.rs @@ -0,0 +1,9 @@ +pub mod add; +pub mod get; +pub mod update; +pub(crate) mod delete; + +pub use add::*; +pub use get::*; +pub use update::*; +pub use delete::*; diff --git a/src/routes/cloud/update.rs b/src/routes/cloud/update.rs new file mode 100644 index 00000000..3e2e564d --- /dev/null +++ b/src/routes/cloud/update.rs @@ -0,0 +1,53 @@ +use crate::forms; +use crate::helpers::JsonResponse; +use crate::models; +use crate::db; +use actix_web::{web, web::Data, Responder, Result, post, put}; +use serde_valid::Validate; +use sqlx::PgPool; +use std::sync::Arc; +use tracing::Instrument; + +#[tracing::instrument(name = "Update cloud.")] +#[put("/{id}")] +pub async fn item( + path: web::Path<(i32,)>, + form: web::Json, + user: web::ReqData>, + pg_pool: Data, +) -> Result { + + let id = path.0; + let cloud_row = db::cloud::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|cloud| match cloud { + Some(cloud) if cloud.user_id != user.id => { + Err(JsonResponse::::build().bad_request("Cloud not found")) + } + Some(cloud) => Ok(cloud), + None => Err(JsonResponse::::build().not_found("Cloud not found")), + })?; + + if let Err(errors) = form.validate() { + return Err(JsonResponse::::build().form_error(errors.to_string())); + } + + let mut cloud:models::Cloud = form.into_inner().into(); + cloud.id = cloud_row.id; + cloud.user_id = user.id.clone(); + + tracing::debug!("Updating cloud {:?}", cloud); + + db::cloud::update(pg_pool.get_ref(), cloud) + .await + .map(|cloud| { + JsonResponse::::build() + .set_item(cloud) + .ok("success") + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + JsonResponse::::build().internal_server_error("Could not update") + }) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 0d6e99e8..647742a3 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -4,5 +4,8 @@ pub(crate) mod rating; pub(crate) mod test; pub use health_checks::*; -pub(crate) mod stack; -pub use stack::*; +pub(crate) mod project; +pub(crate) mod cloud; +pub(crate) mod server; + +pub use project::*; diff --git a/src/routes/project/add.rs b/src/routes/project/add.rs new file mode 100644 index 00000000..e1b84bc9 --- /dev/null +++ b/src/routes/project/add.rs @@ -0,0 +1,51 @@ +use crate::db; +use crate::forms; +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{ + post, web, + web::{Bytes, Data}, + Responder, Result, +}; +use serde_json::Value; +use sqlx::PgPool; +use std::sync::Arc; +use std::str::FromStr; +use std::str; + +#[tracing::instrument(name = "Add project.")] +#[post("")] +pub async fn item( + body: Bytes, + user: web::ReqData>, + pg_pool: Data, +) -> Result { + // @todo ACL + let form = forms::project::form::body_into_form(body.clone()).await?; + let project_name = form.custom.custom_stack_code.clone(); + + let body_bytes = actix_web::body::to_bytes(body).await.unwrap(); + let body_str = str::from_utf8(&body_bytes) + .map_err(|err| JsonResponse::::build().internal_server_error(err.to_string()))?; + let request_json = Value::from_str(body_str).unwrap(); + tracing::debug!("Request json: {:?}", request_json); + + let body: Value = serde_json::to_value::(form) + .or(serde_json::to_value::(forms::project::ProjectForm::default())) + .unwrap(); + + let project = models::Project::new( + user.id.clone(), + project_name, + body, + request_json + ); + + db::project::insert(pg_pool.get_ref(), project) + .await + .map(|project| JsonResponse::build().set_item(project).ok("Ok")) + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + }) +} + diff --git a/src/routes/stack/compose.rs b/src/routes/project/compose.rs similarity index 50% rename from src/routes/stack/compose.rs rename to src/routes/project/compose.rs index 7c926b57..d0ea4323 100644 --- a/src/routes/stack/compose.rs +++ b/src/routes/project/compose.rs @@ -1,5 +1,5 @@ use crate::db; -use crate::helpers::stack::builder::DcBuilder; +use crate::helpers::project::builder::DcBuilder; use crate::helpers::JsonResponse; use crate::models; use actix_web::{get, web, web::Data, Responder, Result}; @@ -14,21 +14,21 @@ pub async fn add( pg_pool: Data, ) -> Result { let id = path.0; - let stack = db::stack::fetch(pg_pool.get_ref(), id) + let project = db::project::fetch(pg_pool.get_ref(), id) .await - .map_err(|err| JsonResponse::::build().internal_server_error(err)) - .and_then(|stack| match stack { - Some(stack) if stack.user_id != user.id => { - Err(JsonResponse::::build().not_found("not found")) + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|project| match project { + Some(project) if project.user_id != user.id => { + Err(JsonResponse::::build().not_found("not found")) } - Some(stack) => Ok(stack), - None => Err(JsonResponse::::build().not_found("not found")), + Some(project) => Ok(project), + None => Err(JsonResponse::::build().not_found("not found")), })?; - DcBuilder::new(stack) + DcBuilder::new(project) .build() .map_err(|err| { - JsonResponse::::build().internal_server_error(err) + JsonResponse::::build().internal_server_error(err) }) .map(|fc| JsonResponse::build().set_id(id).set_item(fc).ok("Success")) } @@ -42,18 +42,18 @@ pub async fn admin( ) -> Result { /// Admin function for generating compose file for specified user let id = path.0; - let stack = db::stack::fetch(pg_pool.get_ref(), id) + let project = db::project::fetch(pg_pool.get_ref(), id) .await - .map_err(|err| JsonResponse::::build().internal_server_error(err)) - .and_then(|stack| match stack { - Some(stack) => Ok(stack), - None => Err(JsonResponse::::build().not_found("not found")), + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|project| match project { + Some(project) => Ok(project), + None => Err(JsonResponse::::build().not_found("not found")), })?; - DcBuilder::new(stack) + DcBuilder::new(project) .build() .map_err(|err| { - JsonResponse::::build().internal_server_error(err) + JsonResponse::::build().internal_server_error(err) }) .map(|fc| JsonResponse::build().set_id(id).set_item(fc).ok("Success")) } diff --git a/src/routes/project/delete.rs b/src/routes/project/delete.rs new file mode 100644 index 00000000..c0e1b269 --- /dev/null +++ b/src/routes/project/delete.rs @@ -0,0 +1,51 @@ +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{delete, web, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use futures_util::FutureExt; +use tracing::Instrument; +use crate::db; +use crate::models::Project; + +#[tracing::instrument(name = "Delete project of a user.")] +#[delete("/{id}")] +pub async fn item( + user: web::ReqData>, + path: web::Path<(i32,)>, + pg_pool: web::Data, +) -> Result { + /// Get project apps of logged user only + let (id,) = path.into_inner(); + + let project = db::project::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|project| { + match project { + Some(project) if project.user_id != user.id => { + Err(JsonResponse::::build().bad_request("Delete is forbidden")) + } + Some(project) => { + Ok(project) + }, + None => Err(JsonResponse::::build().not_found("")) + } + })?; + + db::project::delete(pg_pool.get_ref(), project.id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|result| { + match result + { + true => { + Ok(JsonResponse::::build().ok("Deleted")) + } + _ => { + Err(JsonResponse::::build().bad_request("Could not delete")) + } + } + }) + +} diff --git a/src/routes/project/deploy.rs b/src/routes/project/deploy.rs new file mode 100644 index 00000000..a1261ab6 --- /dev/null +++ b/src/routes/project/deploy.rs @@ -0,0 +1,224 @@ +use crate::configuration::Settings; +use crate::db; +use crate::forms; +use crate::helpers::project::builder::DcBuilder; +use crate::helpers::{JsonResponse, MqManager}; +use crate::models; +use actix_web::{post, web, web::Data, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use serde_valid::Validate; +use crate::helpers::compressor::compress; + + + +#[tracing::instrument(name = "Deploy for every user")] +#[post("/{id}/deploy")] +pub async fn item( + user: web::ReqData>, + path: web::Path<(i32,)>, + form: web::Json, + pg_pool: Data, + mq_manager: Data, + sets: Data, +) -> Result { + let id = path.0; + tracing::debug!("User {:?} is deploying project: {}", user, id); + + if !form.validate().is_ok() { + let errors = form.validate().unwrap_err().to_string(); + let err_msg = format!("Invalid form data received {:?}", &errors); + tracing::debug!(err_msg); + + return Err(JsonResponse::::build().form_error(errors)); + } + + // Validate project + let project = db::project::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|project| match project { + Some(project) => Ok(project), + None => Err(JsonResponse::::build().not_found("not found")), + })?; + + // Build compose + let id = project.id.clone(); + let dc = DcBuilder::new(project); + let fc = dc.build().map_err(|err| { + JsonResponse::::build().internal_server_error(err) + })?; + + + // Save cloud credentials if requested + let mut cloud_creds: models::Cloud = form.cloud.clone().into(); + cloud_creds.project_id = Some(id.clone()); + cloud_creds.user_id = user.id.clone(); + + if let Some(save_token) = cloud_creds.save_token { + if save_token { + db::cloud::insert(pg_pool.get_ref(), cloud_creds.clone()) + .await + .map(|cloud| cloud) + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + })?; + } + } + + // Save server type and region + let mut server: models::Server = form.server.clone().into(); + server.user_id = user.id.clone(); + server.project_id = id.clone(); + let server = db::server::insert(pg_pool.get_ref(), server) + .await + .map(|server| server) + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + })?; + + // Build Payload for the 3-d party service through RabbitMQ + let mut payload = forms::project::Payload::try_from(&dc.project) + .map_err(|err| JsonResponse::::build().bad_request(err))?; + + payload.server = Some(server.into()); + payload.cloud = Some(cloud_creds.into()); + payload.stack = form.stack.clone().into(); + payload.user_token = Some(user.id.clone()); + payload.user_email = Some(user.email.clone()); + payload.docker_compose = Some(compress(fc.as_str())); + + // Store deployment attempts into deployment table in db + let project_id = dc.project.id.clone(); + let json_request = dc.project.body.clone(); + let deployment = models::Deployment::new( + project_id, + String::from("pending"), + json_request + ); + + let result = db::deployment::insert(pg_pool.get_ref(), deployment) + .await + .map(|deployment| deployment) + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + }); + + tracing::debug!("Save deployment result: {:?}", result); + tracing::debug!("Send project data <<<<<<<<<<<>>>>>>>>>>>>>>>>{:?}", payload); + + // Send Payload + mq_manager + .publish( + "install".to_string(), + "install.start.tfa.all.all".to_string(), + &payload, + ) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .map(|_| { + JsonResponse::::build() + .set_id(id) + .ok("Success") + }) + +} +#[tracing::instrument(name = "Deploy, when cloud token is saved")] +#[post("/{id}/deploy/{cloud_id}")] +pub async fn saved_item( + user: web::ReqData>, + path: web::Path<(i32, i32)>, + pg_pool: Data, + mq_manager: Data, + sets: Data, +) -> Result { + let id = path.0; + let cloud_id = path.1; + //let cloud_id = Some(1); + tracing::debug!("User {:?} is deploying project: {} to cloud: {} ", user, id, cloud_id); + + let project = db::project::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|project| match project { + Some(project) => Ok(project), + None => Err(JsonResponse::::build().not_found("Project not found")), + })?; + + let id = project.id.clone(); + let dc = DcBuilder::new(project); + let fc = dc.build().map_err(|err| { + JsonResponse::::build().internal_server_error(err) + })?; + + let cloud = match db::cloud::fetch(pg_pool.get_ref(), cloud_id).await { + Ok(cloud) => { + match cloud { + Some(cloud) => cloud, + None => { + return Err(JsonResponse::::build().not_found("No cloud configured")); + } + } + } + Err(e) => { + return Err(JsonResponse::::build().not_found("No cloud configured")); + } + }; + + let server = match db::server::fetch_by_project(pg_pool.get_ref(), dc.project.id.clone()).await { + Ok(server) => { + // for now we support only one type of servers + // if let Some(server) = server.into_iter().nth(0) { + // server + // } + server.into_iter().nth(0).unwrap() // @todo refactoring is required + } + Err(err) => { + return Err(JsonResponse::::build().not_found("No servers configured")); + } + }; + + // let mut payload = forms::project::Payload::default(); + let mut payload = forms::project::Payload::try_from(&dc.project) + .map_err(|err| JsonResponse::::build().bad_request(err))?; + payload.server = Some(server.into()); + payload.cloud = Some(cloud.into()); + payload.user_token = Some(user.id.clone()); + payload.user_email = Some(user.email.clone()); + // let compressed = fc.unwrap_or("".to_string()); + payload.docker_compose = Some(compress(fc.as_str())); + + + let project_id = dc.project.id.clone(); + let json_request = dc.project.body.clone(); + let deployment = models::Deployment::new( + project_id, + String::from("pending"), + json_request + ); + + let result = db::deployment::insert(pg_pool.get_ref(), deployment) + .await + .map(|deployment| deployment) + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + }); + + tracing::debug!("Save deployment result: {:?}", result); + tracing::debug!("Send project data <<<<<<<<<<<>>>>>>>>>>>>>>>>{:?}", payload); + + mq_manager + .publish( + "install".to_string(), + "install.start.tfa.all.all".to_string(), + &payload, + ) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .map(|_| { + JsonResponse::::build() + .set_id(id) + .ok("Success") + }) + +} diff --git a/src/routes/project/get.rs b/src/routes/project/get.rs new file mode 100644 index 00000000..3cd7fc3e --- /dev/null +++ b/src/routes/project/get.rs @@ -0,0 +1,47 @@ +use crate::db; +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{get, web, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use tracing::Instrument; + +#[tracing::instrument(name = "Get logged user project.")] +#[get("/{id}")] +pub async fn item( + user: web::ReqData>, + path: web::Path<(i32,)>, + pg_pool: web::Data, +) -> Result { + /// Get project apps of logged user only + let (id,) = path.into_inner(); + + db::project::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|project| match project { + Some(project) if project.user_id != user.id => { + Err(JsonResponse::::build().not_found("not found")) + } + Some(project) => Ok(JsonResponse::build().set_item(Some(project)).ok("OK")), + None => Err(JsonResponse::::build().not_found("not found")), + }) +} + +#[tracing::instrument(name = "Get user's project list.")] +#[get("/user/{id}")] +pub async fn list( + user: web::ReqData>, + path: web::Path<(String,)>, + pg_pool: web::Data, +) -> Result { + /// This is admin endpoint, used by a client app, client app is confidential + /// it should return projects by user id + /// in order to pass validation at external deployment service + let user_id = path.into_inner().0; + + db::project::fetch_by_user(pg_pool.get_ref(), &user_id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .map(|projects| JsonResponse::build().set_list(projects).ok("OK")) +} diff --git a/src/routes/stack/mod.rs b/src/routes/project/mod.rs similarity index 86% rename from src/routes/stack/mod.rs rename to src/routes/project/mod.rs index 27c80617..6d66205d 100644 --- a/src/routes/stack/mod.rs +++ b/src/routes/project/mod.rs @@ -3,6 +3,7 @@ pub mod deploy; pub mod get; pub mod update; pub(crate) mod compose; +pub(crate) mod delete; pub use add::*; pub use update::*; diff --git a/src/routes/stack/service.rs b/src/routes/project/service.rs similarity index 100% rename from src/routes/stack/service.rs rename to src/routes/project/service.rs diff --git a/src/routes/project/update.rs b/src/routes/project/update.rs new file mode 100644 index 00000000..ecd72b10 --- /dev/null +++ b/src/routes/project/update.rs @@ -0,0 +1,76 @@ +use std::str::FromStr; +use crate::forms; +use crate::helpers::JsonResponse; +use crate::models; +use crate::db; +use actix_web::{web, web::Data, Responder, Result, put}; +use serde_json::Value; +use serde_valid::Validate; +use sqlx::PgPool; +use std::sync::Arc; +use actix_web::web::Bytes; +use tracing::Instrument; +use std::str; + +#[tracing::instrument(name = "Update project.")] +#[put("/{id}")] +pub async fn item( + path: web::Path<(i32,)>, + body: Bytes, + user: web::ReqData>, + pg_pool: Data, +) -> Result { + let id = path.0; + let mut project = db::project::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|project| match project { + Some(project) if project.user_id != user.id => { + Err(JsonResponse::::build().bad_request("Project not found")) + } + Some(project) => Ok(project), + None => Err(JsonResponse::::build().not_found("Project not found")), + })?; + + let body_bytes = actix_web::body::to_bytes(body.clone()).await.unwrap(); + let body_str = str::from_utf8(&body_bytes) + .map_err(|err| JsonResponse::::build().internal_server_error(err.to_string()))?; + let request_json = Value::from_str(body_str)?; + tracing::debug!("Request json: {:?}", request_json); + + // @todo ACL + let form = forms::project::form::body_into_form(body.clone()).await?; + tracing::debug!("form data: {:?}", form); + + if let Err(errors) = form.validate() { + return Err(JsonResponse::::build().form_error(errors.to_string())); + } + + let project_name = form.custom.custom_stack_code.clone(); + + if !form.is_readable_docker_image().await.is_ok() { + return Err(JsonResponse::::build().bad_request("Can not access docker image")); + } + + let body: Value = serde_json::to_value::(form) + .or(serde_json::to_value::(forms::project::ProjectForm::default())) + .unwrap(); + + + project.name = project_name; + project.body = body; + project.request_json = request_json; + + + db::project::update(pg_pool.get_ref(), project) + .await + .map(|project| { + JsonResponse::::build() + .set_item(project) + .ok("success") + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + JsonResponse::::build().internal_server_error("") + }) +} diff --git a/src/routes/server/add.rs b/src/routes/server/add.rs new file mode 100644 index 00000000..5a8970c3 --- /dev/null +++ b/src/routes/server/add.rs @@ -0,0 +1,76 @@ +// use crate::forms; +// use crate::helpers::JsonResponse; +// use crate::models; +// use crate::db; +// use actix_web::{post, web, Responder, Result}; +// use sqlx::PgPool; +// use tracing::Instrument; +// use std::sync::Arc; +// use serde_valid::Validate; + +// workflow +// add, update, list, get(user_id), ACL, +// ACL - access to func for a user +// ACL - access to objects for a user + +// #[tracing::instrument(name = "Add server.")] +// #[post("")] +// pub async fn add( +// user: web::ReqData>, +// form: web::Json, +// pg_pool: web::Data, +// ) -> Result { +// // +// // if !form.validate().is_ok() { +// // let errors = form.validate().unwrap_err().to_string(); +// // let err_msg = format!("Invalid data received {:?}", &errors); +// // tracing::debug!(err_msg); +// // +// // return Err(JsonResponse::::build().form_error(errors)); +// // } +// // +// // +// // db::cloud::fetch(pg_pool.get_ref(), form.cloud_id) +// // .await +// // .map_err(|err| JsonResponse::::build().internal_server_error(err)) +// // .and_then(|cloud| { +// // match cloud { +// // Some(cloud) if cloud.user_id != user.id => { +// // Err(JsonResponse::::build().bad_request("Cloud not found")) +// // } +// // Some(cloud) => { +// // Ok(cloud) +// // }, +// // None => Err(JsonResponse::::build().not_found("Cloud not found")) +// // } +// // })?; +// // +// // db::project::fetch(pg_pool.get_ref(), form.project_id) +// // .await +// // .map_err(|_err| JsonResponse::::build() +// // .bad_request("Invalid project")) +// // .and_then(|project| { +// // match project { +// // Some(project) if project.user_id != user.id => { +// // Err(JsonResponse::::build().bad_request("Project not found")) +// // } +// // Some(project) => { Ok(project) }, +// // None => Err(JsonResponse::::build().not_found("Project not found")) +// // } +// // })?; +// // +// // let mut server: models::Server = form.into_inner().into(); +// // server.user_id = user.id.clone(); +// // +// // db::server::insert(pg_pool.get_ref(), server) +// // .await +// // .map(|server| JsonResponse::build() +// // .set_item(server) +// // .ok("success")) +// // .map_err(|err| +// // match err { +// // _ => { +// // return JsonResponse::::build().internal_server_error("Failed to insert"); +// // } +// // }) +// } diff --git a/src/routes/server/delete.rs b/src/routes/server/delete.rs new file mode 100644 index 00000000..cd9a8240 --- /dev/null +++ b/src/routes/server/delete.rs @@ -0,0 +1,50 @@ +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{delete, web, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use tracing::Instrument; +use crate::db; +use crate::models::Server; + +#[tracing::instrument(name = "Delete user's server.")] +#[delete("/{id}")] +pub async fn item( + user: web::ReqData>, + path: web::Path<(i32,)>, + pg_pool: web::Data, +) -> Result { + /// Get server apps of logged user only + let (id,) = path.into_inner(); + + let server = db::server::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|server| { + match server { + Some(server) if server.user_id != user.id => { + Err(JsonResponse::::build().bad_request("Delete is forbidden")) + } + Some(server) => { + Ok(server) + }, + None => Err(JsonResponse::::build().not_found("")) + } + })?; + + db::server::delete(pg_pool.get_ref(), server.id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|result| { + match result + { + true => { + Ok(JsonResponse::::build().ok("Item deleted")) + } + _ => { + Err(JsonResponse::::build().bad_request("Could not delete")) + } + } + }) + +} diff --git a/src/routes/server/get.rs b/src/routes/server/get.rs new file mode 100644 index 00000000..23bac5b5 --- /dev/null +++ b/src/routes/server/get.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; +use crate::db; +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{get, web, Responder, Result}; +use sqlx::PgPool; +use tracing::Instrument; + +// workflow +// add, update, list, get(user_id), ACL, +// ACL - access to func for a user +// ACL - access to objects for a user + +#[tracing::instrument(name = "Get server.")] +#[get("/{id}")] +pub async fn item( + path: web::Path<(i32,)>, + pg_pool: web::Data, +) -> Result { + let id = path.0; + let server = db::server::fetch(pg_pool.get_ref(), id) + .await + .map_err(|_err| JsonResponse::::build() + .internal_server_error("")) + .and_then(|server| { + match server { + Some(server) => { Ok(server) }, + None => Err(JsonResponse::::build().not_found("object not found")) + } + })?; + + Ok(JsonResponse::build().set_item(server).ok("OK")) +} + +#[tracing::instrument(name = "Get all servers.")] +#[get("")] +pub async fn list( + path: web::Path<()>, + user: web::ReqData>, + pg_pool: web::Data, +) -> Result { + db::server::fetch_by_user(pg_pool.get_ref(), user.id.as_ref()) + .await + .map(|server| JsonResponse::build().set_list(server).ok("OK")) + .map_err(|_err| JsonResponse::::build().internal_server_error("")) +} diff --git a/src/routes/server/mod.rs b/src/routes/server/mod.rs new file mode 100644 index 00000000..af796d2d --- /dev/null +++ b/src/routes/server/mod.rs @@ -0,0 +1,9 @@ +pub mod add; +pub(crate) mod get; +pub(crate) mod delete; +pub(crate) mod update; + +pub use get::*; +pub use add::*; +pub use update::*; +pub use delete::*; diff --git a/src/routes/server/update.rs b/src/routes/server/update.rs new file mode 100644 index 00000000..62208698 --- /dev/null +++ b/src/routes/server/update.rs @@ -0,0 +1,56 @@ +use crate::forms; +use crate::helpers::JsonResponse; +use crate::models; +use crate::db; +use actix_web::{web, web::Data, Responder, Result, post, put}; +use serde_valid::Validate; +use sqlx::PgPool; +use std::sync::Arc; +use tracing::Instrument; + +#[tracing::instrument(name = "Update server.")] +#[put("/{id}")] +pub async fn item( + path: web::Path<(i32,)>, + form: web::Json, + user: web::ReqData>, + pg_pool: Data, +) -> Result { + + let id = path.0; + let mut server_row = db::server::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|server| match server { + Some(server) if server.user_id != user.id => { + Err(JsonResponse::::build().bad_request("Server not found")) + } + Some(server) => Ok(server), + None => Err(JsonResponse::::build().not_found("Server not found")), + })?; + + if let Err(errors) = form.validate() { + return Err(JsonResponse::::build().form_error(errors.to_string())); + } + + let mut server:models::Server = form.into_inner().into(); + server.id = server_row.id; + server.project_id = server_row.project_id; + server.user_id = user.id.clone(); + // exclude + // server.created_at + + tracing::debug!("Updating server {:?}", server); + + db::server::update(pg_pool.get_ref(), server) + .await + .map(|server| { + JsonResponse::::build() + .set_item(server) + .ok("success") + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + JsonResponse::::build().internal_server_error("Could not update server") + }) +} diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs deleted file mode 100644 index 712ca5a6..00000000 --- a/src/routes/stack/add.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::db; -use crate::forms; -use crate::helpers::JsonResponse; -use crate::models; -use actix_web::Error; -use actix_web::{ - post, web, - web::{Bytes, Data}, - Responder, Result, -}; -use serde_json::Value; -use serde_valid::Validate; -use sqlx::PgPool; -use std::str; -use std::sync::Arc; - -#[tracing::instrument(name = "Add stack.")] -#[post("")] -pub async fn add( - body: Bytes, - user: web::ReqData>, - pg_pool: Data, -) -> Result { - // @todo ACL - let form = body_into_form(body).await?; - let stack_name = form.custom.custom_stack_code.clone(); - - stack_exists(pg_pool.get_ref(), &stack_name).await?; - - let body: Value = serde_json::to_value::(form) - .or(serde_json::to_value::(forms::stack::Stack::default())) - .unwrap(); - - let stack = models::Stack::new(user.id.clone(), stack_name, body); - db::stack::insert(pg_pool.get_ref(), stack) - .await - .map(|stack| JsonResponse::build().set_item(stack).ok("Ok")) - .map_err(|_| { - JsonResponse::::build().internal_server_error("Internal Server Error") - }) -} - - -async fn stack_exists(pool: &PgPool, stack_name: &String) -> Result<(), Error> { - db::stack::fetch_one_by_name(pool, stack_name) - .await - .map_err(|_| { - JsonResponse::::build().internal_server_error("Internal Server Error") - }) - .and_then(|stack| match stack { - Some(_) => Err(JsonResponse::::build() - .conflict("Stack with that name already exists")), - None => Ok(()), - }) -} - -async fn body_into_form(body: Bytes) -> Result { - let body_bytes = actix_web::body::to_bytes(body).await.unwrap(); - let body_str = str::from_utf8(&body_bytes) - .map_err(|err| JsonResponse::::build().internal_server_error(err.to_string()))?; - let deserializer = &mut serde_json::Deserializer::from_str(body_str); - serde_path_to_error::deserialize(deserializer) - .map_err(|err| { - let msg = format!("{}:{:?}", err.path().to_string(), err); - JsonResponse::::build().bad_request(msg) - }) - .and_then(|form: forms::stack::Stack| { - if !form.validate().is_ok() { - let errors = form.validate().unwrap_err().to_string(); - let err_msg = format!("Invalid data received {:?}", &errors); - tracing::debug!(err_msg); - - return Err(JsonResponse::::build().form_error(errors)); - } - - Ok(form) - }) -} diff --git a/src/routes/stack/deploy.rs b/src/routes/stack/deploy.rs deleted file mode 100644 index a2bf8693..00000000 --- a/src/routes/stack/deploy.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::configuration::Settings; -use crate::db; -use crate::forms; -use crate::helpers::stack::builder::DcBuilder; -use crate::helpers::{JsonResponse, MqManager}; -use crate::models; -use actix_web::{post, web, web::Data, Responder, Result}; -use lapin::publisher_confirm::Confirmation; -use sqlx::PgPool; -use std::sync::Arc; -use crate::helpers::compressor::compress; - - -#[tracing::instrument(name = "Deploy for every user. Admin endpoint")] -#[post("/{id}/deploy")] -pub async fn add( - user: web::ReqData>, - path: web::Path<(i32,)>, - pg_pool: Data, - mq_manager: Data, - sets: Data, -) -> Result { - let id = path.0; - let stack = db::stack::fetch(pg_pool.get_ref(), id) - .await - .map_err(|err| JsonResponse::::build().internal_server_error(err)) - .and_then(|stack| match stack { - Some(stack) => Ok(stack), - None => Err(JsonResponse::::build().not_found("not found")), - })?; - - let id = stack.id.clone(); - let dc = DcBuilder::new(stack); - let fc = dc.build().map_err(|err| { - JsonResponse::::build().internal_server_error(err) - })?; - - let mut stack_data = forms::stack::Payload::try_from(&dc.stack) - .map_err(|err| JsonResponse::::build().bad_request(err))?; - stack_data.user_token = Some(user.id.clone()); - stack_data.user_email = Some(user.email.clone()); - // let compressed = fc.unwrap_or("".to_string()); - stack_data.docker_compose = Some(compress(fc.as_str())); - - mq_manager - .publish_and_confirm( - "install".to_string(), - "install.start.tfa.all.all".to_string(), - &stack_data, - ) - .await - .map_err(|err| JsonResponse::::build().internal_server_error(err)) - .map(|_| { - JsonResponse::::build() - .set_id(id) - .ok("Success") - }) -} diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs deleted file mode 100644 index ccb9c9de..00000000 --- a/src/routes/stack/get.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::db; -use crate::helpers::JsonResponse; -use crate::models; -use actix_web::{get, web, Responder, Result}; -use sqlx::PgPool; -use std::convert::From; -use std::sync::Arc; -use tracing::Instrument; - -#[tracing::instrument(name = "Get logged user stack.")] -#[get("/{id}")] -pub async fn item( - user: web::ReqData>, - path: web::Path<(i32,)>, - pg_pool: web::Data, -) -> Result { - /// Get stack apps of logged user only - let (id,) = path.into_inner(); - - db::stack::fetch(pg_pool.get_ref(), id) - .await - .map_err(|err| JsonResponse::::build().internal_server_error(err)) - .and_then(|stack| match stack { - Some(stack) if stack.user_id != user.id => { - Err(JsonResponse::::build().not_found("not found")) - } - Some(stack) => Ok(JsonResponse::build().set_item(Some(stack)).ok("OK")), - None => Err(JsonResponse::::build().not_found("not found")), - }) -} - -#[tracing::instrument(name = "Get user's stack list.")] -#[get("/user/{id}")] -pub async fn list( - user: web::ReqData>, - path: web::Path<(String,)>, - pg_pool: web::Data, -) -> Result { - /// This is admin endpoint, used by a client app, client app is confidential - /// it should return stacks by user id - /// in order to pass validation at external deployment service - let user_id = path.into_inner().0; - - db::stack::fetch_by_user(pg_pool.get_ref(), &user_id) - .await - .map_err(|err| JsonResponse::::build().internal_server_error(err)) - .map(|stacks| JsonResponse::build().set_list(stacks).ok("OK")) -} diff --git a/src/routes/stack/update.rs b/src/routes/stack/update.rs deleted file mode 100644 index ef11d825..00000000 --- a/src/routes/stack/update.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::forms; -use crate::helpers::JsonResponse; -use crate::models; -use crate::db; -use actix_web::{web, web::Data, Responder, Result, post}; -use serde_json::Value; -use serde_valid::Validate; -use sqlx::PgPool; -use std::sync::Arc; -use tracing::Instrument; -use uuid::Uuid; - -#[tracing::instrument(name = "Update stack.")] -#[post("/{id}")] -pub async fn update( - path: web::Path<(i32,)>, - form: web::Json, - user: web::ReqData>, - pg_pool: Data, -) -> Result { - let id = path.0; - let mut stack = db::stack::fetch(pg_pool.get_ref(), id) - .await - .map_err(|err| JsonResponse::::build().internal_server_error(err)) - .and_then(|stack| match stack { - Some(stack) => Ok(stack), - None => Err(JsonResponse::::build().not_found("Object not found")), - })?; - - let stack_name = form.custom.custom_stack_code.clone(); - tracing::debug!("form data: {:?}", form); - let user_id = user.id.clone(); - - if let Err(errors) = form.validate() { - return Err(JsonResponse::::build().form_error(errors.to_string())); - } - - let form_inner = form.into_inner(); - - if !form_inner.is_readable_docker_image().await.is_ok() { - return Err(JsonResponse::::build().bad_request("Can not access docker image")); - } - - let body: Value = serde_json::to_value::(form_inner) - .map_err(|err| - JsonResponse::::build().bad_request(format!("{err}")) - )?; - - stack.stack_id = Uuid::new_v4(); - stack.user_id = user_id; - stack.name = stack_name; - stack.body = body; - - db::stack::update(pg_pool.get_ref(), stack) - .await - .map(|stack| { - JsonResponse::::build() - .set_item(stack) - .ok("success") - }) - .map_err(|err| { - tracing::error!("Failed to execute query: {:?}", err); - JsonResponse::::build().internal_server_error("") - }) -} diff --git a/src/services/mod.rs b/src/services/mod.rs index f61186ff..94b4efce 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,2 +1,2 @@ -pub mod stack; +pub mod project; mod rating; \ No newline at end of file diff --git a/src/services/stack.rs b/src/services/project.rs similarity index 100% rename from src/services/stack.rs rename to src/services/project.rs diff --git a/src/services/rating.rs b/src/services/rating.rs index 82222211..837be7ba 100644 --- a/src/services/rating.rs +++ b/src/services/rating.rs @@ -1,6 +1,6 @@ -use crate::models::rating::Rating; -use tracing::Instrument; -use tracing_subscriber::fmt::format; +// use crate::models::rating::Rating; +// use tracing::Instrument; +// use tracing_subscriber::fmt::format; // impl Rating { // pub async fn filter_by(query_string: &str, pool: PgPool) -> Result<()> { diff --git a/src/startup.rs b/src/startup.rs index 35481927..2c7c4baa 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -66,18 +66,43 @@ pub async fn run( .service(crate::routes::rating::list_handler), ) .service( - web::scope("/stack") + web::scope("/project") .wrap(HttpAuthentication::bearer( middleware::trydirect::bearer_guard, )) .wrap(Cors::permissive()) - .service(crate::routes::stack::deploy::add) - .service(crate::routes::stack::compose::add) - .service(crate::routes::stack::compose::admin) - .service(crate::routes::stack::get::item) - .service(crate::routes::stack::get::list) - .service(crate::routes::stack::add::add) - .service(crate::routes::stack::update::update), + .service(crate::routes::project::deploy::item) + .service(crate::routes::project::compose::add) + .service(crate::routes::project::compose::admin) + .service(crate::routes::project::get::item) + .service(crate::routes::project::get::list) + .service(crate::routes::project::add::item) + .service(crate::routes::project::update::item) + .service(crate::routes::project::delete::item), + ) + .service( + web::scope("/cloud") + .wrap(HttpAuthentication::bearer( + middleware::trydirect::bearer_guard, + )) + .wrap(Cors::permissive()) + .service(crate::routes::cloud::get::item) + .service(crate::routes::cloud::get::list) + .service(crate::routes::cloud::add::add) + .service(crate::routes::cloud::update::item) + .service(crate::routes::cloud::delete::item), + ) + .service( + web::scope("/server") + .wrap(HttpAuthentication::bearer( + middleware::trydirect::bearer_guard, + )) + .wrap(Cors::permissive()) + .service(crate::routes::server::get::item) + .service(crate::routes::server::get::list) + // .service(crate::routes::server::add::add) + .service(crate::routes::server::update::item) + .service(crate::routes::server::delete::item), ) .app_data(pg_pool.clone()) .app_data(mq_manager.clone()) diff --git a/tests/cloud.rs b/tests/cloud.rs new file mode 100644 index 00000000..c3fd2d39 --- /dev/null +++ b/tests/cloud.rs @@ -0,0 +1,48 @@ +mod common; + +// test me: cargo t --test cloud -- --nocapture --show-output +#[tokio::test] +async fn list() { + + let app = common::spawn_app().await; // server + let client = reqwest::Client::new(); // client + + let response = client + .get(&format!("{}/cloud", &app.address)) + .send() + .await + .expect("Failed to execute request."); + + assert!(response.status().is_success()); + assert_eq!(Some(0), response.content_length()); +} + +// test me: cargo t --test cloud add_cloud -- --nocapture --show-output +#[tokio::test] +async fn add_cloud() { + + let app = common::spawn_app().await; // server + let client = reqwest::Client::new(); // client + + let data = r#" + { + "user_id": "fake_user_id", + "provider": "htz", + "cloud_token": "", + "cloud_key": "", + "cloud_secret": "", + "save_token": true + } + "#; + + let response = client + .post(&format!("{}/cloud", &app.address)) + .json(data) + .send() + .await + .expect("Failed to execute request."); + + println!("response: {}", response.status()); + assert!(response.status().is_success()); + assert_eq!(Some(0), response.content_length()); +} diff --git a/tests/dockerhub.rs b/tests/dockerhub.rs index 200b665d..25b30e25 100644 --- a/tests/dockerhub.rs +++ b/tests/dockerhub.rs @@ -1,18 +1,20 @@ // use std::fs; // use std::collections::HashMap; use std::env; +use docker_compose_types::{ComposeVolume, SingleValue}; + mod common; -use stacker::forms::stack::DockerImage; -use stacker::helpers::stack::dctypes::{ComposeVolume, SingleValue}; +use stacker::forms::project::DockerImage; +// use stacker::helpers::project::dctypes::{ComposeVolume, SingleValue}; use serde_yaml; -use stacker::forms::Volume; +use stacker::forms::project::Volume; const DOCKER_USERNAME: &str = "trydirect"; const DOCKER_PASSWORD: &str = "***************"; // Unit Test // #[test] -// fn test_deserialize_user_stack_web() { +// fn test_deserialize_project_web() { // // let body_str = fs::read_to_string("./tests/web-item.json").unwrap(); // // let form:serde_json::Value = serde_json::from_str(&body_str).unwrap(); @@ -24,17 +26,17 @@ const DOCKER_PASSWORD: &str = "***************"; // // } // // Err(_err) => { // // let msg = format!("Invalid data. {:?}", _err); -// // return JsonResponse::::build().bad_request(msg); +// // return JsonResponse::::build().bad_request(msg); // // } // // }; // // // // assert_eq!(result, 12); // } // #[test] -// fn test_deserialize_user_stack() { +// fn test_deserialize_project() { // -// let body_str = fs::read_to_string("./tests/custom-stack-payload-11.json").unwrap(); -// let form = serde_json::from_str::(&body_str).unwrap(); +// let body_str = fs::read_to_string("./tests/custom-project-payload-11.json").unwrap(); +// let form = serde_json::from_str::(&body_str).unwrap(); // println!("{:?}", form); // // @todo assert required data // @@ -44,7 +46,7 @@ const DOCKER_PASSWORD: &str = "***************"; // // } // // Err(_err) => { // // let msg = format!("Invalid data. {:?}", _err); -// // return JsonResponse::::build().bad_request(msg); +// // return JsonResponse::::build().bad_request(msg); // // } // // }; // // @@ -122,6 +124,6 @@ async fn test_docker_named_volume() { println!("ComposeVolume: {:?}", cv); println!("{:?}", cv.driver_opts); assert_eq!(Some("flask-data".to_string()), cv.name); - assert_eq!(&Some(SingleValue::String("/root/stack/flask-data".to_string())), cv.driver_opts.get("device").unwrap()); + assert_eq!(&Some(SingleValue::String("/root/project/flask-data".to_string())), cv.driver_opts.get("device").unwrap()); assert_eq!(&Some(SingleValue::String("none".to_string())), cv.driver_opts.get("type").unwrap()); } diff --git a/tests/app.json b/tests/mock_data/app.json similarity index 100% rename from tests/app.json rename to tests/mock_data/app.json diff --git a/tests/mock_data/cloud-update.json b/tests/mock_data/cloud-update.json new file mode 100644 index 00000000..72967ad9 --- /dev/null +++ b/tests/mock_data/cloud-update.json @@ -0,0 +1,9 @@ +{ + "user_id": "hy181TZa4DaabUZWklsrxw", + "project_id": 1, + "provider": "htz", + "cloud_token": "cloud_token_updates", + "cloud_key": "cloud_token_updates", + "cloud_secret": "cloud_secret_updates", + "save_token": false +} diff --git a/tests/mock_data/cloud.json b/tests/mock_data/cloud.json new file mode 100644 index 00000000..04f74125 --- /dev/null +++ b/tests/mock_data/cloud.json @@ -0,0 +1,9 @@ +{ + "user_id": "hy181TZa4DaabUZWklsrxw", + "project_id": 1, + "provider": "htz", + "cloud_token": "cloud_token_here", + "cloud_key": "cloud_token_here", + "cloud_secret": "cloud_secret_here", + "save_token": true +} diff --git a/tests/custom-stack-payload-no-networks.json b/tests/mock_data/custom-stack-payload-no-networks.json similarity index 100% rename from tests/custom-stack-payload-no-networks.json rename to tests/mock_data/custom-stack-payload-no-networks.json diff --git a/tests/custom-stack-payload.json b/tests/mock_data/custom-stack-payload.json similarity index 100% rename from tests/custom-stack-payload.json rename to tests/mock_data/custom-stack-payload.json diff --git a/tests/custom.json b/tests/mock_data/custom.json similarity index 100% rename from tests/custom.json rename to tests/mock_data/custom.json diff --git a/tests/mock_data/deploy.json b/tests/mock_data/deploy.json new file mode 100644 index 00000000..6917d31b --- /dev/null +++ b/tests/mock_data/deploy.json @@ -0,0 +1 @@ +{"project":{"networks":[{"id":"ltoi393j2i4iit2pz","name":"default_network"}],"web":[{"_created":"2023-05-10T09:57:23.773552","_etag":null,"_id":"ltoi3u3515z6nwsk1","_updated":"2024-02-27T15:10:40.107999","ansible_var":null,"autodeploy":null,"avoid_render":null,"category":[null],"category_id":null,"code":"openresty","commercial":null,"cpu":"0.0","custom_preset":{"dockerhub_name":"openresty","dockerhub_user":"openresty","environment":[],"restart":"always","shared_ports":[],"volumes":[]},"default":true,"dependency":null,"descr":null,"description":"

a dynamic web platform built on NGINX and LuaJIT. Learn more

","disk_size":null,"docker_image_is_internal":true,"dockerhub_image":"openresty","dockerhub_name":"openresty","dockerhub_user":"openresty","domain":"","environment":[],"form":null,"full_description":null,"group":[],"icon":{"dark":{},"light":{"height":150,"image":"12140d93-350c-4fb8-b3d2-e350f3943b0b.svg","width":147}},"links":[{"follow":false,"title":"Openresty","type":"vendor","url":"https://openresty.org/"},{"follow":false,"repo_name":"openresty","repo_owner":"openresty","type":"github"}],"name":"OpenResty","network":["ltoi393j2i4iit2pz"],"parent_app_id":null,"plan_type":null,"popularity":null,"ports":{"public":["80","443"]},"price":null,"ram_size":null,"repo_dir":null,"requirements":null,"restart":"always","role":null,"shared_ports":[{"host_port":"80","container_port":"80"}],"subscription":null,"suggested":null,"timestamp":"2024-03-12T15:02:14.033Z","type":"web","version":{"_id":586,"name":"1.15.8.3","tag":"1.15.8.3","update_status":"published","version":"1.15.8.3"},"versions":[{"_id":586,"name":"1.15.8.3","tag":"1.15.8.3","update_status":"published","version":"1.15.8.3"}],"volumes":[]}],"feature":[],"service":[],"custom_stack_category":null,"custom_stack_code":"project-1","custom_stack_description":null,"custom_stack_short_description":null,"project_description":null,"project_git_url":null,"project_name":"Project 1","project_overview":null},"cloud":{"provider": "htz","save_token":true,"cloud_token":"*****"},"server":{"region":"fsn1","zone":null,"server":"cx11","os":"ubuntu-20.04","disk_type":"pd-standart","servers_count":3},"stack":{"commonDomain":"","domainList":{},"ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":[],"form_app":[]}} \ No newline at end of file diff --git a/tests/mock_data/deploy2.json b/tests/mock_data/deploy2.json new file mode 100644 index 00000000..4bf38579 --- /dev/null +++ b/tests/mock_data/deploy2.json @@ -0,0 +1 @@ +{"cloud":{"save_token":false,"cloud_token":"****","provider":"htz"},"server":{"region":"fsn1","zone":null,"server":"cx11","os":"ubuntu-20.04","disk_type":"pd-standart","servers_count":3},"stack":{"vars":[],"integrated_features":[],"extended_features":[],"subscriptions":[],"form_app":[]}} diff --git a/tests/mock_data/project-update.json b/tests/mock_data/project-update.json new file mode 100644 index 00000000..f13f68be --- /dev/null +++ b/tests/mock_data/project-update.json @@ -0,0 +1,7 @@ +{ + "id": 1, + "stack_id": "9239ea1d-8306-4493-aae1-fcc00de76241", + "user_id": "hy181TZa4DaabUZWklsrxw", + "name": "sample", + "body": "{\"key\": \"val\"}" +} \ No newline at end of file diff --git a/tests/mock_data/project.json b/tests/mock_data/project.json new file mode 100644 index 00000000..4c0e7c7c --- /dev/null +++ b/tests/mock_data/project.json @@ -0,0 +1,6 @@ +{ + "stack_id": "9239ea1d-8306-4493-aae1-fcc00de76241", + "user_id": "hy181TZa4DaabUZWklsrxw", + "name": "sample", + "body": "{}" +} \ No newline at end of file diff --git a/tests/mock_data/server-update-invalid.json b/tests/mock_data/server-update-invalid.json new file mode 100644 index 00000000..51103450 --- /dev/null +++ b/tests/mock_data/server-update-invalid.json @@ -0,0 +1,14 @@ +{ + "id": 1, + "user_id": "hy181TZa4DaabUZWklsrxw", + "cloud_id": 100000, + "region": "fra-1", + "zone": "a", + "server": "server-1", + "os": "3408230498203948234", + "disk_type": "samples", + "created_at": "", + "updated_at": "", + "project_id": 100000 +} + diff --git a/tests/mock_data/server-update.json b/tests/mock_data/server-update.json new file mode 100644 index 00000000..b85eb429 --- /dev/null +++ b/tests/mock_data/server-update.json @@ -0,0 +1,14 @@ +{ + "id": 1, + "user_id": "hy181TZa4DaabUZWklsrxw", + "project_id": 1, + "cloud_id": 1, + "region": "fra-1", + "zone": "a", + "server": "server-1", + "os": "3408230498203948234", + "disk_type": "samples", + "created_at": "", + "updated_at": "" +} + diff --git a/tests/mock_data/server.json b/tests/mock_data/server.json new file mode 100644 index 00000000..2d7d6269 --- /dev/null +++ b/tests/mock_data/server.json @@ -0,0 +1,13 @@ +{ + "user_id": "hy181TZa4DaabUZWklsrxw", + "project_id":1, + "cloud_id": 1, + "region": "fra-1", + "zone": "a", + "server": "server-1", + "os": "3408230498203948234", + "disk_type": "samples", + "created_at": "", + "updated_at": "" +} + diff --git a/tests/web-item.json b/tests/mock_data/web-item.json similarity index 100% rename from tests/web-item.json rename to tests/mock_data/web-item.json diff --git a/tests/model_user_stack.rs b/tests/model_project.rs similarity index 88% rename from tests/model_user_stack.rs rename to tests/model_project.rs index 88a005b5..e5fd40da 100644 --- a/tests/model_user_stack.rs +++ b/tests/model_project.rs @@ -1,13 +1,13 @@ -use stacker::forms::stack::StackForm; -use stacker::forms::stack::DockerImage; -use stacker::forms::stack::App; +use stacker::forms::project::ProjectForm; +use stacker::forms::project::DockerImage; +use stacker::forms::project::App; use std::fs; use std::collections::HashMap; // Unit Test // #[test] -// fn test_deserialize_user_stack_web() { +// fn test_deserialize_project_web() { // // let body_str = fs::read_to_string("./tests/web-item.json").unwrap(); // // let form:serde_json::Value = serde_json::from_str(&body_str).unwrap(); @@ -19,17 +19,17 @@ use std::collections::HashMap; // // } // // Err(_err) => { // // let msg = format!("Invalid data. {:?}", _err); -// // return JsonResponse::::build().bad_request(msg); +// // return JsonResponse::::build().bad_request(msg); // // } // // }; // // // // assert_eq!(result, 12); // } #[test] -fn test_deserialize_user_stack() { +fn test_deserialize_project() { - let body_str = fs::read_to_string("./tests/custom-stack-payload-11.json").unwrap(); - let form = serde_json::from_str::(&body_str).unwrap(); + let body_str = fs::read_to_string("./tests/custom-project-payload-11.json").unwrap(); + let form = serde_json::from_str::(&body_str).unwrap(); println!("{:?}", form); // @todo assert required data @@ -39,7 +39,7 @@ fn test_deserialize_user_stack() { // } // Err(_err) => { // let msg = format!("Invalid data. {:?}", _err); - // return JsonResponse::::build().bad_request(msg); + // return JsonResponse::::build().bad_request(msg); // } // }; //