From 1dd107eddac19c8955b47d749b01edf01919b72f Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Tue, 7 Nov 2023 14:34:42 +0200
Subject: [PATCH 1/5] dctypes lib added, ports converted

---
 Cargo.lock                   | 124 +++++-
 Cargo.toml                   |  12 +
 custom-stack-payload-4.json  |   2 +-
 files/docker-compose.yml     |  20 +
 src/helpers/mod.rs           |   1 +
 src/helpers/stack/builder.rs | 207 +++++++++
 src/helpers/stack/dctypes.rs | 842 +++++++++++++++++++++++++++++++++++
 src/helpers/stack/mod.rs     |   2 +
 src/models/stack.rs          |   2 +-
 src/routes/stack/add.rs      |  52 ++-
 src/routes/stack/get.rs      |   1 +
 src/startup.rs               |   1 +
 12 files changed, 1259 insertions(+), 7 deletions(-)
 create mode 100644 files/docker-compose.yml
 create mode 100644 src/helpers/stack/builder.rs
 create mode 100644 src/helpers/stack/dctypes.rs
 create mode 100644 src/helpers/stack/mod.rs

diff --git a/Cargo.lock b/Cargo.lock
index 21202d53..68ef0eb4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -562,6 +562,41 @@ dependencies = [
  "typenum",
 ]
 
+[[package]]
+name = "darling"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "deranged"
 version = "0.3.9"
@@ -571,6 +606,37 @@ dependencies = [
  "powerfmt",
 ]
 
+[[package]]
+name = "derive_builder"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
+dependencies = [
+ "derive_builder_core",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "derive_more"
 version = "0.99.17"
@@ -645,6 +711,12 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
 [[package]]
 name = "errno"
 version = "0.3.5"
@@ -803,6 +875,12 @@ version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
 
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
 [[package]]
 name = "h2"
 version = "0.3.21"
@@ -815,7 +893,7 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "http",
- "indexmap",
+ "indexmap 1.9.3",
  "slab",
  "tokio",
  "tokio-util",
@@ -983,6 +1061,12 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
 [[package]]
 name = "idna"
 version = "0.4.0"
@@ -1004,6 +1088,17 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "indexmap"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.1",
+ "serde",
+]
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -1886,7 +1981,7 @@ version = "0.16.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0adc7a19d45e581abc6d169c865a0b14b84bb43a9e966d1cca4d733e70f7f35a"
 dependencies = [
- "indexmap",
+ "indexmap 1.9.3",
  "itertools 0.10.5",
  "num-traits",
  "once_cell",
@@ -1924,6 +2019,19 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "serde_yaml"
+version = "0.9.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574"
+dependencies = [
+ "indexmap 2.1.0",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
 [[package]]
 name = "sha1"
 version = "0.10.6"
@@ -2059,7 +2167,7 @@ dependencies = [
  "hex",
  "hkdf",
  "hmac",
- "indexmap",
+ "indexmap 1.9.3",
  "itoa",
  "libc",
  "log",
@@ -2129,6 +2237,9 @@ dependencies = [
  "actix-web-httpauth",
  "chrono",
  "config",
+ "derive_builder",
+ "glob",
+ "indexmap 2.1.0",
  "rand",
  "regex",
  "reqwest",
@@ -2136,6 +2247,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "serde_valid",
+ "serde_yaml",
  "sqlx",
  "thiserror",
  "tokio",
@@ -2545,6 +2657,12 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
 
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
+
 [[package]]
 name = "untrusted"
 version = "0.7.1"
diff --git a/Cargo.toml b/Cargo.toml
index 47cbb107..581f4d5a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,6 +34,11 @@ tracing-actix-web = "0.7.7"
 regex = "1.10.2"
 rand = "0.8.5"
 
+# dctypes
+derive_builder = "0.12.0"
+indexmap = { version = "2.0.0", features = ["serde"], optional = true }
+serde_yaml = "0.9"
+
 [dependencies.sqlx]
 version = "0.6.3"
 features = [
@@ -45,3 +50,10 @@ features = [
     "json",
     "offline"
 ]
+
+[features]
+default = ["indexmap"]
+indexmap = ["dep:indexmap"]
+
+[dev-dependencies]
+glob = "0.3"
\ No newline at end of file
diff --git a/custom-stack-payload-4.json b/custom-stack-payload-4.json
index 4f0ce614..07ee1569 100644
--- a/custom-stack-payload-4.json
+++ b/custom-stack-payload-4.json
@@ -1 +1 @@
-{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cx21","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":["stack_migration"],"save_token":false,"cloud_token":"r6LAjqrynVt7pUwctVkzBlJmKjLOCxJIWjZFMLTkPYCCB4rsgphhEVhiL4DuO757","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"name":"Smarty Bot","code":"smarty-bot","domain":"smartybot.xyz","sharedPorts":["8000"],"versions":[],"custom":true,"type":"web","main":true,"_id":"lltkpq6p347kystct","dockerhub_user":"trydirect","dockerhub_name":"smarty-bot","url_app":"smartybot.xyz","url_git":"https://github.com/vsilent/smarty.git","disk_size":"1Gb","ram_size":"1Gb","cpu":1}],"feature":[{"_etag":null,"_id":198,"_created":"2022-04-27T14:10:27.280327","_updated":"2023-08-03T08:24:18.958721","name":"Portainer CE Feature","code":"portainer_ce_feature","role":["portainer-ce-feature"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["9000","8000"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":1138,"height":1138,"image":"08589075-44e6-430e-98a5-f9dcf711e054.svg"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"0.6","ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}],"domain":"","sharedPorts":["9000"],"main":true,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}}],"service":[{"_etag":null,"_id":230,"_created":"2023-05-24T12:51:52.108972","_updated":"2023-08-04T12:18:34.670194","name":"pgrst","code":"pgrst","role":null,"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":null,"category_id":null,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\">PostgREST description</p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"1","ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"pgrst","versions":[{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"},{"_etag":null,"_id":563,"_created":null,"_updated":"2023-05-24T12:52:15.351522","app_id":230,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"}],"domain":"","sharedPorts":["9999"],"main":true,"version":{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"}}],"servers_count":3,"custom_stack_name":"mysampleproject2","custom_stack_code":"another-bot2","custom_stack_category":["New"],"custom_stack_short_description":"sample short description","custom_stack_description":"stack description","custom_stack_publish":false,"project_name":"Smarty Bot","project_git_url":"https://github.com/vsilent/smarty.git","project_overview":"my product 1","project_description":"my product 1"}}
+{"commonDomain":"", "domainList":{}, "region":"fsn1", "zone":null, "server":"cx21", "os":"ubuntu-20.04", "ssl":"letsencrypt", "vars":[], "integrated_features":[],"extended_features":[],"subscriptions":["stack_migration"],"save_token":false,"cloud_token":"r6LAjqrynVt7pUwctVkzBlJmKjLOCxJIWjZFMLTkPYCCB4rsgphhEVhiL4DuO757","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"name":"Smarty Bot","code":"smarty-bot","domain":"smartybot.xyz","sharedPorts":["8000"],"versions":[],"custom":true,"type":"web","main":true,"_id":"lltkpq6p347kystct","dockerhub_user":"trydirect","dockerhub_name":"smarty-bot","url_app":"smartybot.xyz","url_git":"https://github.com/vsilent/smarty.git","disk_size":"1Gb","ram_size":"1Gb","cpu":1}],"feature":[{"_etag":null,"_id":198,"_created":"2022-04-27T14:10:27.280327","_updated":"2023-08-03T08:24:18.958721","name":"Portainer CE Feature","code":"portainer_ce_feature","role":["portainer-ce-feature"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["9000","8000"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":1138,"height":1138,"image":"08589075-44e6-430e-98a5-f9dcf711e054.svg"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\"><strong style=\"font-weight:bold\">Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)</strong></p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"0.6","ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}],"domain":"","sharedPorts":["9000"],"main":true,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-03-17T13:46:51.433539","app_id":198,"name":"latest","version":"latest","update_status":"published","tag":"latest"}}],"service":[{"_etag":null,"_id":230,"_created":"2023-05-24T12:51:52.108972","_updated":"2023-08-04T12:18:34.670194","name":"pgrst","code":"pgrst","role":null,"type":"service","default":null,"popularity":null,"descr":null,"ports":null,"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":null,"category_id":null,"parent_app_id":null,"full_description":null,"description":"<p class=\"content-editor__unstyled\">PostgREST description</p>","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"1","ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"pgrst","versions":[{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"},{"_etag":null,"_id":563,"_created":null,"_updated":"2023-05-24T12:52:15.351522","app_id":230,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"}],"domain":"","sharedPorts":["9999"],"main":true,"version":{"_etag":"566","_id":566,"_created":"2023-08-15T12:10:44","_updated":"2023-08-15T12:10:44.905249","app_id":230,"name":"PostgreSQL","version":"15_4","update_status":"ready_for_testing","tag":"unstable"}}],"servers_count":3,"custom_stack_name":"mysampleproject2","custom_stack_code":"another-bot2","custom_stack_category":["New"],"custom_stack_short_description":"sample short description","custom_stack_description":"stack description","custom_stack_publish":false,"project_name":"Smarty Bot","project_git_url":"https://github.com/vsilent/smarty.git","project_overview":"my product 1","project_description":"my product 1"}}
diff --git a/files/docker-compose.yml b/files/docker-compose.yml
new file mode 100644
index 00000000..c6e60d64
--- /dev/null
+++ b/files/docker-compose.yml
@@ -0,0 +1,20 @@
+version: '3.8'
+services:
+  smarty-bot:
+    image: trydirect/smarty-bot:latest
+    ports:
+    - target: 8000
+      published: 8000
+    restart: always
+  pgrst:
+    image: pgrst
+    ports:
+    - target: 9999
+      published: 9999
+    restart: always
+  portainer_ce_feature:
+    image: portainer-ce-feature
+    ports:
+    - target: 9000
+      published: 9000
+    restart: always
diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs
index 3500bbd3..823b2b1c 100644
--- a/src/helpers/mod.rs
+++ b/src/helpers/mod.rs
@@ -1,5 +1,6 @@
 pub mod client;
 pub(crate) mod json;
 pub mod serialize_datetime;
+pub(crate) mod stack;
 
 pub use json::*;
diff --git a/src/helpers/stack/builder.rs b/src/helpers/stack/builder.rs
new file mode 100644
index 00000000..023ee964
--- /dev/null
+++ b/src/helpers/stack/builder.rs
@@ -0,0 +1,207 @@
+use crate::helpers::stack::dctypes::{Compose, Port, Ports, PublishedPort, Service, Services, SingleService};
+use serde_yaml;
+use crate::forms;
+use crate::forms::{StackForm, Web, Feature};
+use crate::models::stack::Stack;
+
+#[derive(Clone, Debug)]
+struct Config {
+
+}
+
+impl Default for Config {
+    fn default() -> Self {
+        Config {}
+    }
+}
+
+/// A builder for constructing docker compose.
+#[derive(Clone, Debug)]
+pub struct DcBuilder {
+    config: Config,
+    stack: Stack
+}
+
+impl TryInto<Vec<Port>> for Web {
+    type Error = String;
+    fn try_into(self) -> Result<Vec<Port>, Self::Error> {
+        convert_shared_ports(self.shared_ports.clone().unwrap())
+    }
+}
+
+impl TryInto<Vec<Port>> for &Feature {
+    type Error = String;
+    fn try_into(self) -> Result<Vec<Port>, Self::Error> {
+        convert_shared_ports(self.shared_ports.clone().unwrap())
+    }
+}
+
+impl TryInto<Vec<Port>> for &forms::stack::Service {
+    type Error = String;
+    fn try_into(self) -> Result<Vec<Port>, Self::Error> {
+        convert_shared_ports(self.shared_ports.clone().unwrap())
+    }
+}
+
+fn convert_shared_ports(ports: Vec<String>) -> Result<Vec<Port>, String> {
+    let mut _ports: Vec<Port> = vec![];
+    for p in ports {
+        let port = p.parse::<u16>().map_err(|e| e.to_string())?;
+        _ports.push(Port {
+            target: port,
+            host_ip: None,
+            published: Some(PublishedPort::Single(port)),
+            protocol: None,
+            mode: None,
+        });
+    }
+    Ok(_ports)
+}
+
+impl DcBuilder {
+
+    pub fn new() -> Self {
+        DcBuilder {
+            config: Config::default(),
+            stack: Stack {
+                id: 0,
+                stack_id: Default::default(),
+                user_id: "".to_string(),
+                name: "".to_string(),
+                body: Default::default(),
+                created_at: Default::default(),
+                updated_at: Default::default(),
+            },
+        }
+    }
+
+    // pub fn add_ports(&self, ports: &Vec<String>) -> Vec<Port> {
+    //     // @todo re-factor using TryInto or TryFrom
+    //
+    //     let mut _ports:Vec<Port> = vec![];
+    //     for p in ports {
+    //         let port = p.parse::<u16>().unwrap();
+    //         _ports.push(
+    //             Port {
+    //                 target: port,
+    //                 host_ip: None,
+    //                 published: Some(PublishedPort::Single(port)),
+    //                 protocol: None,
+    //                 mode: None,
+    //             }
+    //         );
+    //     }
+    //     _ports
+    // }
+
+    pub fn build(&self, stack:Stack) -> Option<Compose> {
+
+        tracing::debug!("Start build docker compose from {:?}", stack.body);
+        let _stack = serde_json::from_value::<StackForm>(stack.body);
+        let mut services = indexmap::IndexMap::new();
+        match _stack  {
+            Ok(apps) => {
+                println!("stack item {:?}", apps.custom.web);
+
+                for app in apps.custom.web {
+                    // println!("app name {:?}", app.name);
+                    let tag = "latest";
+                    let img= format!("{}/{}:{}",app.dockerhub_user, app.dockerhub_name, tag);
+                    let code = app.code.clone().to_owned();
+                    let mut service = Service {
+                        image: Some(img.to_string()),
+                        ..Default::default()
+                    };
+
+                    if let Some(ports) = &app.shared_ports {
+                        if !ports.is_empty() {
+                            // service.ports = Ports::Long(self.add_ports(ports));
+                            service.ports = Ports::Long(app.try_into().unwrap())
+                        }
+                    }
+
+                    service.restart = Some("always".to_owned());
+                    services.insert(
+                        code,
+                        Some(service),
+                    );
+                }
+
+                if let Some(srvs) = &apps.custom.service {
+
+                    if !srvs.is_empty() {
+
+                        for app in srvs {
+                            let code = app.code.to_owned();
+                            let tag = "latest";
+
+                            let mut service = Service {
+                                image: Some(app.dockerhub_image.as_ref().unwrap().to_owned()),
+                                ..Default::default()
+                            };
+
+                            if let Some(ports) = &app.shared_ports {
+                                if !ports.is_empty() {
+                                    // service.ports = Ports::Long(self.add_ports(ports));
+                                    service.ports = Ports::Long(app.try_into().unwrap())
+                                }
+                            }
+                            service.restart = Some("always".to_owned());
+                            services.insert(
+                                code,
+                                Some(service),
+                            );
+                        }
+                    }
+                }
+                if let Some(features) = &apps.custom.feature {
+
+                    if !features.is_empty() {
+
+                        for app in features {
+                            let code = app.code.to_owned();
+                            let mut service = Service {
+                                image: Some(app.dockerhub_image.as_ref().unwrap().to_owned()),
+                                ..Default::default()
+                            };
+
+                            if let Some(ports) = &app.shared_ports {
+                                if !ports.is_empty() {
+                                    // service.ports = Ports::Long(self.add_ports(ports));
+                                    service.ports = Ports::Long(app.try_into().unwrap())
+                                }
+                            }
+                            service.restart = Some("always".to_owned());
+                            services.insert(
+                                code,
+                                Some(service),
+                            );
+                        }
+                    }
+                }
+            }
+            Err(e) => {
+                ()
+            }
+        }
+
+        let compose_content = Compose {
+            version: Some("3.8".to_string()),
+            services: {
+                Services(services)
+            },
+            ..Default::default()
+        };
+
+        let target_file = std::path::Path::new("./files/docker-compose.yml");
+        // serialize to string
+        let serialized = match serde_yaml::to_string(&compose_content) {
+            Ok(s) => s,
+            Err(e) => panic!("Failed to serialize docker-compose file: {}", e),
+        };
+        // serialize to file
+        std::fs::write(target_file, serialized).unwrap();
+
+        Some(compose_content)
+    }
+}
diff --git a/src/helpers/stack/dctypes.rs b/src/helpers/stack/dctypes.rs
new file mode 100644
index 00000000..931deab1
--- /dev/null
+++ b/src/helpers/stack/dctypes.rs
@@ -0,0 +1,842 @@
+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;
+use std::str::FromStr;
+
+#[allow(clippy::large_enum_variant)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum ComposeFile {
+    V2Plus(Compose),
+    #[cfg(feature = "indexmap")]
+    V1(IndexMap<String, Service>),
+    #[cfg(not(feature = "indexmap"))]
+    V1(HashMap<String, Service>),
+    Single(SingleService),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+pub struct SingleService {
+    pub service: Service,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+pub struct Compose {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub version: Option<String>,
+    #[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<Service>,
+    #[cfg(feature = "indexmap")]
+    #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")]
+    pub extensions: IndexMap<Extension, Value>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
+    pub extensions: HashMap<Extension, Value>,
+}
+
+impl Compose {
+    pub fn new() -> Self {
+        Default::default()
+    }
+}
+
+#[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<String>,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub privileged: bool,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub healthcheck: Option<Healthcheck>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub deploy: Option<Deploy>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub image: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub container_name: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none", rename = "build")]
+    pub build_: Option<BuildStep>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pid: Option<String>,
+    #[serde(default, skip_serializing_if = "Ports::is_empty")]
+    pub ports: Ports,
+    #[serde(default, skip_serializing_if = "Environment::is_empty")]
+    pub environment: Environment,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub network_mode: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub devices: Vec<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub restart: Option<String>,
+    #[serde(default, skip_serializing_if = "Labels::is_empty")]
+    pub labels: Labels,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub tmpfs: Option<Tmpfs>,
+    #[serde(default, skip_serializing_if = "Ulimits::is_empty")]
+    pub ulimits: Ulimits,
+    #[serde(default, skip_serializing_if = "Volumes::is_empty")]
+    pub volumes: Volumes,
+    #[serde(default, skip_serializing_if = "Networks::is_empty")]
+    pub networks: Networks,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub cap_add: Vec<String>,
+    #[serde(default, skip_serializing_if = "DependsOnOptions::is_empty")]
+    pub depends_on: DependsOnOptions,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub command: Option<Command>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub entrypoint: Option<Entrypoint>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub env_file: Option<EnvFile>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub stop_grace_period: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub profiles: Vec<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub links: Vec<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub dns: Vec<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ipc: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub net: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub stop_signal: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub user: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub working_dir: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub expose: Vec<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub volumes_from: Vec<String>,
+    #[cfg(feature = "indexmap")]
+    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
+    pub extends: IndexMap<String, String>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub extends: HashMap<String, String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub logging: Option<LoggingParameters>,
+    #[serde(default, skip_serializing_if = "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<String>,
+    #[cfg(feature = "indexmap")]
+    #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")]
+    pub extensions: IndexMap<Extension, Value>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
+    pub extensions: HashMap<Extension, Value>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub extra_hosts: Vec<String>,
+    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
+    pub tty: bool,
+    #[serde(default, skip_serializing_if = "SysCtls::is_empty")]
+    pub sysctls: SysCtls,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub security_opt: Vec<String>,
+}
+
+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()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum EnvFile {
+    Simple(String),
+    List(Vec<String>),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum DependsOnOptions {
+    Simple(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    Conditional(IndexMap<String, DependsCondition>),
+    #[cfg(not(feature = "indexmap"))]
+    Conditional(HashMap<String, DependsCondition>),
+}
+
+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(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+pub struct DependsCondition {
+    pub condition: String,
+}
+
+#[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<IndexMap<String, SingleValue>>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub options: Option<HashMap<String, SingleValue>>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum Ports {
+    Short(Vec<String>),
+    Long(Vec<Port>),
+}
+
+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(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct Port {
+    pub target: u16,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub host_ip: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub published: Option<PublishedPort>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub protocol: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mode: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum PublishedPort {
+    Single(u16),
+    Range(String),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum Environment {
+    List(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    KvPair(IndexMap<String, Option<SingleValue>>),
+    #[cfg(not(feature = "indexmap"))]
+    KvPair(HashMap<String, Option<SingleValue>>),
+}
+
+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(),
+        }
+    }
+}
+
+#[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 = ExtensionParseError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let owned = s.to_owned();
+        Extension::try_from(owned)
+    }
+}
+
+impl TryFrom<String> for Extension {
+    type Error = ExtensionParseError;
+
+    fn try_from(s: String) -> Result<Self, Self::Error> {
+        if s.starts_with("x-") {
+            Ok(Self(s))
+        } else {
+            Err(ExtensionParseError(s))
+        }
+    }
+}
+
+/// The result of a failed TryFrom<String> 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 {}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct Services(pub IndexMap<String, Option<Service>>);
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct Services(pub HashMap<String, Option<Service>>);
+
+impl Services {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum Labels {
+    List(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    Map(IndexMap<String, String>),
+    #[cfg(not(feature = "indexmap"))]
+    Map(HashMap<String, String>),
+}
+
+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(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum Tmpfs {
+    Simple(String),
+    List(Vec<String>),
+}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct Ulimits(pub IndexMap<String, Ulimit>);
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct Ulimits(pub HashMap<String, Ulimit>);
+
+impl Ulimits {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum Ulimit {
+    Single(i64),
+    SoftHard { soft: i64, hard: i64 },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum Networks {
+    Simple(Vec<String>),
+    Advanced(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(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum BuildStep {
+    Simple(String),
+    Advanced(AdvancedBuildStep),
+}
+
+#[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<String>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub args: Option<BuildArgs>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub shm_size: Option<u64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub target: Option<String>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub network: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub cache_from: Vec<String>,
+    #[serde(default, skip_serializing_if = "Labels::is_empty")]
+    pub labels: Labels,
+}
+
+#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
+#[serde(untagged)]
+pub enum BuildArgs {
+    Simple(String),
+    List(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    KvPair(IndexMap<String, String>),
+    #[cfg(not(feature = "indexmap"))]
+    KvPair(HashMap<String, String>),
+}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct AdvancedNetworks(pub IndexMap<String, MapOrEmpty<AdvancedNetworkSettings>>);
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
+pub struct AdvancedNetworks(pub HashMap<String, MapOrEmpty<AdvancedNetworkSettings>>);
+
+#[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<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ipv6_address: Option<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub aliases: Vec<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum SysCtls {
+    List(Vec<String>),
+    #[cfg(feature = "indexmap")]
+    Map(IndexMap<String, Option<SingleValue>>),
+    #[cfg(not(feature = "indexmap"))]
+    Map(HashMap<String, Option<SingleValue>>),
+}
+
+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(),
+        }
+    }
+}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct TopLevelVolumes(pub IndexMap<String, MapOrEmpty<ComposeVolume>>);
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct TopLevelVolumes(pub HashMap<String, MapOrEmpty<ComposeVolume>>);
+
+impl TopLevelVolumes {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct ComposeVolume {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub driver: Option<String>,
+    #[cfg(feature = "indexmap")]
+    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
+    pub driver_opts: IndexMap<String, Option<SingleValue>>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub driver_opts: HashMap<String, Option<SingleValue>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub external: Option<ExternalVolume>,
+    #[serde(default, skip_serializing_if = "Labels::is_empty")]
+    pub labels: Labels,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub name: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum ExternalVolume {
+    Bool(bool),
+    Name { name: String },
+}
+
+#[cfg(feature = "indexmap")]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct ComposeNetworks(pub IndexMap<String, MapOrEmpty<NetworkSettings>>);
+
+#[cfg(not(feature = "indexmap"))]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
+pub struct ComposeNetworks(pub HashMap<String, MapOrEmpty<NetworkSettings>>);
+
+impl ComposeNetworks {
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum ComposeNetwork {
+    Detailed(ComposeNetworkSettingDetails),
+    Bool(bool),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct ComposeNetworkSettingDetails {
+    pub name: String,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(deny_unknown_fields)]
+pub struct ExternalNetworkSettingBool(bool);
+
+#[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<String>,
+    #[cfg(feature = "indexmap")]
+    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
+    pub driver_opts: IndexMap<String, Option<SingleValue>>,
+    #[cfg(not(feature = "indexmap"))]
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub driver_opts: HashMap<String, Option<SingleValue>>,
+    #[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<ComposeNetwork>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ipam: Option<Ipam>,
+    #[serde(default, skip_serializing_if = "Labels::is_empty")]
+    pub labels: Labels,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub name: Option<String>,
+}
+
+#[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<String>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub config: Vec<IpamConfig>,
+}
+
+#[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<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+#[serde(deny_unknown_fields)]
+pub struct Deploy {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub mode: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub replicas: Option<i64>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub labels: Vec<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub update_config: Option<UpdateConfig>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub resources: Option<Resources>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub restart_policy: Option<RestartPolicy>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub placement: Option<Placement>,
+}
+
+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<HealthcheckTest>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub interval: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub timeout: Option<String>,
+    #[serde(default, skip_serializing_if = "is_zero")]
+    pub retries: i64,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub start_period: Option<String>,
+    #[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<String>),
+}
+
+#[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<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub memory: Option<String>,
+}
+
+#[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<String>,
+    #[serde(skip_serializing_if = "Vec::is_empty", default)]
+    pub preferences: Vec<Preferences>,
+}
+
+#[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<Limits>,
+    pub reservations: Option<Limits>,
+}
+
+#[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<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub delay: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub max_attempts: Option<i64>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub window: Option<String>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
+#[serde(deny_unknown_fields)]
+pub struct UpdateConfig {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub parallelism: Option<i64>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub delay: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub failure_action: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub monitor: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub max_failure_ratio: Option<f64>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum Volumes {
+    Simple(Vec<String>),
+    Advanced(Vec<AdvancedVolumes>),
+}
+
+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<String>,
+    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<Bind>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub volume: Option<Volume>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tmpfs: Option<TmpfsSettings>,
+}
+
+#[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<String>),
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
+#[serde(untagged)]
+pub enum Entrypoint {
+    Simple(String),
+    List(Vec<String>),
+}
+
+#[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<T> {
+    Map(T),
+    Empty,
+}
+
+impl<T> Default for MapOrEmpty<T> {
+    fn default() -> Self {
+        Self::Empty
+    }
+}
+
+impl<T> From<MapOrEmpty<T>> for Option<T> {
+    fn from(value: MapOrEmpty<T>) -> Self {
+        match value {
+            MapOrEmpty::Map(t) => Some(t),
+            MapOrEmpty::Empty => None,
+        }
+    }
+}
+
+impl<T> Serialize for MapOrEmpty<T>
+    where
+        T: Serialize,
+{
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: serde::Serializer,
+    {
+        match self {
+            Self::Map(t) => t.serialize(serializer),
+            Self::Empty => {
+                use serde::ser::SerializeMap;
+                serializer.serialize_map(None)?.end()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/helpers/stack/mod.rs b/src/helpers/stack/mod.rs
new file mode 100644
index 00000000..7da2d2ec
--- /dev/null
+++ b/src/helpers/stack/mod.rs
@@ -0,0 +1,2 @@
+pub(crate) mod builder;
+pub(crate) mod dctypes;
\ No newline at end of file
diff --git a/src/models/stack.rs b/src/models/stack.rs
index 21432d35..9ce7370e 100644
--- a/src/models/stack.rs
+++ b/src/models/stack.rs
@@ -3,7 +3,7 @@ use serde_json::Value;
 use uuid::Uuid;
 use serde::{Serialize,Deserialize};
 
-#[derive(Debug, Serialize, Deserialize)]
+#[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
diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs
index d1d8b87c..aa1cfcee 100644
--- a/src/routes/stack/add.rs
+++ b/src/routes/stack/add.rs
@@ -15,8 +15,7 @@ use sqlx::PgPool;
 use std::str;
 use tracing::Instrument;
 use uuid::Uuid;
-
-
+use crate::helpers::stack::builder::DcBuilder;
 
 #[tracing::instrument(name = "Add stack.")]
 #[post("")]
@@ -110,3 +109,52 @@ pub async fn add(
         }
     };
 }
+
+#[tracing::instrument(name = "Generate docker-compose.")]
+#[post("/{id}")]
+pub async fn gen(
+    user: web::ReqData<User>,
+    path: web::Path<(i32,)>,
+    pool: Data<PgPool>,
+) -> Result<impl Responder> {
+    let id = path.0;
+    tracing::debug!("Received id: {}", id);
+
+    let stack = match sqlx::query_as!(
+        Stack,
+        r#"
+        SELECT * FROM user_stack WHERE id=$1 AND user_id=$2 LIMIT 1
+        "#,
+        id, user.id
+    )
+            .fetch_one(pool.get_ref())
+            .await
+        {
+            Ok(stack) => {
+                tracing::info!("stack found: {:?}", stack.id,);
+                Some(stack)
+            }
+            Err(sqlx::Error::RowNotFound) => {
+                tracing::error!("Row not found 404");
+                None
+            }
+            Err(e) => {
+                tracing::error!("Failed to fetch stack, error: {:?}", e);
+                None
+            }
+        };
+
+
+    match stack {
+        Some(stack) => {
+            let mut dc = DcBuilder::new();
+            let fc = dc.build(stack);
+            tracing::debug!("Docker compose file content {:?}", fc.unwrap());
+        }
+        None => {
+
+        }
+    }
+
+    Ok(Json(JsonResponse::<Stack>::ok(1, "OK")))
+}
diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs
index 7b5fa7f5..b3ce3898 100644
--- a/src/routes/stack/get.rs
+++ b/src/routes/stack/get.rs
@@ -46,3 +46,4 @@ pub async fn get(
         }
     }
 }
+
diff --git a/src/startup.rs b/src/startup.rs
index edd58c1b..773ce2c0 100644
--- a/src/startup.rs
+++ b/src/startup.rs
@@ -128,6 +128,7 @@ pub async fn run(
                     .wrap(HttpAuthentication::bearer(bearer_guard))
                     .wrap(Cors::permissive())
                     .service(crate::routes::stack::add::add)
+                    .service(crate::routes::stack::add::gen)
                     .service(crate::routes::stack::get::get)
             )
             .app_data(db_pool.clone())

From d04ecab475ff60907c257246aa4fed29d9213029 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Tue, 7 Nov 2023 16:23:09 +0200
Subject: [PATCH 2/5] return contents of the compose file on build

---
 files/{docker-compose.yml => 1.yml}           |  0
 .../c60d329f-9159-48b7-abf3-10ccabbca213.yml  | 20 ++++++
 src/helpers/stack/builder.rs                  | 65 +++++++------------
 src/routes/stack/add.rs                       | 21 ++++--
 4 files changed, 59 insertions(+), 47 deletions(-)
 rename files/{docker-compose.yml => 1.yml} (100%)
 create mode 100644 files/c60d329f-9159-48b7-abf3-10ccabbca213.yml

diff --git a/files/docker-compose.yml b/files/1.yml
similarity index 100%
rename from files/docker-compose.yml
rename to files/1.yml
diff --git a/files/c60d329f-9159-48b7-abf3-10ccabbca213.yml b/files/c60d329f-9159-48b7-abf3-10ccabbca213.yml
new file mode 100644
index 00000000..c6e60d64
--- /dev/null
+++ b/files/c60d329f-9159-48b7-abf3-10ccabbca213.yml
@@ -0,0 +1,20 @@
+version: '3.8'
+services:
+  smarty-bot:
+    image: trydirect/smarty-bot:latest
+    ports:
+    - target: 8000
+      published: 8000
+    restart: always
+  pgrst:
+    image: pgrst
+    ports:
+    - target: 9999
+      published: 9999
+    restart: always
+  portainer_ce_feature:
+    image: portainer-ce-feature
+    ports:
+    - target: 9000
+      published: 9000
+    restart: always
diff --git a/src/helpers/stack/builder.rs b/src/helpers/stack/builder.rs
index 023ee964..1ee5fe9b 100644
--- a/src/helpers/stack/builder.rs
+++ b/src/helpers/stack/builder.rs
@@ -1,7 +1,18 @@
-use crate::helpers::stack::dctypes::{Compose, Port, Ports, PublishedPort, Service, Services, SingleService};
+use crate::helpers::stack::dctypes::{
+    Compose,
+    Port,
+    Ports,
+    PublishedPort,
+    Service,
+    Services
+};
 use serde_yaml;
 use crate::forms;
-use crate::forms::{StackForm, Web, Feature};
+use crate::forms::{
+    StackForm,
+    Web,
+    Feature
+};
 use crate::models::stack::Stack;
 
 #[derive(Clone, Debug)]
@@ -60,44 +71,17 @@ fn convert_shared_ports(ports: Vec<String>) -> Result<Vec<Port>, String> {
 
 impl DcBuilder {
 
-    pub fn new() -> Self {
+    pub fn new(stack: Stack) -> Self {
         DcBuilder {
             config: Config::default(),
-            stack: Stack {
-                id: 0,
-                stack_id: Default::default(),
-                user_id: "".to_string(),
-                name: "".to_string(),
-                body: Default::default(),
-                created_at: Default::default(),
-                updated_at: Default::default(),
-            },
+            stack: stack,
         }
     }
 
-    // pub fn add_ports(&self, ports: &Vec<String>) -> Vec<Port> {
-    //     // @todo re-factor using TryInto or TryFrom
-    //
-    //     let mut _ports:Vec<Port> = vec![];
-    //     for p in ports {
-    //         let port = p.parse::<u16>().unwrap();
-    //         _ports.push(
-    //             Port {
-    //                 target: port,
-    //                 host_ip: None,
-    //                 published: Some(PublishedPort::Single(port)),
-    //                 protocol: None,
-    //                 mode: None,
-    //             }
-    //         );
-    //     }
-    //     _ports
-    // }
-
-    pub fn build(&self, stack:Stack) -> Option<Compose> {
-
-        tracing::debug!("Start build docker compose from {:?}", stack.body);
-        let _stack = serde_json::from_value::<StackForm>(stack.body);
+    pub fn build(&self) -> Option<String> {
+
+        tracing::debug!("Start build docker compose from {:?}", &self.stack.body);
+        let _stack = serde_json::from_value::<StackForm>(self.stack.body.clone());
         let mut services = indexmap::IndexMap::new();
         match _stack  {
             Ok(apps) => {
@@ -115,7 +99,6 @@ impl DcBuilder {
 
                     if let Some(ports) = &app.shared_ports {
                         if !ports.is_empty() {
-                            // service.ports = Ports::Long(self.add_ports(ports));
                             service.ports = Ports::Long(app.try_into().unwrap())
                         }
                     }
@@ -142,7 +125,6 @@ impl DcBuilder {
 
                             if let Some(ports) = &app.shared_ports {
                                 if !ports.is_empty() {
-                                    // service.ports = Ports::Long(self.add_ports(ports));
                                     service.ports = Ports::Long(app.try_into().unwrap())
                                 }
                             }
@@ -167,7 +149,6 @@ impl DcBuilder {
 
                             if let Some(ports) = &app.shared_ports {
                                 if !ports.is_empty() {
-                                    // service.ports = Ports::Long(self.add_ports(ports));
                                     service.ports = Ports::Long(app.try_into().unwrap())
                                 }
                             }
@@ -193,15 +174,17 @@ impl DcBuilder {
             ..Default::default()
         };
 
-        let target_file = std::path::Path::new("./files/docker-compose.yml");
+        let fname= format!("./files/{}.yml", self.stack.stack_id);
+        tracing::debug!("Save docker compose to file {:?}", fname);
+        let target_file = std::path::Path::new(fname.as_str());
         // serialize to string
         let serialized = match serde_yaml::to_string(&compose_content) {
             Ok(s) => s,
             Err(e) => panic!("Failed to serialize docker-compose file: {}", e),
         };
         // serialize to file
-        std::fs::write(target_file, serialized).unwrap();
+        std::fs::write(target_file, serialized.clone()).unwrap();
 
-        Some(compose_content)
+        Some(serialized)
     }
 }
diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs
index aa1cfcee..ef3e0fcb 100644
--- a/src/routes/stack/add.rs
+++ b/src/routes/stack/add.rs
@@ -16,6 +16,7 @@ use std::str;
 use tracing::Instrument;
 use uuid::Uuid;
 use crate::helpers::stack::builder::DcBuilder;
+use crate::helpers::stack::dctypes::Compose;
 
 #[tracing::instrument(name = "Add stack.")]
 #[post("")]
@@ -147,14 +148,22 @@ pub async fn gen(
 
     match stack {
         Some(stack) => {
-            let mut dc = DcBuilder::new();
-            let fc = dc.build(stack);
-            tracing::debug!("Docker compose file content {:?}", fc.unwrap());
+            let id = stack.id.clone();
+            let mut dc = DcBuilder::new(stack);
+            let fc = dc.build();
+            // tracing::debug!("Docker compose file content {:?}", fc.unwrap());
+            return Ok(Json(JsonResponse::new(
+                "OK".to_owned(),
+                "Success".to_owned(),
+                200,
+                Some(id),
+                Some(fc.unwrap()),
+                None
+            )));
+
         }
         None => {
-
+            return Ok(Json(JsonResponse::internal_error("Could not generate compose file")));
         }
     }
-
-    Ok(Json(JsonResponse::<Stack>::ok(1, "OK")))
 }

From 327bc563a1ab8fc2dee5451e57b7d4b7d9f396d5 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Tue, 7 Nov 2023 16:25:00 +0200
Subject: [PATCH 3/5] exclude ./files from git

---
 .gitignore                                    |  1 +
 .idea/stacker.iml                             |  2 ++
 files/1.yml                                   | 20 -------------------
 .../c60d329f-9159-48b7-abf3-10ccabbca213.yml  | 20 -------------------
 4 files changed, 3 insertions(+), 40 deletions(-)
 delete mode 100644 files/1.yml
 delete mode 100644 files/c60d329f-9159-48b7-abf3-10ccabbca213.yml

diff --git a/.gitignore b/.gitignore
index c5078494..3acd8afb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 target
 .idea
+files
diff --git a/.idea/stacker.iml b/.idea/stacker.iml
index 227e58a7..a97e9259 100644
--- a/.idea/stacker.iml
+++ b/.idea/stacker.iml
@@ -4,6 +4,8 @@
     <content url="file://$MODULE_DIR$">
       <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
       <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/.idea/dataSources" />
+      <excludeFolder url="file://$MODULE_DIR$/files" />
       <excludeFolder url="file://$MODULE_DIR$/target" />
     </content>
     <orderEntry type="inheritedJdk" />
diff --git a/files/1.yml b/files/1.yml
deleted file mode 100644
index c6e60d64..00000000
--- a/files/1.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-version: '3.8'
-services:
-  smarty-bot:
-    image: trydirect/smarty-bot:latest
-    ports:
-    - target: 8000
-      published: 8000
-    restart: always
-  pgrst:
-    image: pgrst
-    ports:
-    - target: 9999
-      published: 9999
-    restart: always
-  portainer_ce_feature:
-    image: portainer-ce-feature
-    ports:
-    - target: 9000
-      published: 9000
-    restart: always
diff --git a/files/c60d329f-9159-48b7-abf3-10ccabbca213.yml b/files/c60d329f-9159-48b7-abf3-10ccabbca213.yml
deleted file mode 100644
index c6e60d64..00000000
--- a/files/c60d329f-9159-48b7-abf3-10ccabbca213.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-version: '3.8'
-services:
-  smarty-bot:
-    image: trydirect/smarty-bot:latest
-    ports:
-    - target: 8000
-      published: 8000
-    restart: always
-  pgrst:
-    image: pgrst
-    ports:
-    - target: 9999
-      published: 9999
-    restart: always
-  portainer_ce_feature:
-    image: portainer-ce-feature
-    ports:
-    - target: 9000
-      published: 9000
-    restart: always

From 9726e6d97c0a2304006769ebd283582857f0c81a Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Sun, 12 Nov 2023 11:24:44 +0200
Subject: [PATCH 4/5] generate docker compose, response builder, dctypes

---
 Cargo.lock                   | 508 ++++++++++++++++++++++++++++++++++-
 Cargo.toml                   |   4 +-
 configuration.yaml           |   7 +
 src/configuration.rs         |  17 ++
 src/forms/stack.rs           |  36 ++-
 src/helpers/json.rs          |  95 +++++--
 src/helpers/stack/builder.rs |   4 +-
 src/models/stack.rs          |  10 +
 src/routes/stack/add.rs      |  65 +----
 src/routes/stack/compose.rs  |  71 +++++
 src/routes/stack/deploy.rs   | 124 ++++++++-
 src/routes/stack/get.rs      |  28 +-
 src/routes/stack/mod.rs      |   2 +
 src/startup.rs               |   3 +-
 14 files changed, 873 insertions(+), 101 deletions(-)
 create mode 100644 src/routes/stack/compose.rs

diff --git a/Cargo.lock b/Cargo.lock
index 68ef0eb4..332a9ecc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -279,6 +279,54 @@ version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
 
+[[package]]
+name = "amq-protocol"
+version = "7.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d40d8b2465c7959dd40cee32ba6ac334b5de57e9fca0cc756759894a4152a5d"
+dependencies = [
+ "amq-protocol-tcp",
+ "amq-protocol-types",
+ "amq-protocol-uri",
+ "cookie-factory",
+ "nom",
+ "serde",
+]
+
+[[package]]
+name = "amq-protocol-tcp"
+version = "7.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cb2100adae7da61953a2c3a01935d86caae13329fadce3333f524d6d6ce12e2"
+dependencies = [
+ "amq-protocol-uri",
+ "tcp-stream",
+ "tracing",
+]
+
+[[package]]
+name = "amq-protocol-types"
+version = "7.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "156ff13c8a3ced600b4e54ed826a2ae6242b6069d00dd98466827cef07d3daff"
+dependencies = [
+ "cookie-factory",
+ "nom",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "amq-protocol-uri"
+version = "7.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "751bbd7d440576066233e740576f1b31fdc6ab86cfabfbd48c548de77eca73e4"
+dependencies = [
+ "amq-protocol-types",
+ "percent-encoding",
+ "url",
+]
+
 [[package]]
 name = "android-tzdata"
 version = "0.1.1"
@@ -294,6 +342,104 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0"
+dependencies = [
+ "async-lock",
+ "async-task",
+ "concurrent-queue",
+ "fastrand 2.0.1",
+ "futures-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776"
+dependencies = [
+ "async-channel",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "blocking",
+ "futures-lite",
+ "once_cell",
+]
+
+[[package]]
+name = "async-global-executor-trait"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33dd14c5a15affd2abcff50d84efd4009ada28a860f01c14f9d654f3e81b3f75"
+dependencies = [
+ "async-global-executor",
+ "async-trait",
+ "executor-trait",
+]
+
+[[package]]
+name = "async-io"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
+dependencies = [
+ "async-lock",
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-lite",
+ "log",
+ "parking",
+ "polling",
+ "rustix 0.37.27",
+ "slab",
+ "socket2 0.4.9",
+ "waker-fn",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
+dependencies = [
+ "event-listener",
+]
+
+[[package]]
+name = "async-reactor-trait"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6012d170ad00de56c9ee354aef2e358359deb1ec504254e0e5a3774771de0e"
+dependencies = [
+ "async-io",
+ "async-trait",
+ "futures-core",
+ "reactor-trait",
+]
+
+[[package]]
+name = "async-task"
+version = "4.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1"
+
 [[package]]
 name = "async-trait"
 version = "0.1.74"
@@ -314,6 +460,12 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -368,6 +520,31 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a"
+dependencies = [
+ "async-channel",
+ "async-lock",
+ "async-task",
+ "fastrand 2.0.1",
+ "futures-io",
+ "futures-lite",
+ "piper",
+ "tracing",
+]
+
 [[package]]
 name = "brotli"
 version = "3.4.0"
@@ -416,6 +593,15 @@ dependencies = [
  "bytes",
 ]
 
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
 [[package]]
 name = "cc"
 version = "1.0.83"
@@ -448,6 +634,25 @@ dependencies = [
  "windows-targets",
 ]
 
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "config"
 version = "0.13.3"
@@ -484,6 +689,12 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "cookie-factory"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
+
 [[package]]
 name = "core-foundation"
 version = "0.9.3"
@@ -650,6 +861,15 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "des"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e"
+dependencies = [
+ "cipher",
+]
+
 [[package]]
 name = "digest"
 version = "0.10.7"
@@ -687,6 +907,12 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
 
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
 [[package]]
 name = "dotenvy"
 version = "0.15.7"
@@ -733,6 +959,24 @@ version = "2.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
 
+[[package]]
+name = "executor-trait"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a1052dd43212a7777ec6a69b117da52f5e52f07aec47d00c1a2b33b85d06b08"
+dependencies = [
+ "async-trait",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
 [[package]]
 name = "fastrand"
 version = "2.0.1"
@@ -755,6 +999,18 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "flume"
+version = "0.10.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "pin-project",
+ "spin 0.9.8",
+]
+
 [[package]]
 name = "fnv"
 version = "1.0.7"
@@ -812,6 +1068,27 @@ dependencies = [
  "parking_lot 0.11.2",
 ]
 
+[[package]]
+name = "futures-io"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
+
+[[package]]
+name = "futures-lite"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
+dependencies = [
+ "fastrand 1.9.0",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
 [[package]]
 name = "futures-sink"
 version = "0.3.28"
@@ -1099,6 +1376,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -1108,6 +1395,17 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "io-lifetimes"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys",
+]
+
 [[package]]
 name = "ipnet"
 version = "2.8.0"
@@ -1173,6 +1471,29 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
 
+[[package]]
+name = "lapin"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f3067a1fcfbc3fc46455809c023e69b8f6602463201010f4ae5a3b572adb9dc"
+dependencies = [
+ "amq-protocol",
+ "async-global-executor-trait",
+ "async-reactor-trait",
+ "async-trait",
+ "executor-trait",
+ "flume",
+ "futures-core",
+ "futures-io",
+ "parking_lot 0.12.1",
+ "pinky-swear",
+ "reactor-trait",
+ "serde",
+ "serde_json",
+ "tracing",
+ "waker-fn",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -1191,6 +1512,12 @@ version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.10"
@@ -1420,6 +1747,29 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 
+[[package]]
+name = "p12"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4873306de53fe82e7e484df31e1e947d61514b6ea2ed6cd7b45d63006fd9224"
+dependencies = [
+ "cbc",
+ "cipher",
+ "des",
+ "getrandom",
+ "hmac",
+ "lazy_static",
+ "rc2",
+ "sha1",
+ "yasna",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
+
 [[package]]
 name = "parking_lot"
 version = "0.11.2"
@@ -1563,12 +1913,51 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
+[[package]]
+name = "pinky-swear"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d894b67aa7a4bf295db5e85349078c604edaa6fa5c8721e8eca3c7729a27f2ac"
+dependencies = [
+ "doc-comment",
+ "flume",
+ "parking_lot 0.12.1",
+ "tracing",
+]
+
+[[package]]
+name = "piper"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
+dependencies = [
+ "atomic-waker",
+ "fastrand 2.0.1",
+ "futures-io",
+]
+
 [[package]]
 name = "pkg-config"
 version = "0.3.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
 
+[[package]]
+name = "polling"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
+dependencies = [
+ "autocfg",
+ "bitflags 1.3.2",
+ "cfg-if",
+ "concurrent-queue",
+ "libc",
+ "log",
+ "pin-project-lite",
+ "windows-sys",
+]
+
 [[package]]
 name = "powerfmt"
 version = "0.2.0"
@@ -1653,6 +2042,26 @@ dependencies = [
  "getrandom",
 ]
 
+[[package]]
+name = "rc2"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "reactor-trait"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "438a4293e4d097556730f4711998189416232f009c137389e0f961d2bc0ddc58"
+dependencies = [
+ "async-trait",
+ "futures-core",
+ "futures-io",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.2.16"
@@ -1838,6 +2247,20 @@ dependencies = [
  "semver",
 ]
 
+[[package]]
+name = "rustix"
+version = "0.37.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
+dependencies = [
+ "bitflags 1.3.2",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys 0.3.8",
+ "windows-sys",
+]
+
 [[package]]
 name = "rustix"
 version = "0.38.19"
@@ -1847,7 +2270,7 @@ dependencies = [
  "bitflags 2.4.1",
  "errno",
  "libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.4.10",
  "windows-sys",
 ]
 
@@ -1863,6 +2286,42 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "rustls"
+version = "0.21.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c"
+dependencies = [
+ "log",
+ "ring 0.17.4",
+ "rustls-webpki",
+ "sct",
+]
+
+[[package]]
+name = "rustls-connector"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "060bcc1795b840d0e56d78f3293be5f652aa1611d249b0e63ffe19f4a8c9ae23"
+dependencies = [
+ "log",
+ "rustls 0.21.8",
+ "rustls-native-certs",
+ "rustls-webpki",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile",
+ "schannel",
+ "security-framework",
+]
+
 [[package]]
 name = "rustls-pemfile"
 version = "1.0.3"
@@ -1872,6 +2331,16 @@ dependencies = [
  "base64 0.21.4",
 ]
 
+[[package]]
+name = "rustls-webpki"
+version = "0.101.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
+dependencies = [
+ "ring 0.17.4",
+ "untrusted 0.9.0",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.15"
@@ -2118,6 +2587,9 @@ name = "spin"
 version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
 
 [[package]]
 name = "sqlformat"
@@ -2177,7 +2649,7 @@ dependencies = [
  "paste",
  "percent-encoding",
  "rand",
- "rustls",
+ "rustls 0.20.9",
  "rustls-pemfile",
  "serde",
  "serde_json",
@@ -2238,8 +2710,10 @@ dependencies = [
  "chrono",
  "config",
  "derive_builder",
+ "futures-lite",
  "glob",
  "indexmap 2.1.0",
+ "lapin",
  "rand",
  "regex",
  "reqwest",
@@ -2325,6 +2799,18 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "tcp-stream"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4da30af7998f51ee1aa48ab24276fe303a697b004e31ff542b192c088d5630a5"
+dependencies = [
+ "cfg-if",
+ "p12",
+ "rustls-connector",
+ "rustls-pemfile",
+]
+
 [[package]]
 name = "tempfile"
 version = "3.8.0"
@@ -2332,9 +2818,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
 dependencies = [
  "cfg-if",
- "fastrand",
+ "fastrand 2.0.1",
  "redox_syscall 0.3.5",
- "rustix",
+ "rustix 0.38.19",
  "windows-sys",
 ]
 
@@ -2469,7 +2955,7 @@ version = "0.23.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
 dependencies = [
- "rustls",
+ "rustls 0.20.9",
  "tokio",
  "webpki",
 ]
@@ -2714,6 +3200,12 @@ version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
+[[package]]
+name = "waker-fn"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690"
+
 [[package]]
 name = "want"
 version = "0.3.1"
@@ -2956,6 +3448,12 @@ dependencies = [
  "linked-hash-map",
 ]
 
+[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+
 [[package]]
 name = "zstd"
 version = "0.12.4"
diff --git a/Cargo.toml b/Cargo.toml
index 581f4d5a..4112688c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,6 +38,8 @@ rand = "0.8.5"
 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"
 
 [dependencies.sqlx]
 version = "0.6.3"
@@ -56,4 +58,4 @@ default = ["indexmap"]
 indexmap = ["dep:indexmap"]
 
 [dev-dependencies]
-glob = "0.3"
\ No newline at end of file
+glob = "0.3"
diff --git a/configuration.yaml b/configuration.yaml
index 1de0778b..4f88a4cb 100644
--- a/configuration.yaml
+++ b/configuration.yaml
@@ -9,3 +9,10 @@ database:
   username: postgres
   password: postgres
   database_name: stacker
+
+amqp:
+  host: 51.15.74.139
+  port: 5672
+  username: guest
+  password: wua4Eeyi
+
diff --git a/src/configuration.rs b/src/configuration.rs
index a3beeaf4..90d22c92 100644
--- a/src/configuration.rs
+++ b/src/configuration.rs
@@ -7,6 +7,7 @@ pub struct Settings {
     pub app_host: String,
     pub auth_url: String,
     pub max_clients_number: i64,
+    pub amqp: AmqpSettings
 }
 
 #[derive(Debug, serde::Deserialize)]
@@ -18,6 +19,13 @@ pub struct DatabaseSettings {
     pub database_name: String,
 }
 
+#[derive(Debug, serde::Deserialize)]
+pub struct AmqpSettings {
+    pub username: String,
+    pub password: String,
+    pub host: String,
+    pub port: u16,
+}
 impl DatabaseSettings {
     // Connection string: postgresql://<username>:<password>@<host>:<port>/<database_name>
     pub fn connection_string(&self) -> String {
@@ -35,6 +43,15 @@ impl DatabaseSettings {
     }
 }
 
+impl AmqpSettings {
+    pub fn connection_string(&self) -> String {
+        format!(
+            "amqp://{}:{}@{}:{}/%2f",
+            self.username, self.password, self.host, self.port,
+        )
+    }
+}
+
 pub fn get_configuration() -> Result<Settings, config::ConfigError> {
     // Initialize our configuration reader
     let mut settings = config::Config::default();
diff --git a/src/forms/stack.rs b/src/forms/stack.rs
index 93a1be85..ce9105e8 100644
--- a/src/forms/stack.rs
+++ b/src/forms/stack.rs
@@ -3,8 +3,42 @@ use serde_json::Value;
 use serde_valid::Validate;
 
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
-#[serde(rename_all = "camelCase")]
+#[serde(rename_all = "snake_case")]
 pub struct StackForm {
+    #[serde(rename= "commonDomain")]
+    pub common_domain: String,
+    pub domain_list: Option<DomainList>,
+    pub region: String,
+    pub zone: Option<String>,
+    pub server: String,
+    pub os: String,
+    pub ssl: String,
+    pub vars: Option<Vec<Var>>,
+    #[serde(rename = "integrated_features")]
+    pub integrated_features: Option<Vec<Value>>,
+    #[serde(rename = "extended_features")]
+    pub extended_features: Option<Vec<Value>>,
+    pub subscriptions: Option<Vec<String>>,
+    #[serde(rename = "save_token")]
+    pub save_token: bool,
+    #[serde(rename = "cloud_token")]
+    pub cloud_token: String,
+    pub provider: String,
+    #[serde(rename = "stack_code")]
+    pub stack_code: String,
+    #[serde(rename = "selected_plan")]
+    pub selected_plan: String,
+    pub custom: Custom,
+}
+
+
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
+#[serde(rename_all = "snake_case")]
+pub struct StackPayload {
+    pub(crate) user_token: Option<String>,
+    pub(crate) user_email: Option<String>,
+    pub(crate) installation_id: Option<u32>,
+    #[serde(rename= "commonDomain")]
     pub common_domain: String,
     pub domain_list: Option<DomainList>,
     pub region: String,
diff --git a/src/helpers/json.rs b/src/helpers/json.rs
index ad183050..de5dfd9f 100644
--- a/src/helpers/json.rs
+++ b/src/helpers/json.rs
@@ -1,8 +1,8 @@
-use actix_web::{HttpRequest, HttpResponse, Responder};
+use actix_web::{Responder, Result};
 use serde_derive::Serialize;
-use crate::models;
+use actix_web::web;
 
-#[derive(Serialize, Default)]
+#[derive(Serialize)]
 pub(crate) struct JsonResponse<T> {
     pub(crate) status: String,
     pub(crate) message: String,
@@ -23,7 +23,67 @@ pub(crate) struct JsonResponse<T> {
 //     pub(crate) list: Option<Vec<T>>
 // }
 
-impl<T> JsonResponse<T> {
+#[derive(Serialize, Default)]
+pub struct JsonResponseBuilder<T>
+    where T: serde::Serialize + Default
+{
+    status: String,
+    message: String,
+    code: u32,
+    id: Option<i32>,
+    item: Option<T>,
+    list: Option<Vec<T>>
+}
+
+impl<T> JsonResponseBuilder<T>
+where T: serde::Serialize + Default
+{
+   fn new() -> Self {
+       Self::default()
+   }
+
+    fn set_item(mut self, item:T) -> Self {
+        self.item = Some(item);
+        self
+    }
+
+
+    fn set_list(mut self, list:Vec<T>) -> Self {
+        self.list = Some(list);
+        self
+    }
+
+    pub(crate) fn ok(self) -> Result<impl Responder>  {
+
+        Ok(web::Json(
+            JsonResponse {
+                status: self.status,
+                message: self.message,
+                code: self.code,
+                id: self.id,
+                item: self.item,
+                list: self.list,
+            }
+        ))
+    }
+}
+
+impl<T> From<T> for JsonResponseBuilder<T>
+    where T: serde::Serialize + Default {
+    fn from(value: T) -> Self {
+        JsonResponseBuilder::default().set_item(value)
+    }
+}
+
+impl<T> From<Vec<T>> for JsonResponseBuilder<T>
+where T: serde::Serialize + Default {
+    fn from(value: Vec<T>) -> Self {
+        JsonResponseBuilder::default().set_list(value)
+    }
+}
+
+impl<T> JsonResponse<T>
+{
     pub(crate) fn new(status: String,
                       message: String,
                       code: u32,
@@ -55,8 +115,7 @@ impl<T> JsonResponse<T> {
             message: msg,
             code: 200,
             id: Some(id),
-            item: None,
-            list: None
+            ..Default::default()
         }
     }
 
@@ -65,9 +124,7 @@ impl<T> JsonResponse<T> {
             status: "Error".to_string(),
             code: 404,
             message: format!("Object not found"),
-            id: None,
-            item: None,
-            list: None
+            ..Default::default()
         }
     }
 
@@ -83,9 +140,7 @@ impl<T> JsonResponse<T> {
             status: "Error".to_string(),
             code: 500,
             message: msg,
-            id: None,
-            item: None,
-            list: None
+            ..Default::default()
         }
     }
 
@@ -101,9 +156,19 @@ impl<T> JsonResponse<T> {
             status: "Error".to_string(),
             code: 400,
             message: msg,
-            id: None,
-            item: None,
-            list: None
+            ..Default::default()
+        }
+    }
+}
+
+impl<T> Default for JsonResponse<T> {
+
+    fn default() -> Self {
+        JsonResponse {
+
+            status: "200".to_string(),
+            message: "OK".to_string(),
+            ..Default::default()
         }
     }
 }
diff --git a/src/helpers/stack/builder.rs b/src/helpers/stack/builder.rs
index 1ee5fe9b..a6c9fd99 100644
--- a/src/helpers/stack/builder.rs
+++ b/src/helpers/stack/builder.rs
@@ -30,7 +30,7 @@ impl Default for Config {
 #[derive(Clone, Debug)]
 pub struct DcBuilder {
     config: Config,
-    stack: Stack
+    pub(crate) stack: Stack
 }
 
 impl TryInto<Vec<Port>> for Web {
@@ -88,10 +88,10 @@ impl DcBuilder {
                 println!("stack item {:?}", apps.custom.web);
 
                 for app in apps.custom.web {
-                    // println!("app name {:?}", app.name);
                     let tag = "latest";
                     let img= format!("{}/{}:{}",app.dockerhub_user, app.dockerhub_name, tag);
                     let code = app.code.clone().to_owned();
+
                     let mut service = Service {
                         image: Some(img.to_string()),
                         ..Default::default()
diff --git a/src/models/stack.rs b/src/models/stack.rs
index 9ce7370e..f9ce272e 100644
--- a/src/models/stack.rs
+++ b/src/models/stack.rs
@@ -14,3 +14,13 @@ pub struct Stack {
     pub created_at: DateTime<Utc>,
     pub updated_at: DateTime<Utc>,
 }
+
+impl Default for Stack {
+    fn default() -> Self {
+        Stack {
+            user_id: "".to_string(),
+            name: "".to_string(),
+                ..Default::default()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs
index ef3e0fcb..64c65cb6 100644
--- a/src/routes/stack/add.rs
+++ b/src/routes/stack/add.rs
@@ -15,8 +15,7 @@ use sqlx::PgPool;
 use std::str;
 use tracing::Instrument;
 use uuid::Uuid;
-use crate::helpers::stack::builder::DcBuilder;
-use crate::helpers::stack::dctypes::Compose;
+
 
 #[tracing::instrument(name = "Add stack.")]
 #[post("")]
@@ -28,14 +27,12 @@ pub async fn add(
 
     let body_bytes = actix_web::body::to_bytes(body).await.unwrap();
     let body_str = str::from_utf8(&body_bytes).unwrap();
-    // method 1 let app_state: AppState = serde_json::from_str(body_str).unwrap();
-    // method 2 let app_state = serde_json::from_str::<AppState>(body_str).unwrap();
     let form = match serde_json::from_str::<StackForm>(body_str) {
         Ok(f) => {
             f
         }
-        Err(err) => {
-            return Ok(Json(JsonResponse::<Stack>::not_valid("")));
+        Err(_err) => {
+            return Ok(Json(JsonResponse::<Stack>::not_valid("Invalid data")));
         }
     };
 
@@ -111,59 +108,3 @@ pub async fn add(
     };
 }
 
-#[tracing::instrument(name = "Generate docker-compose.")]
-#[post("/{id}")]
-pub async fn gen(
-    user: web::ReqData<User>,
-    path: web::Path<(i32,)>,
-    pool: Data<PgPool>,
-) -> Result<impl Responder> {
-    let id = path.0;
-    tracing::debug!("Received id: {}", id);
-
-    let stack = match sqlx::query_as!(
-        Stack,
-        r#"
-        SELECT * FROM user_stack WHERE id=$1 AND user_id=$2 LIMIT 1
-        "#,
-        id, user.id
-    )
-            .fetch_one(pool.get_ref())
-            .await
-        {
-            Ok(stack) => {
-                tracing::info!("stack found: {:?}", stack.id,);
-                Some(stack)
-            }
-            Err(sqlx::Error::RowNotFound) => {
-                tracing::error!("Row not found 404");
-                None
-            }
-            Err(e) => {
-                tracing::error!("Failed to fetch stack, error: {:?}", e);
-                None
-            }
-        };
-
-
-    match stack {
-        Some(stack) => {
-            let id = stack.id.clone();
-            let mut dc = DcBuilder::new(stack);
-            let fc = dc.build();
-            // tracing::debug!("Docker compose file content {:?}", fc.unwrap());
-            return Ok(Json(JsonResponse::new(
-                "OK".to_owned(),
-                "Success".to_owned(),
-                200,
-                Some(id),
-                Some(fc.unwrap()),
-                None
-            )));
-
-        }
-        None => {
-            return Ok(Json(JsonResponse::internal_error("Could not generate compose file")));
-        }
-    }
-}
diff --git a/src/routes/stack/compose.rs b/src/routes/stack/compose.rs
new file mode 100644
index 00000000..dc3230d4
--- /dev/null
+++ b/src/routes/stack/compose.rs
@@ -0,0 +1,71 @@
+use actix_web::{
+    web,
+    web::{Data, Json},
+    Responder, Result,
+};
+
+use crate::helpers::JsonResponse;
+use crate::models::user::User;
+use crate::models::Stack;
+use actix_web::post;
+use sqlx::PgPool;
+use std::str;
+use tracing::Instrument;
+use crate::helpers::stack::builder::DcBuilder;
+
+#[tracing::instrument(name = "Generate docker-compose.")]
+#[post("/{id}")]
+pub async fn add(
+    user: web::ReqData<User>,
+    path: web::Path<(i32,)>,
+    pool: Data<PgPool>,
+) -> Result<impl Responder> {
+    let id = path.0;
+    tracing::debug!("Received id: {}", id);
+
+    let stack = match sqlx::query_as!(
+        Stack,
+        r#"
+        SELECT * FROM user_stack WHERE id=$1 AND user_id=$2 LIMIT 1
+        "#,
+        id, user.id
+    )
+        .fetch_one(pool.get_ref())
+        .await
+    {
+        Ok(stack) => {
+            tracing::info!("stack found: {:?}", stack.id,);
+            Some(stack)
+        }
+        Err(sqlx::Error::RowNotFound) => {
+            tracing::error!("Row not found 404");
+            None
+        }
+        Err(e) => {
+            tracing::error!("Failed to fetch stack, error: {:?}", e);
+            None
+        }
+    };
+
+
+    match stack {
+        Some(stack) => {
+            let id = stack.id.clone();
+            let mut dc = DcBuilder::new(stack);
+            let fc = dc.build();
+            // tracing::debug!("Docker compose file content {:?}", fc.unwrap());
+            return Ok(Json(JsonResponse::new(
+                "OK".to_owned(),
+                "Success".to_owned(),
+                200,
+                Some(id),
+                Some(fc.unwrap()),
+                None
+            )));
+
+        }
+        None => {
+            return Ok(Json(JsonResponse::internal_error("Could not generate compose file")));
+        }
+    }
+}
diff --git a/src/routes/stack/deploy.rs b/src/routes/stack/deploy.rs
index 67edaa81..1c871e5c 100644
--- a/src/routes/stack/deploy.rs
+++ b/src/routes/stack/deploy.rs
@@ -1,5 +1,121 @@
-use actix_web::HttpResponse;
+use std::sync::Arc;
+use actix_web::{
+    web,
+    post,
+    web::{Data, Json},
+    Responder, Result,
+};
+use crate::models::user::User;
+use crate::models::stack::Stack;
+use sqlx::PgPool;
+use lapin::{
+    options::*, publisher_confirm::Confirmation, types::FieldTable, BasicProperties, Connection,
+    ConnectionProperties
+};
+use crate::configuration::Settings;
+use crate::helpers::JsonResponse;
+use crate::helpers::stack::builder::DcBuilder;
+use futures_lite::stream::StreamExt;
+use serde::Serialize;
+use crate::forms::{StackForm, StackPayload};
 
-pub async fn deploy() -> HttpResponse {
-    unimplemented!()
-}
\ No newline at end of file
+
+#[derive(Serialize, Debug, Clone)]
+struct Payload {
+    user_token: String,
+    user_email: String,
+    installation_id: String,
+}
+
+
+#[tracing::instrument(name = "Deploy.")]
+#[post("/{id}/deploy")]
+pub async fn add(
+    user: web::ReqData<User>,
+    path: web::Path<(i32,)>,
+    pool: Data<PgPool>,
+    sets: Data<Arc<Settings>>,
+) -> Result<impl Responder> {
+    let id = path.0;
+    tracing::debug!("Received id: {}", id);
+
+    let stack = match sqlx::query_as!(
+        Stack,
+        r#"
+        SELECT * FROM user_stack WHERE id=$1 AND user_id=$2 LIMIT 1
+        "#,
+        id, user.id
+    )
+        .fetch_one(pool.get_ref())
+        .await
+    {
+        Ok(stack) => {
+            tracing::info!("Stack found: {:?}", stack.id,);
+            Some(stack)
+        }
+        Err(sqlx::Error::RowNotFound) => {
+            tracing::error!("Row not found 404");
+            None
+        }
+        Err(e) => {
+            tracing::error!("Failed to fetch stack, error: {:?}", e);
+            None
+        }
+    };
+
+    return match stack {
+        Some(stack) => {
+            let id = stack.id.clone();
+            let mut dc = DcBuilder::new(stack);
+            dc.build();
+
+            let addr = sets.amqp.connection_string();
+            let routing_key = "install.start.tfa.all.all".to_string();
+            tracing::debug!("Sending message to {:?}", routing_key);
+
+            let conn = Connection::connect(&addr, ConnectionProperties::default())
+                .await
+                .unwrap();
+
+            tracing::info!("RABBITMQ CONNECTED");
+
+            let channel = conn.create_channel().await.unwrap();
+            let mut stack_data = serde_json::from_value::<StackPayload>(
+                dc.stack.body.clone()
+            ).unwrap();
+
+            stack_data.installation_id = Some(1);
+            stack_data.user_token = Some(user.id.clone());
+            stack_data.user_email= Some(user.email.clone());
+
+            let payload = serde_json::to_string::<StackPayload>(&stack_data).unwrap();
+            let _payload = payload.as_bytes();
+
+            let confirm = channel
+                .basic_publish(
+                    "install",
+                    routing_key.as_str(),
+                    BasicPublishOptions::default(),
+                    _payload,
+                    BasicProperties::default(),
+                )
+                .await.unwrap()
+                .await.unwrap();
+
+            assert_eq!(confirm, Confirmation::NotRequested);
+            tracing::debug!("Message sent to rabbitmq");
+
+            Ok(Json(JsonResponse::<Stack>::new(
+                "OK".to_owned(),
+                "Success".to_owned(),
+                200,
+                Some(id),
+                None,
+                None
+            )))
+        }
+        None => {
+            Ok(Json(JsonResponse::internal_error("Deployment failed")))
+        }
+    }
+}
diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs
index b3ce3898..786d905f 100644
--- a/src/routes/stack/get.rs
+++ b/src/routes/stack/get.rs
@@ -1,8 +1,9 @@
 use actix_web::{web, get, Responder, Result};
 use sqlx::PgPool;
-use crate::helpers::JsonResponse;
+use crate::helpers::{JsonResponse, JsonResponseBuilder};
 use crate::models;
 use crate::models::user::User;
+use std::convert::From;
 
 
 #[tracing::instrument(name = "Get logged user stack.")]
@@ -29,20 +30,27 @@ pub async fn get(
     {
         Ok(stack) => {
             tracing::info!("stack found: {:?}", stack.id,);
-            return Ok(web::Json(JsonResponse::<models::Stack>::new(
-                "Success".to_string(),
-                "".to_string(),
-                200,
-                Some(stack.id),
-                Some(stack),
-                None)));
+            // return Ok(web::Json(JsonResponse::<models::Stack>::new(
+            //     "Success".to_string(),
+            //     "".to_string(),
+            //     200,
+            //     Some(stack.id),
+            //     Some(stack),
+            //     None)));
+            let response_builder:JsonResponseBuilder<models::Stack> = From::from(stack);
+            // let response: JsonResponse<models::Stack> = From::<models::Stack>::from(stack).build();
+            return response_builder.ok();
         }
         Err(sqlx::Error::RowNotFound) => {
-            return Ok(web::Json(JsonResponse::<models::Stack>::not_found()));
+            // return Ok(web::Json(JsonResponse::<models::Stack>::not_found()));
+            let response_builder:JsonResponseBuilder<models::Stack> =JsonResponseBuilder::default();
+            return response_builder.ok();
         }
         Err(e) => {
             tracing::error!("Failed to fetch stack, error: {:?}", e);
-            return Ok(web::Json(JsonResponse::<models::Stack>::internal_error("")));
+            // return Ok(web::Json(JsonResponse::<models::Stack>::internal_error("")));
+            let response_builder:JsonResponseBuilder<models::Stack> = JsonResponseBuilder::default();
+            return response_builder.ok();
         }
     }
 }
diff --git a/src/routes/stack/mod.rs b/src/routes/stack/mod.rs
index f3e5bc95..27c80617 100644
--- a/src/routes/stack/mod.rs
+++ b/src/routes/stack/mod.rs
@@ -2,6 +2,8 @@ pub mod add;
 pub mod deploy;
 pub mod get;
 pub mod update;
+pub(crate) mod compose;
+
 pub use add::*;
 pub use update::*;
 pub use deploy::*;
diff --git a/src/startup.rs b/src/startup.rs
index 773ce2c0..b7fd3243 100644
--- a/src/startup.rs
+++ b/src/startup.rs
@@ -127,8 +127,9 @@ pub async fn run(
                 web::scope("/stack")
                     .wrap(HttpAuthentication::bearer(bearer_guard))
                     .wrap(Cors::permissive())
+                    .service(crate::routes::stack::deploy::add)
                     .service(crate::routes::stack::add::add)
-                    .service(crate::routes::stack::add::gen)
+                    .service(crate::routes::stack::compose::add)
                     .service(crate::routes::stack::get::get)
             )
             .app_data(db_pool.clone())

From d5be426847f7299c38ad9611b29f0b003cc231d9 Mon Sep 17 00:00:00 2001
From: vsilent <jabberroid@gmail.com>
Date: Sun, 12 Nov 2023 11:25:51 +0200
Subject: [PATCH 5/5] rabbitmq config

---
 configuration.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/configuration.yaml b/configuration.yaml
index 4f88a4cb..9498400b 100644
--- a/configuration.yaml
+++ b/configuration.yaml
@@ -11,8 +11,8 @@ database:
   database_name: stacker
 
 amqp:
-  host: 51.15.74.139
+  host: 127.0.0.1
   port: 5672
   username: guest
-  password: wua4Eeyi
+  password:
 
