diff --git a/.gitignore b/.gitignore index c507849..3acd8af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target .idea +files diff --git a/.idea/stacker.iml b/.idea/stacker.iml index 227e58a..a97e925 100644 --- a/.idea/stacker.iml +++ b/.idea/stacker.iml @@ -4,6 +4,8 @@ + + diff --git a/Cargo.lock b/Cargo.lock index a4b8f33..2ac8449 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" @@ -562,6 +773,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 +817,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" @@ -584,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" @@ -621,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" @@ -645,6 +937,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" @@ -661,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" @@ -683,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" @@ -772,6 +1100,21 @@ 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-macro" version = "0.3.29" @@ -850,6 +1193,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" @@ -862,7 +1211,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -1030,6 +1379,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" @@ -1051,6 +1406,27 @@ 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 = "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" @@ -1060,6 +1436,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" @@ -1125,6 +1512,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" @@ -1143,6 +1553,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" @@ -1372,6 +1788,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" @@ -1515,12 +1954,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" @@ -1605,6 +2083,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" @@ -1790,6 +2288,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" @@ -1799,7 +2311,7 @@ dependencies = [ "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.10", "windows-sys", ] @@ -1815,6 +2327,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" @@ -1824,6 +2372,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" @@ -1933,7 +2491,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", @@ -1971,6 +2529,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" @@ -2057,6 +2628,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" @@ -2106,7 +2680,7 @@ dependencies = [ "hex", "hkdf", "hmac", - "indexmap", + "indexmap 1.9.3", "itoa", "libc", "log", @@ -2116,7 +2690,7 @@ dependencies = [ "paste", "percent-encoding", "rand", - "rustls", + "rustls 0.20.9", "rustls-pemfile", "serde", "serde_json", @@ -2177,9 +2751,14 @@ dependencies = [ "actix-web-httpauth", "chrono", "config", + "derive_builder", "futures", + "futures-lite", "futures-util", + "glob", "hmac", + "indexmap 2.1.0", + "lapin", "rand", "regex", "reqwest", @@ -2187,6 +2766,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_valid", + "serde_yaml", "sha2", "sqlx", "thiserror", @@ -2266,6 +2846,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" @@ -2273,9 +2865,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", ] @@ -2410,7 +3002,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", ] @@ -2598,6 +3190,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" @@ -2649,6 +3247,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" @@ -2891,6 +3495,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 9153031..0301748 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,13 @@ actix-http = "3.4.0" hmac = "0.12.1" sha2 = "0.10.8" +# dctypes +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" features = [ @@ -51,3 +58,10 @@ features = [ "json", "offline" ] + +[features] +default = ["indexmap"] +indexmap = ["dep:indexmap"] + +[dev-dependencies] +glob = "0.3" diff --git a/configuration.yaml b/configuration.yaml index 5099d3d..030dd49 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -9,3 +9,9 @@ database: username: postgres password: postgres database_name: stacker + +amqp: + host: 127.0.0.1 + port: 5672 + username: guest + password: guest diff --git a/docker-compose.yml b/docker-compose.yml index 2d3b934..26311c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ networks: services: stacker: - image: trydirect/stacker:0.0.3 + image: trydirect/stacker:0.0.4 build: . container_name: stacker restart: always diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index 1ba68f2..bdfdd89 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -7,7 +7,7 @@ volumes: services: stacker: - image: trydirect/stacker:0.0.3 + image: trydirect/stacker:0.0.4 build: . container_name: stacker restart: always diff --git a/migrations/20230905145525_creating_stack_tables.up.sql b/migrations/20230905145525_creating_stack_tables.up.sql index e908e97..b20b2cd 100644 --- a/migrations/20230905145525_creating_stack_tables.up.sql +++ b/migrations/20230905145525_creating_stack_tables.up.sql @@ -1,12 +1,13 @@ --- Add up migration script here --- Add migration script here CREATE TABLE user_stack ( - id serial, + id serial4 NOT NULL, stack_id uuid NOT NULL, user_id VARCHAR(50) NOT NULL, name TEXT NOT NULL UNIQUE, body JSON NOT NULL, created_at timestamptz NOT NULL, - updated_at timestamptz NOT NULL -) + updated_at timestamptz NOT NULL, + CONSTRAINT user_stack_pkey PRIMARY KEY (id) +); +CREATE INDEX idx_stack_id ON user_stack(stack_id); +CREATE INDEX idx_stack_user_id ON user_stack(user_id); \ No newline at end of file diff --git a/src/configuration.rs b/src/configuration.rs index a3beeaf..90d22c9 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://:@:/ 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 { // Initialize our configuration reader let mut settings = config::Config::default(); diff --git a/src/forms/stack.rs b/src/forms/stack.rs index 93a1be8..78a0847 100644 --- a/src/forms/stack.rs +++ b/src/forms/stack.rs @@ -1,10 +1,99 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_valid::Validate; +use std::fmt; + + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Role { + pub role: Option>, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Requirements { + #[validate(minimum=0.1)] + pub cpu: Option, + #[validate(min_length=1)] + #[validate(max_length=10)] + #[validate(pattern = r"^\d+G$")] + #[serde(rename = "disk_size")] + pub disk_size: Option, + #[serde(rename = "ram_size")] + #[validate(min_length=1)] + #[validate(max_length=10)] + #[validate(pattern = r"^\d+G$")] + pub ram_size: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Ports { + #[serde(rename(deserialize = "sharedPorts"))] + #[serde(rename(serialize = "shared_ports"))] + #[serde(alias = "shared_ports")] + pub shared_ports: Option>, + pub ports: Option>, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct DockerImage { + pub dockerhub_user: Option, + pub dockerhub_name: Option, + pub dockerhub_image: Option, +} + +impl fmt::Display for DockerImage +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let tag = "latest"; + + let dim = self.dockerhub_image.clone() + .unwrap_or("".to_string()); + write!(f, "{}/{}:{}", self.dockerhub_user.clone() + .unwrap_or("trydirect".to_string()).clone(), + self.dockerhub_name.clone().unwrap_or(dim), tag + ) + } +} + +impl AsRef for App { + fn as_ref(&self) -> &DockerImage { + &self.docker_image + } +} + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] -#[serde(rename_all = "camelCase")] pub struct StackForm { + #[serde(rename= "commonDomain")] + pub common_domain: Option, + pub domain_list: Option, + pub stack_code: Option, + pub region: String, + pub zone: Option, + pub server: String, + pub os: String, + pub ssl: String, + pub vars: Option>, + pub integrated_features: Option>, + pub extended_features: Option>, + pub subscriptions: Option>, + pub form_app: Option>, + pub disk_type: Option, + pub save_token: bool, + pub cloud_token: String, + pub provider: String, + 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) id: Option, + pub(crate) user_token: Option, + pub(crate) user_email: Option, + #[serde(rename= "commonDomain")] pub common_domain: String, pub domain_list: Option, pub region: String, @@ -18,12 +107,13 @@ pub struct StackForm { #[serde(rename = "extended_features")] pub extended_features: Option>, pub subscriptions: Option>, + pub form_app: Option>, + pub disk_type: Option, #[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, @@ -52,13 +142,11 @@ pub struct Custom { pub feature: Option>, pub service: Option>, #[serde(rename = "servers_count")] - pub servers_count: i64, - #[serde(rename = "custom_stack_name")] - pub custom_stack_name: String, + pub servers_count: u32, #[serde(rename = "custom_stack_code")] pub custom_stack_code: String, - #[serde(rename = "custom_stack_git_url")] - pub custom_stack_git_url: Option, + #[serde(rename = "project_git_url")] + pub project_git_url: Option, #[serde(rename = "custom_stack_category")] pub custom_stack_category: Option>, #[serde(rename = "custom_stack_short_description")] @@ -73,44 +161,8 @@ pub struct Custom { pub project_description: Option, } - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] -#[serde(rename_all = "camelCase")] -pub struct Web { - pub name: String, - pub code: String, - pub domain: Option, - pub shared_ports: Option>, - pub versions: Option>, - pub custom: bool, - #[serde(rename = "type")] - pub type_field: String, - pub main: bool, - #[serde(rename = "_id")] - pub id: String, - #[serde(rename = "dockerhub_user")] - pub dockerhub_user: String, - #[serde(rename = "dockerhub_name")] - pub dockerhub_name: String, - pub url_app: Option, - pub url_git: Option, - #[validate(min_length=1)] - #[validate(max_length=10)] - #[validate(pattern = r"^\d+G$")] - #[serde(rename = "disk_size")] - pub disk_size: String, - #[serde(rename = "ram_size")] - #[validate(min_length=1)] - #[validate(max_length=10)] - #[validate(pattern = r"^\d+G$")] - pub ram_size: String, - #[validate(minimum=0.1)] - pub cpu: f64, -} - #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] -#[serde(rename_all = "camelCase")] -pub struct Feature { +pub struct App { #[serde(rename = "_etag")] pub etag: Option, #[serde(rename = "_id")] @@ -121,57 +173,60 @@ pub struct Feature { pub updated: Option, pub name: String, pub code: String, - pub role: Vec, #[serde(rename = "type")] pub type_field: String, + #[serde(flatten)] + pub role: Role, pub default: Option, - pub popularity: Option, - pub descr: Option, + #[serde(flatten)] pub ports: Option, + pub versions: Option>, + #[serde(flatten)] + pub docker_image: DockerImage, + #[serde(flatten)] + pub requirements: Requirements, + pub popularity: Option, pub commercial: Option, pub subscription: Option, pub autodeploy: Option, pub suggested: Option, pub dependency: Option, - #[serde(rename = "avoid_render")] pub avoid_render: Option, pub price: Option, pub icon: Option, - #[serde(rename = "category_id")] + pub domain: Option, + pub main: bool, pub category_id: Option, - #[serde(rename = "parent_app_id")] pub parent_app_id: Option, - #[serde(rename = "full_description")] + pub descr: Option, pub full_description: Option, pub description: Option, - #[serde(rename = "plan_type")] pub plan_type: Option, - #[serde(rename = "ansible_var")] pub ansible_var: Option, - #[serde(rename = "repo_dir")] pub repo_dir: Option, - #[validate(min_length=1)] - pub cpu: String, - #[validate(min_length=1)] - #[serde(rename = "ram_size")] - pub ram_size: String, - #[validate(min_length=1)] - #[serde(rename = "disk_size")] - pub disk_size: String, - #[serde(rename = "dockerhub_image")] - pub dockerhub_image: Option, - pub versions: Option>, - pub domain: Option, - pub shared_ports: Option>, - pub main: bool, + pub url_app: Option, + pub url_git: Option, } -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Ports { - pub public: Vec, +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Web { + #[serde(flatten)] + pub app: App, + pub custom: Option, } +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Feature { + #[serde(flatten)] + pub app: App, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +#[serde(rename_all = "camelCase")] +pub struct Service { + #[serde(flatten)] + pub(crate) app: App, +} #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -199,13 +254,13 @@ pub struct Version { #[serde(rename = "_etag")] pub etag: Option, #[serde(rename = "_id")] - pub id: i64, + pub id: u32, #[serde(rename = "_created")] pub created: Option, #[serde(rename = "_updated")] pub updated: Option, #[serde(rename = "app_id")] - pub app_id: i64, + pub app_id: u32, pub name: String, pub version: String, #[serde(rename = "update_status")] @@ -213,61 +268,4 @@ pub struct Version { pub tag: String, } -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] -#[serde(rename_all = "camelCase")] -pub struct Service { - #[serde(rename = "_etag")] - pub etag: Option, - #[serde(rename = "_id")] - pub id: i64, - #[serde(rename = "_created")] - pub created: Option, - #[serde(rename = "_updated")] - pub updated: Option, - pub name: String, - pub code: String, - pub role: Option>, - #[serde(rename = "type")] - pub type_field: String, - pub default: Option, - pub popularity: Option, - pub descr: Option, - pub ports: Option, - pub commercial: Option, - pub subscription: Option, - pub autodeploy: Option, - pub suggested: Option, - pub dependency: Option, - #[serde(rename = "avoid_render")] - pub avoid_render: Option, - pub price: Option, - pub icon: Option, - #[serde(rename = "category_id")] - pub category_id: Option, - #[serde(rename = "parent_app_id")] - pub parent_app_id: Option, - #[serde(rename = "full_description")] - pub full_description: Option, - pub description: Option, - #[serde(rename = "plan_type")] - pub plan_type: Option, - #[serde(rename = "ansible_var")] - pub ansible_var: Option, - #[serde(rename = "repo_dir")] - pub repo_dir: Option, - #[validate(min_length=1)] - pub cpu: String, - #[serde(rename = "ram_size")] - #[validate(min_length=1)] - pub ram_size: String, - #[serde(rename = "disk_size")] - #[validate(min_length=1)] - pub disk_size: String, - #[serde(rename = "dockerhub_image")] - pub dockerhub_image: Option, - pub versions: Option>, - pub domain: String, - pub shared_ports: Option>, - pub main: bool, -} diff --git a/src/forms/user.rs b/src/forms/user.rs index 2da0dcf..8ff0b4c 100644 --- a/src/forms/user.rs +++ b/src/forms/user.rs @@ -16,45 +16,45 @@ pub struct User { #[serde(rename = "_id")] pub id: String, #[serde(rename = "first_name")] - pub first_name: String, + pub first_name: Option, #[serde(rename = "last_name")] - pub last_name: String, - pub created: String, - pub updated: String, + pub last_name: Option, + pub created: Option, + pub updated: Option, pub email: String, #[serde(rename = "email_confirmed")] pub email_confirmed: bool, pub social: bool, - pub website: String, + pub website: Option, pub currency: Value, - pub phone: String, + pub phone: Option, #[serde(rename = "password_change_required")] pub password_change_required: Value, - pub photo: String, - pub country: String, + pub photo: Option, + pub country: Option, #[serde(rename = "billing_first_name")] pub billing_first_name: Value, #[serde(rename = "billing_last_name")] pub billing_last_name: Value, #[serde(rename = "billing_postcode")] - pub billing_postcode: String, + pub billing_postcode: Option, #[serde(rename = "billing_address_1")] - pub billing_address_1: String, + pub billing_address_1: Option, #[serde(rename = "billing_address_2")] - pub billing_address_2: String, + pub billing_address_2: Option, #[serde(rename = "billing_city")] - pub billing_city: String, + pub billing_city: Option, #[serde(rename = "billing_country_code")] - pub billing_country_code: String, + pub billing_country_code: Option, #[serde(rename = "billing_country_area")] - pub billing_country_area: String, - pub tokens: Vec, - pub subscriptions: Vec, - pub plan: Plan, + pub billing_country_area: Option, + pub tokens: Option>, + pub subscriptions: Option>, + pub plan: Option, #[serde(rename = "deployments_left")] pub deployments_left: Value, #[serde(rename = "suspension_hints")] - pub suspension_hints: SuspensionHints, + pub suspension_hints: Option, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -72,9 +72,9 @@ pub struct Subscription { #[serde(rename = "user_id")] pub user_id: i64, #[serde(rename = "date_created")] - pub date_created: String, + pub date_created: Option, #[serde(rename = "date_updated")] - pub date_updated: String, + pub date_updated: Option, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -92,21 +92,21 @@ pub struct Plan { pub billing_email: String, #[serde(rename = "date_of_purchase")] pub date_of_purchase: String, - pub currency: String, - pub price: String, - pub period: String, + pub currency: Option, + pub price: Option, + pub period: Option, #[serde(rename = "date_start")] pub date_start: String, pub active: bool, #[serde(rename = "billing_id")] - pub billing_id: String, + pub billing_id: Option, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SupportedStacks { - pub monthly: i64, - pub annually: i64, + pub monthly: Option, + pub annually: Option, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -132,8 +132,8 @@ impl TryInto for UserForm { // )?; Ok(UserModel { id: self.user.id, - first_name: self.user.first_name, - last_name: self.user.last_name, + first_name: self.user.first_name.unwrap_or("Noname".to_string()), + last_name: self.user.last_name.unwrap_or("Noname".to_string()), email: self.user.email, email_confirmed: self.user.email_confirmed, }) diff --git a/src/helpers/json.rs b/src/helpers/json.rs index 80a21b3..87e7327 100644 --- a/src/helpers/json.rs +++ b/src/helpers/json.rs @@ -1,15 +1,15 @@ -use actix_web::error::{ErrorBadRequest, ErrorInternalServerError}; +use actix_web::error::{ErrorBadRequest, ErrorConflict, ErrorNotFound, ErrorInternalServerError}; +use serde_derive::Serialize; use actix_web::web::Json; use actix_web::Error; use actix_web::Result; -use serde_derive::Serialize; #[derive(Serialize)] pub(crate) struct JsonResponse { - message: String, - id: Option, - item: Option, - list: Option>, + pub(crate) message: String, + pub(crate) id: Option, + pub(crate) item: Option, + pub(crate) list: Option> } #[derive(Serialize, Default)] @@ -62,6 +62,45 @@ where )) } + pub(crate) fn not_found(self, msg: String) -> Result>, Error> { + + let json_response = JsonResponse { + message: msg, + id: self.id, + item: self.item, + list: self.list + }; + + Err(ErrorNotFound( + serde_json::to_string(&json_response).unwrap())) + } + + pub(crate) fn internal_error(self, msg: String) -> Result>, Error> { + + let json_response = JsonResponse { + message: msg, + id: self.id, + item: self.item, + list: self.list + }; + + Err(ErrorInternalServerError( + serde_json::to_string(&json_response).unwrap())) + } + + pub(crate) fn conflict(self, msg: String) -> Result>, Error> { + + let json_response = JsonResponse { + message: msg, + id: self.id, + item: self.item, + list: self.list + }; + + Err(ErrorConflict( + serde_json::to_string(&json_response).unwrap())) + } + pub(crate) fn err_internal_server_error>( self, msg: I, @@ -81,4 +120,75 @@ where pub fn build() -> JsonResponseBuilder { JsonResponseBuilder::default() } + + pub(crate) fn new(message: String, + id: Option, + item:Option, + list: Option>) -> Self { + tracing::debug!("Executed.."); + JsonResponse { + message, + id, + item, + list, + } + } + + // pub(crate) fn ok(id: i32, message: &str) -> JsonResponse { + // + // let msg = if !message.trim().is_empty() { + // message.to_string() + // } + // else{ + // String::from("Success") + // }; + // + // JsonResponse { + // message: msg, + // id: Some(id), + // item: None, + // list: None, + // } + // } + + // pub(crate) fn not_found() -> Self { + // JsonResponse { + // id: None, + // item: None, + // message: format!("Object not found"), + // list: None, + // } + // } + // + // pub(crate) fn internal_error(message: &str) -> Self { + // + // let msg = if !message.trim().is_empty() { + // message.to_string() + // } + // else{ + // String::from("Internal error") + // }; + // JsonResponse { + // id: None, + // item: None, + // message: msg, + // list: None, + // } + // } + // + // pub(crate) fn not_valid(message: &str) -> Self { + // + // let msg = if !message.trim().is_empty() { + // message.to_string() + // } + // else{ + // String::from("Validation error") + // }; + // JsonResponse { + // id: None, + // item: None, + // message: msg, + // list: None, + // } + // } } diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs index 203eb0d..e5439f7 100644 --- a/src/helpers/mod.rs +++ b/src/helpers/mod.rs @@ -1,4 +1,5 @@ pub mod client; pub(crate) mod json; +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 0000000..c319d8a --- /dev/null +++ b/src/helpers/stack/builder.rs @@ -0,0 +1,167 @@ +use crate::helpers::stack::dctypes::{ + Compose, + Port, + Ports, + PublishedPort, + Service, + Services +}; +use serde_yaml; +use crate::forms::{StackForm, stack}; +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, + pub(crate) stack: Stack +} + +impl TryInto> for stack::Ports { + type Error = String; + fn try_into(self) -> Result, Self::Error> { + convert_shared_ports(self.shared_ports.clone().unwrap()) + } +} + + +fn convert_shared_ports(ports: Vec) -> Result, String> { + let mut _ports: Vec = vec![]; + for p in ports { + let port = p.parse::().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(stack: Stack) -> Self { + DcBuilder { + config: Config::default(), + stack: stack, + } + } + + + pub fn build(&self) -> Option { + + tracing::debug!("Start build docker compose from {:?}", &self.stack.body); + let _stack = serde_json::from_value::(self.stack.body.clone()); + let mut services = indexmap::IndexMap::new(); + match _stack { + Ok(apps) => { + println!("stack item {:?}", apps.custom.web); + + for app_type in apps.custom.web { + let code = app_type.app.code.clone().to_owned(); + let mut service = Service { + image: Some(app_type.app.docker_image.to_string()), + ..Default::default() + }; + + if let Some(ports) = &app_type.app.ports { + if !ports.shared_ports.clone()?.is_empty() { + service.ports = Ports::Long(app_type.app.ports?.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_type in srvs { + let code = app_type.app.code.to_owned(); + let mut service = Service { + image: Some(app_type.app.docker_image.to_string()), + ..Default::default() + }; + + if let Some(ports) = &app_type.app.ports { + if !ports.shared_ports.clone()?.is_empty() { + service.ports = Ports::Long(app_type.app.ports?.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_type in features { + let code = app_type.app.code.to_owned(); + let mut service = Service { + // image: Some(app.dockerhub_image.as_ref().unwrap().to_owned()), + image: Some(app_type.app.docker_image.to_string()), + ..Default::default() + }; + + if let Some(ports) = &app_type.app.ports { + if !ports.shared_ports.clone()?.is_empty() { + service.ports = Ports::Long(app_type.app.ports?.try_into().unwrap()) + } + } + service.restart = Some("always".to_owned()); + services.insert( + code, + Some(service), + ); + } + } + } + } + Err(e) => { + tracing::debug!("Unpack stack form {:?}", e); + () + } + } + + let compose_content = Compose { + version: Some("3.8".to_string()), + services: { + Services(services) + }, + ..Default::default() + }; + + 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.clone()).unwrap(); + + Some(serialized) + } +} diff --git a/src/helpers/stack/dctypes.rs b/src/helpers/stack/dctypes.rs new file mode 100644 index 0000000..931deab --- /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), + #[cfg(not(feature = "indexmap"))] + V1(HashMap), + 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, + #[serde(default, skip_serializing_if = "Services::is_empty")] + pub services: Services, + #[serde(default, skip_serializing_if = "TopLevelVolumes::is_empty")] + pub volumes: TopLevelVolumes, + #[serde(default, skip_serializing_if = "ComposeNetworks::is_empty")] + pub networks: ComposeNetworks, + #[serde(skip_serializing_if = "Option::is_none")] + pub service: Option, + #[cfg(feature = "indexmap")] + #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")] + pub extensions: IndexMap, + #[cfg(not(feature = "indexmap"))] + #[serde(flatten, skip_serializing_if = "HashMap::is_empty")] + pub extensions: HashMap, +} + +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, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub privileged: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub healthcheck: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub deploy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub image: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub container_name: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "build")] + pub build_: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub pid: Option, + #[serde(default, skip_serializing_if = "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, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub devices: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub restart: Option, + #[serde(default, skip_serializing_if = "Labels::is_empty")] + pub labels: Labels, + #[serde(skip_serializing_if = "Option::is_none")] + pub tmpfs: Option, + #[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, + #[serde(default, skip_serializing_if = "DependsOnOptions::is_empty")] + pub depends_on: DependsOnOptions, + #[serde(skip_serializing_if = "Option::is_none")] + pub command: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub entrypoint: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub env_file: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub stop_grace_period: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub profiles: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub links: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub dns: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub ipc: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub net: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub stop_signal: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub working_dir: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub expose: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub volumes_from: Vec, + #[cfg(feature = "indexmap")] + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub extends: IndexMap, + #[cfg(not(feature = "indexmap"))] + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub extends: HashMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub logging: Option, + #[serde(default, skip_serializing_if = "is_zero")] + pub scale: i64, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub init: bool, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub stdin_open: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub shm_size: Option, + #[cfg(feature = "indexmap")] + #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")] + pub extensions: IndexMap, + #[cfg(not(feature = "indexmap"))] + #[serde(flatten, skip_serializing_if = "HashMap::is_empty")] + pub extensions: HashMap, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub extra_hosts: Vec, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub tty: bool, + #[serde(default, skip_serializing_if = "SysCtls::is_empty")] + pub sysctls: SysCtls, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub security_opt: Vec, +} + +impl Service { + pub fn image(&self) -> &str { + self.image.as_deref().unwrap_or_default() + } + + pub fn network_mode(&self) -> &str { + self.network_mode.as_deref().unwrap_or_default() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum EnvFile { + Simple(String), + List(Vec), +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum DependsOnOptions { + Simple(Vec), + #[cfg(feature = "indexmap")] + Conditional(IndexMap), + #[cfg(not(feature = "indexmap"))] + Conditional(HashMap), +} + +impl Default for DependsOnOptions { + fn default() -> Self { + Self::Simple(Vec::new()) + } +} + +impl DependsOnOptions { + pub fn is_empty(&self) -> bool { + match self { + Self::Simple(v) => v.is_empty(), + Self::Conditional(m) => m.is_empty(), + } + } +} + +#[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>, + #[cfg(not(feature = "indexmap"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum Ports { + Short(Vec), + Long(Vec), +} + +impl Default for Ports { + fn default() -> Self { + Self::Short(Vec::default()) + } +} + +impl Ports { + pub fn is_empty(&self) -> bool { + match self { + Self::Short(v) => v.is_empty(), + Self::Long(v) => v.is_empty(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Port { + pub target: u16, + #[serde(skip_serializing_if = "Option::is_none")] + pub host_ip: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub published: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub protocol: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, +} + +#[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), + #[cfg(feature = "indexmap")] + KvPair(IndexMap>), + #[cfg(not(feature = "indexmap"))] + KvPair(HashMap>), +} + +impl Default for Environment { + fn default() -> Self { + Self::List(Vec::new()) + } +} + +impl Environment { + pub fn is_empty(&self) -> bool { + match self { + Self::List(v) => v.is_empty(), + Self::KvPair(m) => m.is_empty(), + } + } +} + +#[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 { + let owned = s.to_owned(); + Extension::try_from(owned) + } +} + +impl TryFrom for Extension { + type Error = ExtensionParseError; + + fn try_from(s: String) -> Result { + if s.starts_with("x-") { + Ok(Self(s)) + } else { + Err(ExtensionParseError(s)) + } + } +} + +/// The result of a failed TryFrom conversion for [`Extension`] +/// +/// Contains the string that was being converted +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct ExtensionParseError(pub String); + +impl fmt::Display for ExtensionParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "unknown attribute {:?}, extensions must start with 'x-' (see https://docs.docker.com/compose/compose-file/#extension)", self.0) + } +} + +impl std::error::Error for ExtensionParseError {} + +#[cfg(feature = "indexmap")] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct Services(pub IndexMap>); +#[cfg(not(feature = "indexmap"))] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct Services(pub HashMap>); + +impl Services { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum Labels { + List(Vec), + #[cfg(feature = "indexmap")] + Map(IndexMap), + #[cfg(not(feature = "indexmap"))] + Map(HashMap), +} + +impl Default for Labels { + fn default() -> Self { + Self::List(Vec::new()) + } +} + +impl Labels { + pub fn is_empty(&self) -> bool { + match self { + Self::List(v) => v.is_empty(), + Self::Map(m) => m.is_empty(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum Tmpfs { + Simple(String), + List(Vec), +} + +#[cfg(feature = "indexmap")] +#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct Ulimits(pub IndexMap); +#[cfg(not(feature = "indexmap"))] +#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct Ulimits(pub HashMap); + +impl Ulimits { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[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), + 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, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub args: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub shm_size: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub target: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub network: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub cache_from: Vec, + #[serde(default, skip_serializing_if = "Labels::is_empty")] + pub labels: Labels, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum BuildArgs { + Simple(String), + List(Vec), + #[cfg(feature = "indexmap")] + KvPair(IndexMap), + #[cfg(not(feature = "indexmap"))] + KvPair(HashMap), +} + +#[cfg(feature = "indexmap")] +#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct AdvancedNetworks(pub IndexMap>); +#[cfg(not(feature = "indexmap"))] +#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct AdvancedNetworks(pub HashMap>); + +#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct AdvancedNetworkSettings { + #[serde(skip_serializing_if = "Option::is_none")] + pub ipv4_address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ipv6_address: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub aliases: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum SysCtls { + List(Vec), + #[cfg(feature = "indexmap")] + Map(IndexMap>), + #[cfg(not(feature = "indexmap"))] + Map(HashMap>), +} + +impl Default for SysCtls { + fn default() -> Self { + Self::List(Vec::new()) + } +} + +impl SysCtls { + pub fn is_empty(&self) -> bool { + match self { + Self::List(v) => v.is_empty(), + Self::Map(m) => m.is_empty(), + } + } +} + +#[cfg(feature = "indexmap")] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct TopLevelVolumes(pub IndexMap>); +#[cfg(not(feature = "indexmap"))] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct TopLevelVolumes(pub HashMap>); + +impl TopLevelVolumes { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct ComposeVolume { + #[serde(skip_serializing_if = "Option::is_none")] + pub driver: Option, + #[cfg(feature = "indexmap")] + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub driver_opts: IndexMap>, + #[cfg(not(feature = "indexmap"))] + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub driver_opts: HashMap>, + #[serde(skip_serializing_if = "Option::is_none")] + pub external: Option, + #[serde(default, skip_serializing_if = "Labels::is_empty")] + pub labels: Labels, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +#[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>); + +#[cfg(not(feature = "indexmap"))] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct ComposeNetworks(pub HashMap>); + +impl ComposeNetworks { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[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, + #[cfg(feature = "indexmap")] + #[serde(default, skip_serializing_if = "IndexMap::is_empty")] + pub driver_opts: IndexMap>, + #[cfg(not(feature = "indexmap"))] + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub driver_opts: HashMap>, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub enable_ipv6: bool, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub internal: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub external: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ipam: Option, + #[serde(default, skip_serializing_if = "Labels::is_empty")] + pub labels: Labels, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct Ipam { + #[serde(skip_serializing_if = "Option::is_none")] + pub driver: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub config: Vec, +} + +#[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, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] +#[serde(deny_unknown_fields)] +pub struct Deploy { + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub replicas: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub labels: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub update_config: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub resources: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub restart_policy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub placement: Option, +} + +fn is_zero(val: &i64) -> bool { + *val == 0 +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct Healthcheck { + #[serde(skip_serializing_if = "Option::is_none")] + pub test: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub interval: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option, + #[serde(default, skip_serializing_if = "is_zero")] + pub retries: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_period: Option, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub disable: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum HealthcheckTest { + Single(String), + Multiple(Vec), +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[serde(deny_unknown_fields)] +pub struct Limits { + #[serde(skip_serializing_if = "Option::is_none")] + pub cpus: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub memory: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[serde(deny_unknown_fields)] +pub struct Placement { + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub constraints: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub preferences: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct Preferences { + pub spread: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[serde(deny_unknown_fields)] +pub struct Resources { + pub limits: Option, + pub reservations: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[serde(deny_unknown_fields)] +pub struct RestartPolicy { + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub delay: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_attempts: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub window: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)] +#[serde(deny_unknown_fields)] +pub struct UpdateConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub parallelism: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub delay: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub failure_action: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub monitor: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_failure_ratio: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum Volumes { + Simple(Vec), + Advanced(Vec), +} + +impl Default for Volumes { + fn default() -> Self { + Self::Simple(Vec::new()) + } +} + +impl Volumes { + pub fn is_empty(&self) -> bool { + match self { + Self::Simple(v) => v.is_empty(), + Self::Advanced(v) => v.is_empty(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct AdvancedVolumes { + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + pub target: String, + #[serde(rename = "type")] + pub _type: String, + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub read_only: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bind: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub volume: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tmpfs: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[serde(deny_unknown_fields)] +pub struct Bind { + pub propagation: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[serde(deny_unknown_fields)] +pub struct Volume { + pub nocopy: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Default)] +#[serde(deny_unknown_fields)] +pub struct TmpfsSettings { + pub size: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum Command { + Simple(String), + Args(Vec), +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum Entrypoint { + Simple(String), + List(Vec), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd)] +#[serde(untagged)] +pub enum SingleValue { + String(String), + Bool(bool), + Unsigned(u64), + Signed(i64), + Float(f64), +} + +impl fmt::Display for SingleValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::String(s) => f.write_str(s), + Self::Bool(b) => write!(f, "{b}"), + Self::Unsigned(u) => write!(f, "{u}"), + Self::Signed(i) => write!(f, "{i}"), + Self::Float(fl) => write!(f, "{fl}"), + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Hash)] +#[serde(untagged)] +pub enum MapOrEmpty { + Map(T), + Empty, +} + +impl Default for MapOrEmpty { + fn default() -> Self { + Self::Empty + } +} + +impl From> for Option { + fn from(value: MapOrEmpty) -> Self { + match value { + MapOrEmpty::Map(t) => Some(t), + MapOrEmpty::Empty => None, + } + } +} + +impl Serialize for MapOrEmpty + where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Map(t) => t.serialize(serializer), + Self::Empty => { + use serde::ser::SerializeMap; + serializer.serialize_map(None)?.end() + } + } + } +} \ 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 0000000..7da2d2e --- /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 21432d3..f9ce272 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 @@ -14,3 +14,13 @@ pub struct Stack { pub created_at: DateTime, pub updated_at: DateTime, } + +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/client/add.rs b/src/routes/client/add.rs index 8e3e51f..eee2ce0 100644 --- a/src/routes/client/add.rs +++ b/src/routes/client/add.rs @@ -9,6 +9,7 @@ use sqlx::PgPool; use std::sync::Arc; use tracing::Instrument; + #[tracing::instrument(name = "Add client.")] #[post("")] pub async fn add_handler( @@ -17,6 +18,7 @@ pub async fn add_handler( pool: web::Data, ) -> Result { let query_span = tracing::info_span!("Counting the user's clients"); + match sqlx::query!( r#" SELECT @@ -39,12 +41,12 @@ pub async fn add_handler( client_count ); - return JsonResponse::build().err("Too many clients already created"); + return JsonResponse::build().err("Too many clients created".to_owned()); } } Err(e) => { tracing::error!("Failed to execute query: {:?}", e); - return JsonResponse::build().err_internal_server_error(""); + return JsonResponse::build().internal_error("Internal Server Error".to_owned()); } }; @@ -73,11 +75,16 @@ pub async fn add_handler( Ok(result) => { tracing::info!("New client {} have been saved to database", result.id); client.id = result.id; - JsonResponse::build().set_item(client).ok("success") + + return JsonResponse::build() + .set_id(client.id) + .set_item(Some(client)) + .ok("OK".to_owned()); } Err(e) => { tracing::error!("Failed to execute query: {:?}", e); - JsonResponse::build().err_internal_server_error("") + let err = format!("Failed to insert. {}", e); + return JsonResponse::build().err(err); } } } diff --git a/src/routes/rating/add.rs b/src/routes/rating/add.rs index ba23a9d..dd13eee 100644 --- a/src/routes/rating/add.rs +++ b/src/routes/rating/add.rs @@ -3,16 +3,20 @@ use crate::helpers::JsonResponse; use crate::models; use crate::models::user::User; use crate::models::RateCategory; -use actix_web::post; -use actix_web::{web, Responder, Result}; use sqlx::PgPool; use tracing::Instrument; +use actix_web::{ + web, + post, + Responder, Result, +}; // workflow // add, update, list, get(user_id), ACL, // ACL - access to func for a user // ACL - access to objects for a user + #[tracing::instrument(name = "Add rating.")] #[post("")] pub async fn add_handler( @@ -59,10 +63,7 @@ pub async fn add_handler( form.obj_id, form.category ); - - return JsonResponse::build() - .set_id(record.id) - .ok(format!("Already Rated")); + return JsonResponse::build().conflict("Already rated".to_owned()); } Err(sqlx::Error::RowNotFound) => {} Err(e) => { @@ -95,13 +96,13 @@ pub async fn add_handler( Ok(result) => { tracing::info!("New rating {} have been saved to database", result.id); - return JsonResponse::build() + JsonResponse::build() .set_id(result.id) - .ok("Saved".to_string()); + .ok("Saved".to_owned()) } Err(e) => { tracing::error!("Failed to execute query: {:?}", e); - return JsonResponse::build().err("Failed to insert".to_string()); + JsonResponse::build().internal_error("Failed to insert".to_owned()) } } } diff --git a/src/routes/rating/get.rs b/src/routes/rating/get.rs index 1603287..75281c0 100644 --- a/src/routes/rating/get.rs +++ b/src/routes/rating/get.rs @@ -15,6 +15,7 @@ pub async fn get_handler( path: web::Path<(i32,)>, pool: web::Data, ) -> Result { + /// Get rating of any user let rate_id = path.0; let query_span = tracing::info_span!("Search for rate id={}.", rate_id); match sqlx::query_as!( @@ -42,7 +43,12 @@ pub async fn get_handler( #[tracing::instrument(name = "Get all ratings.")] #[get("")] -pub async fn default(path: web::Path<()>, pool: web::Data) -> Result { +pub async fn list_handler( + path: web::Path<()>, + pool: web::Data, +) -> Result { + /// Get ratings of all users + let query_span = tracing::info_span!("Get all rates."); // let category = path.0; match sqlx::query_as!(models::Rating, r"SELECT * FROM rating") @@ -52,14 +58,14 @@ pub async fn default(path: web::Path<()>, pool: web::Data) -> Result { tracing::info!("Ratings found: {:?}", rating.len()); - return JsonResponse::build().set_list(rating).ok("OK".to_string()); + return JsonResponse::build().set_list(rating).ok("OK".to_owned()); } Err(sqlx::Error::RowNotFound) => { - return JsonResponse::build().err("Not Found"); + return JsonResponse::build().not_found("Not Found".to_owned()); } Err(e) => { tracing::error!("Failed to fetch rating, error: {:?}", e); - return JsonResponse::build().err("Internal Server Error"); + return JsonResponse::build().internal_error("Internal Server Error".to_owned()); } } } diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs index bbe2e8a..27260e0 100644 --- a/src/routes/stack/add.rs +++ b/src/routes/stack/add.rs @@ -1,12 +1,11 @@ use actix_web::{ web, - web::{Bytes, Data, Json}, + web::{Bytes, Data}, Responder, Result, }; use crate::forms::stack::StackForm; use crate::helpers::JsonResponse; use crate::models::user::User; -use crate::models::Stack; use actix_web::post; use chrono::Utc; use serde_json::Value; @@ -16,6 +15,7 @@ use tracing::Instrument; use uuid::Uuid; use crate::models; + #[tracing::instrument(name = "Add stack.")] #[post("")] pub async fn add( @@ -28,21 +28,19 @@ pub async fn add( let body_str = str::from_utf8(&body_bytes).unwrap(); let form = match serde_json::from_str::(body_str) { Ok(f) => { - println!("fine"); f } - Err(err) => { - let err = format!("Error: {}", err); - return JsonResponse::::build().err(err); + Err(_err) => { + let msg = format!("Invalid data. {:?}", _err); + return JsonResponse::::build().err("Invalid data".to_owned()); } }; - // println!("app: {:?}", form); let user_id = user.id.clone(); let request_id = Uuid::new_v4(); let request_span = tracing::info_span!( "Validating a new stack", %request_id, - commonDomain=?&form.common_domain, + commonDomain=?&form.custom.project_name, region=?&form.region, domainList=?&form.domain_list ); @@ -52,7 +50,7 @@ pub async fn add( tracing::info!( "request_id {} Adding '{}' '{}' as a new stack", request_id, - form.common_domain, + form.custom.project_name, form.region ); @@ -67,33 +65,18 @@ pub async fn add( } }; - let stack = Stack { - id: 0_i32, // internal stack id - stack_id: Uuid::new_v4(), // public uuid of the stack - // user_id: Uuid::from_u128(user_id as u128), - user_id: user_id, // - name: stack_name, - body: body, - // body: body_str.to_string(), - created_at: Utc::now(), - updated_at: Utc::now(), - }; - - println!("stack object {:?}", stack); return match sqlx::query!( r#" - INSERT INTO user_stack (id, stack_id, user_id, name, body, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) + INSERT INTO user_stack (stack_id, user_id, name, body, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id; "#, - 0_i32, - stack.stack_id, - stack.user_id, - stack.name, - // sqlx::types::Json(stack.body), - stack.body, - stack.created_at, - stack.updated_at + Uuid::new_v4(), + user_id, + stack_name, + body, + Utc::now(), + Utc::now(), ) .fetch_one(pool.get_ref()) .instrument(query_span) @@ -104,11 +87,12 @@ pub async fn add( "req_id: {} New stack details have been saved to database", request_id ); - return JsonResponse::build().set_id(record.id).ok("OK".to_string()); + return JsonResponse::build().set_id(record.id).ok("OK".to_owned()); } Err(e) => { tracing::error!("req_id: {} Failed to execute query: {:?}", request_id, e); - return JsonResponse::build().err("Internal Server Error".to_string()); + return JsonResponse::build().err("Internal Server Error".to_owned()); } }; } + diff --git a/src/routes/stack/compose.rs b/src/routes/stack/compose.rs new file mode 100644 index 0000000..4d19a3f --- /dev/null +++ b/src/routes/stack/compose.rs @@ -0,0 +1,119 @@ +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::{get, post}; +use sqlx::PgPool; +use std::str; +use tracing::Instrument; +use uuid::Uuid; +use crate::helpers::stack::builder::DcBuilder; + +#[tracing::instrument(name = "User's generate docker-compose.")] +#[post("/{id}")] +pub async fn add( + user: web::ReqData, + path: web::Path<(i32,)>, + pool: Data, +) -> Result { + 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); + + return JsonResponse::build() + .set_id(id) + .set_item(fc.unwrap()) + .ok("Success".to_owned()); + } + None => { + return JsonResponse::build().err("Could not generate compose file".to_owned()); + } + } +} + +#[tracing::instrument(name = "Generate docker-compose. Admin")] +#[get("/{id}/compose")] +pub async fn admin( + user: web::ReqData, + path: web::Path<(i32,)>, + pool: Data, +) -> Result { + /// Admin function for generating compose file for specified user + let id = path.0; + tracing::debug!("Received id: {}", id); + + let stack = match sqlx::query_as!( + Stack, + r#" + SELECT * FROM user_stack WHERE id=$1 LIMIT 1 + "#, + 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); + return JsonResponse::build() + .set_id(id) + .set_item(fc.unwrap()).ok("Success".to_owned()); + + } + None => { + return JsonResponse::build().err("Could not generate compose file".to_owned()); + } + } +} diff --git a/src/routes/stack/deploy.rs b/src/routes/stack/deploy.rs index 67edaa8..45c1b38 100644 --- a/src/routes/stack/deploy.rs +++ b/src/routes/stack/deploy.rs @@ -1,5 +1,105 @@ -use actix_web::HttpResponse; +use std::sync::Arc; +use actix_web::{ + web, + post, + web::Data, + Responder, Result, +}; +use crate::models::user::User; +use crate::models::stack::Stack; +use sqlx::PgPool; +use lapin::{ + options::*, publisher_confirm::Confirmation, BasicProperties, Connection, + ConnectionProperties +}; +use crate::configuration::Settings; +use crate::helpers::JsonResponse; +use crate::helpers::stack::builder::DcBuilder; +use futures_lite::stream::StreamExt; +use crate::forms::StackPayload; -pub async fn deploy() -> HttpResponse { - unimplemented!() -} \ No newline at end of file + +#[tracing::instrument(name = "Deploy for every user. Admin endpoint")] +#[post("/{id}/deploy")] +pub async fn add( + user: web::ReqData, + path: web::Path<(i32,)>, + pool: Data, + sets: Data>, +) -> Result { + let id = path.0; + tracing::debug!("Received id: {}", id); + + let stack = match sqlx::query_as!( + Stack, + r#" + SELECT * FROM user_stack WHERE id=$1 LIMIT 1 + "#, + 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 + .expect("Could not connect RabbitMQ"); + + tracing::info!("RABBITMQ CONNECTED"); + + let channel = conn.create_channel().await.unwrap(); + let mut stack_data = serde_json::from_value::( + dc.stack.body.clone() + ).unwrap(); + + stack_data.id = Some(id); + stack_data.user_token = Some(user.id.clone()); + stack_data.user_email = Some(user.email.clone()); + stack_data.stack_code = stack_data.custom.custom_stack_code.clone(); + + let payload = serde_json::to_string::(&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"); + return JsonResponse::::build().set_id(id).ok("Success".to_owned()); + } + None => { + JsonResponse::build().internal_error("Deployment failed".to_owned()) + } + } +} diff --git a/src/routes/stack/get.rs b/src/routes/stack/get.rs index 17f9181..271d2dc 100644 --- a/src/routes/stack/get.rs +++ b/src/routes/stack/get.rs @@ -1,30 +1,24 @@ use actix_web::{web, get, Responder, Result}; -use serde_derive::Serialize; use sqlx::PgPool; +use crate::helpers::{JsonResponse, JsonResponseBuilder}; use crate::models; use crate::models::user::User; +use std::convert::From; +use tracing::Instrument; -#[derive(Serialize)] -struct JsonResponse { - status: String, - message: String, - code: u32, - id: Option, - object: Option, - objects: Option>, -} -#[tracing::instrument(name = "Get stack.")] +#[tracing::instrument(name = "Get logged user stack.")] #[get("/{id}")] -pub async fn get( +pub async fn item( user: web::ReqData, path: web::Path<(i32,)>, pool: web::Data, ) -> Result { + /// Get stack apps of logged user only let (id,) = path.into_inner(); - tracing::info!("User {:?} is getting stack by id {:?}", user, id); + tracing::info!("User {:?} gets stack by id {:?}", user.id, id); match sqlx::query_as!( models::Stack, r#" @@ -36,37 +30,59 @@ pub async fn get( .await { Ok(stack) => { - tracing::info!("stack found: {:?}", stack.id,); - let response = JsonResponse { - status: "Success".to_string(), - code: 200, - message: "".to_string(), - id: Some(stack.id), - object: Some(stack), - objects: None - }; - return Ok(web::Json(response)); + tracing::info!("Stack found: {:?}", stack.id,); + return JsonResponse::build().set_item(Some(stack)).ok("OK".to_owned()); } Err(sqlx::Error::RowNotFound) => { - return Ok(web::Json(JsonResponse { - status: "Error".to_string(), - code: 404, - message: format!("Not Found"), - id: None, - object: None, - objects: None - })); + JsonResponse::build().not_found("Record not found".to_owned()) } Err(e) => { tracing::error!("Failed to fetch stack, error: {:?}", e); - return Ok(web::Json(JsonResponse { - status: "Error".to_string(), - code: 500, - message: format!("Internal Server Error"), - id: None, - object: None, - objects: None - })); + return JsonResponse::build().internal_error("Could not fetch data".to_owned()); } } } + +#[tracing::instrument(name = "Get user's stack list.")] +#[get("/user/{id}")] +pub async fn list( + user: web::ReqData, + path: web::Path<(String,)>, + pool: web::Data, +) -> Result { + + /// This is admin endpoint, used by a m2m app, client app is confidential + /// it should return stacks by user id + /// in order to pass validation at external deployment service + + let (id,) = path.into_inner(); + tracing::info!("Logged user: {:?}", user.id); + tracing::info!("Get stack list for user {:?}", id); + + let query_span = tracing::info_span!("Get stacks by user id."); + + match sqlx::query_as!( + models::Stack, + r#" + SELECT * FROM user_stack WHERE user_id=$1 + "#, + id + ) + .fetch_all(pool.get_ref()) + .instrument(query_span) + .await + { + Ok(list) => { + return JsonResponse::build().set_list(list).ok("OK".to_string()); + } + Err(sqlx::Error::RowNotFound) => { + tracing::error!("No stacks found for user: {:?}", &user.id); + return JsonResponse::build().not_found("No stacks found for user".to_string()) + } + Err(e) => { + tracing::error!("Failed to fetch stack, error: {:?}", e); + return JsonResponse::build().internal_error("Could not fetch".to_string()); + } + } +} + diff --git a/src/routes/stack/mod.rs b/src/routes/stack/mod.rs index f3e5bc9..27c8061 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 3f5c390..3306fcd 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -53,25 +53,20 @@ pub async fn run( .wrap(Cors::permissive()) .service(crate::routes::rating::add_handler) .service(crate::routes::rating::get_handler) - .service(crate::routes::rating::default), + .service(crate::routes::rating::list_handler), ) - // .service( - // web::resource("/stack/{id}") - // .route(web::get() - // .to(crate::routes::stack::get)) - // .route(web::post() - // .to(crate::routes::stack::update)) - // .route(web::post() - // .to(crate::routes::stack::add)), - // ) .service( web::scope("/stack") .wrap(HttpAuthentication::bearer( crate::middleware::trydirect::bearer_guard, )) .wrap(Cors::permissive()) - .service(crate::routes::stack::add::add) - .service(crate::routes::stack::get::get), + .service(crate::routes::stack::deploy::add) + .service(crate::routes::stack::compose::add) + .service(crate::routes::stack::compose::admin) + .service(crate::routes::stack::get::item) + .service(crate::routes::stack::get::list) + .service(crate::routes::stack::add::add), ) .app_data(db_pool.clone()) .app_data(settings.clone()) diff --git a/custom-stack-payload-2.json b/tests/custom-stack-payload-2.json similarity index 100% rename from custom-stack-payload-2.json rename to tests/custom-stack-payload-2.json diff --git a/custom-stack-payload-3.json b/tests/custom-stack-payload-3.json similarity index 100% rename from custom-stack-payload-3.json rename to tests/custom-stack-payload-3.json diff --git a/tests/custom-stack-payload-4.json b/tests/custom-stack-payload-4.json new file mode 100644 index 0000000..6f4bc7c --- /dev/null +++ b/tests/custom-stack-payload-4.json @@ -0,0 +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":"

Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)

","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":"

PostgREST description

","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":"mysampleproject4","custom_stack_code":"another-bot4","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/tests/custom-stack-payload-5.json b/tests/custom-stack-payload-5.json new file mode 100644 index 0000000..70bda71 --- /dev/null +++ b/tests/custom-stack-payload-5.json @@ -0,0 +1 @@ +{"commonDomain":"test.app", "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":"

Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)

","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":"

PostgREST description

","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":"mysampleproject5","custom_stack_code":"another-bot5","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/tests/custom-stack-payload-6.json b/tests/custom-stack-payload-6.json new file mode 100644 index 0000000..61dedb9 --- /dev/null +++ b/tests/custom-stack-payload-6.json @@ -0,0 +1 @@ +{"commonDomain":"test.app", "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":"another-bot8","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":0,"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":"

Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)

","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":"

PostgREST description

","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":"mysampleproject6","custom_stack_code":"another-bot8","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/tests/custom-stack-payload-7.json b/tests/custom-stack-payload-7.json new file mode 100644 index 0000000..1fbfbbd --- /dev/null +++ b/tests/custom-stack-payload-7.json @@ -0,0 +1 @@ +{"commonDomain":"test.app", "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":"another-bot8","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":0,"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":"

Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)

","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":"

PostgREST description

","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":"mysampleproject7","custom_stack_code":"another-bot9","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/tests/custom-stack-payload-8.json b/tests/custom-stack-payload-8.json new file mode 100644 index 0000000..7a55fc9 --- /dev/null +++ b/tests/custom-stack-payload-8.json @@ -0,0 +1 @@ +{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cpx11","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":[],"form_app":[],"save_token":false,"disk_type":"pd-standart","cloud_token":"****","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"_etag":null,"_id":233,"_created":"2023-07-19T06:38:57.608807","_updated":"2023-08-15T11:12:14.921797","name":"FastAPI","code":"fastapi","role":null,"type":"web","default":true,"popularity":null,"descr":null,"ports":{"public":["5050","8000","8080"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":2500,"height":2500,"image":"8d1ba06d-04e2-4523-879e-2846c86a10d8.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.1,"ram_size":"0.2Gb","disk_size":"0.2Gb","dockerhub_image":"fastapi","form":null,"versions":[{"_etag":null,"_id":587,"_created":null,"_updated":"2023-07-20T12:51:20.321999","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"0.0.1"},{"_etag":null,"_id":590,"_created":null,"_updated":"2023-07-20T08:36:49.651219","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"latest"},{"_etag":null,"_id":589,"_created":null,"_updated":"2023-07-20T08:36:55.200575","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"unstable"},{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}],"domain":"fastapi.test","sharedPorts":["8000"],"main":true,"version":{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}}],"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":"

Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)

","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.6,"ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","form":null,"versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-09-07T07:28:51.18965","app_id":198,"name":"2.18.4","version":"2.18.4","update_status":"published","tag":"2.18.4"}],"domain":"","sharedPorts":["9000"],"main":false,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-09-07T07:28:51.18965","app_id":198,"name":"2.18.4","version":"2.18.4","update_status":"published","tag":"2.18.4"}}],"service":[{"_etag":null,"_id":24,"_created":"2020-06-19T13:07:24.228389","_updated":"2023-08-08T10:34:13.4985","name":"PostgreSQL","code":"postgres","role":[],"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":{"light":{"width":576,"height":594,"image":"fd23f54c-e250-4228-8d56-7e5d93ffb925.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0,"ram_size":null,"disk_size":null,"dockerhub_image":"postgres","form":null,"versions":[{"_etag":null,"_id":458,"_created":"2022-10-20T07:57:05.88997","_updated":"2023-04-05T07:24:39.637749","app_id":24,"name":"15","version":"15","update_status":"published","tag":"15"},{"_etag":null,"_id":288,"_created":"2022-10-20T07:56:16.160116","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"10.22","version":"10.22","update_status":"published","tag":"10.22"},{"_etag":null,"_id":303,"_created":"2022-10-20T07:57:24.710286","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"13.8","version":"13.8","update_status":"published","tag":"13.8"},{"_etag":null,"_id":266,"_created":"2022-10-20T07:56:32.360852","_updated":"2023-04-05T06:49:31.782132","app_id":24,"name":"11","version":"11","update_status":"published","tag":"11"},{"_etag":null,"_id":267,"_created":"2022-10-20T07:57:35.552085","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"12.12","version":"12.12","update_status":"published","tag":"12.12"},{"_etag":null,"_id":38,"_created":"2020-06-19T13:07:24.258724","_updated":"2022-10-20T07:58:06.882602","app_id":24,"name":"14.5","version":"14.5","update_status":"published","tag":"14.5"},{"_etag":null,"_id":564,"_created":null,"_updated":"2023-05-24T12:55:57.894215","app_id":24,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"},{"_etag":null,"_id":596,"_created":null,"_updated":"2023-08-09T11:00:33.004267","app_id":24,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"}],"domain":"","sharedPorts":["6372"],"main":false,"version":{"_etag":null,"_id":596,"_created":null,"_updated":"2023-08-09T11:00:33.004267","app_id":24,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"}}],"servers_count":3,"project_name":"FastAPI example","custom_stack_code":"fastapi-example","project_git_url":"https://github.com/trydirect/fastapi.git"}} diff --git a/tests/custom-stack-payload-9.json b/tests/custom-stack-payload-9.json new file mode 100644 index 0000000..3bf473d --- /dev/null +++ b/tests/custom-stack-payload-9.json @@ -0,0 +1 @@ +{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cx21","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":["stack_migration"],"form_app":[],"save_token":true,"disk_type":"pd-standart","cloud_token":"D3dkDL4Qy1NpFezTl60V5RYfB5p0BSwLLoKVHhEqmJQ3jObSG77irP86e9YtCYVi","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"_etag":null,"_id":233,"_created":"2023-07-19T06:38:57.608807","_updated":"2023-08-15T11:12:14.921797","name":"FastAPI","code":"fastapi","role":null,"type":"web","default":true,"popularity":null,"descr":null,"ports":{"public":["5050","8000","8080"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":2500,"height":2500,"image":"8d1ba06d-04e2-4523-879e-2846c86a10d8.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.1,"ram_size":"0.2Gb","disk_size":"0.2Gb","dockerhub_image":"fastapi","form":null,"versions":[{"_etag":null,"_id":587,"_created":null,"_updated":"2023-07-20T12:51:20.321999","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"0.0.1"},{"_etag":null,"_id":590,"_created":null,"_updated":"2023-07-20T08:36:49.651219","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"latest"},{"_etag":null,"_id":589,"_created":null,"_updated":"2023-07-20T08:36:55.200575","app_id":233,"name":"0.0.1","version":"0.0.1","update_status":"ready_for_testing","tag":"unstable"},{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}],"domain":"fastapi.test","sharedPorts":["8000"],"main":true,"version":{"_etag":"591","_id":591,"_created":null,"_updated":"2023-08-15T08:17:40.226186","app_id":233,"name":"Fastapi","version":"0.100.0","update_status":"published","tag":"stable"}},{"name":"smarty bot","code":"smarty-bot","domain":"smarty-bot.fastapi.test","sharedPorts":["8080"],"versions":[],"custom":true,"type":"web","main":false,"_id":0,"dockerhub_user":"vsilent","dockerhub_name":"smarty","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":"

Portainer is a lightweight management UI which allows you to easily manage your different Docker environments (Docker hosts or Swarm clusters)

","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0.6,"ram_size":"1Gb","disk_size":"1Gb","dockerhub_image":"portainer-ce-feature","form":null,"versions":[{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-09-07T07:28:51.18965","app_id":198,"name":"2.18.4","version":"2.18.4","update_status":"published","tag":"2.18.4"}],"domain":"","sharedPorts":["9000"],"main":false,"version":{"_etag":null,"_id":456,"_created":"2022-04-25T12:44:30.964547","_updated":"2023-09-07T07:28:51.18965","app_id":198,"name":"2.18.4","version":"2.18.4","update_status":"published","tag":"2.18.4"}}],"service":[{"_etag":null,"_id":246,"_created":"2023-09-15T11:41:21.353013","_updated":"2023-09-15T11:45:40.307663","name":"Mailhog","code":"mailhog_service","role":["mailhog"],"type":"service","default":null,"popularity":null,"descr":null,"ports":{"public":["8025"],"private":["1025","8025"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":48,"height":48,"image":"9ed27fd5-4745-4fa1-86a6-e53557d700ff.png"},"dark":{"width":48,"height":48,"image":"0d7b38f8-d2ad-4068-a5bd-aa32c7e1958f.png"}},"category_id":null,"parent_app_id":null,"full_description":null,"description":"

MailHog is an email testing tool for developers.

","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0,"ram_size":null,"disk_size":null,"dockerhub_image":null,"form":null,"versions":[{"_etag":null,"_id":625,"_created":null,"_updated":"2023-09-15T11:42:10.48131","app_id":246,"name":"latest","version":"latest","update_status":"ready_for_production","tag":"latest"}],"domain":"","sharedPorts":["8025"],"main":false,"version":{"_etag":null,"_id":625,"_created":null,"_updated":"2023-09-15T11:42:10.48131","app_id":246,"name":"latest","version":"latest","update_status":"ready_for_production","tag":"latest"}},{"_etag":null,"_id":24,"_created":"2020-06-19T13:07:24.228389","_updated":"2023-08-08T10:34:13.4985","name":"PostgreSQL","code":"postgres","role":[],"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":{"light":{"width":576,"height":594,"image":"fd23f54c-e250-4228-8d56-7e5d93ffb925.svg"},"dark":{}},"category_id":null,"parent_app_id":null,"full_description":null,"description":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":0,"ram_size":null,"disk_size":null,"dockerhub_image":"postgres","form":null,"versions":[{"_etag":null,"_id":458,"_created":"2022-10-20T07:57:05.88997","_updated":"2023-04-05T07:24:39.637749","app_id":24,"name":"15","version":"15","update_status":"published","tag":"15"},{"_etag":null,"_id":288,"_created":"2022-10-20T07:56:16.160116","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"10.22","version":"10.22","update_status":"published","tag":"10.22"},{"_etag":null,"_id":303,"_created":"2022-10-20T07:57:24.710286","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"13.8","version":"13.8","update_status":"published","tag":"13.8"},{"_etag":null,"_id":266,"_created":"2022-10-20T07:56:32.360852","_updated":"2023-04-05T06:49:31.782132","app_id":24,"name":"11","version":"11","update_status":"published","tag":"11"},{"_etag":null,"_id":267,"_created":"2022-10-20T07:57:35.552085","_updated":"2023-03-17T13:46:51.433539","app_id":24,"name":"12.12","version":"12.12","update_status":"published","tag":"12.12"},{"_etag":null,"_id":38,"_created":"2020-06-19T13:07:24.258724","_updated":"2022-10-20T07:58:06.882602","app_id":24,"name":"14.5","version":"14.5","update_status":"published","tag":"14.5"},{"_etag":null,"_id":564,"_created":null,"_updated":"2023-05-24T12:55:57.894215","app_id":24,"name":"0.0.5","version":"0.0.5","update_status":"ready_for_testing","tag":"0.0.5"},{"_etag":null,"_id":596,"_created":null,"_updated":"2023-08-09T11:00:33.004267","app_id":24,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"}],"domain":"","sharedPorts":["6372"],"main":false,"version":{"_etag":null,"_id":596,"_created":null,"_updated":"2023-08-09T11:00:33.004267","app_id":24,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"}}],"servers_count":3,"project_name":"FastAPI example 3","custom_stack_code":"fastapi-example-3","project_git_url":"https://github.com/trydirect/fastapi.git"}} diff --git a/custom-stack-payload-singleapp.json b/tests/custom-stack-payload-singleapp.json similarity index 100% rename from custom-stack-payload-singleapp.json rename to tests/custom-stack-payload-singleapp.json diff --git a/custom-stack-payload.json b/tests/custom-stack-payload.json similarity index 100% rename from custom-stack-payload.json rename to tests/custom-stack-payload.json