diff --git a/src/services/stack.rs b/.dockerignore similarity index 100% rename from src/services/stack.rs rename to .dockerignore diff --git a/.env b/.env index dffc672..0f19229 100644 --- a/.env +++ b/.env @@ -1,5 +1,10 @@ +#BUILDKIT_PROGRESS=plain +#DOCKER_BUILDKIT=1 DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/stacker POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_DB=stacker -POSTGRES_PORT=5432 \ No newline at end of file +POSTGRES_PORT=5432 +SECURITY_KEY=SECURITY_KEY_SHOULD_BE_OF_LEN_32 + +REDIS_URL=redis://127.0.0.1/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3acd8af..1d0de11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ target -.idea +.idea/ files +access_control.conf +configuration.yaml +configuration.yaml.backup +configuration.yaml.orig diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml deleted file mode 100644 index a9f6743..0000000 --- a/.idea/dataSources.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - postgresql - true - org.postgresql.Driver - jdbc:postgresql://localhost:5432/stacker - $ProjectFileDir$ - - - \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..99ebb1c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/gitguardian/ggshield + rev: v1.28.0 + hooks: + - id: ggshield + language_version: python3 + stages: [commit] diff --git a/Cargo.lock b/Cargo.lock index 191b1e7..996de5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,13 +2,25 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "actix-casbin-auth" +version = "1.1.0" +source = "git+https://github.com/casbin-rs/actix-casbin-auth.git#66662102a92fe1ae80ad427e07c1879cbdf65f4f" +dependencies = [ + "actix-service", + "actix-web", + "casbin", + "futures", + "tokio", +] + [[package]] name = "actix-codec" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "bytes", "futures-core", "futures-sink", @@ -21,9 +33,9 @@ dependencies = [ [[package]] name = "actix-cors" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b340e9cfa5b08690aae90fb61beb44e9b06f44fe3d0f93781aaa58cfba86245e" +checksum = "0346d8c1f762b41b458ed3145eea914966bb9ad20b9be0d6d463b20d45586370" dependencies = [ "actix-utils", "actix-web", @@ -36,17 +48,17 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.4.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92ef85799cba03f76e4f7c10f533e66d87c9a7e7055f3391f09000ad8351bc9" +checksum = "d223b13fd481fc0d1f83bb12659ae774d9e3601814c68a0bc539731698cca743" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-utils", - "ahash 0.8.3", - "base64 0.21.4", - "bitflags 2.4.1", + "ahash 0.8.11", + "base64 0.21.7", + "bitflags 2.5.0", "brotli", "bytes", "bytestring", @@ -64,7 +76,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand", + "rand 0.8.5", "sha1", "smallvec", "tokio", @@ -80,14 +92,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.38", + "syn 2.0.58", ] [[package]] name = "actix-router" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +checksum = "d22475596539443685426b6bdadb926ad0ecaefdfc5fb05e5e3441f15463c511" dependencies = [ "bytestring", "http", @@ -118,7 +130,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2 0.5.4", + "socket2 0.5.6", "tokio", "tracing", ] @@ -146,9 +158,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.4.0" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4a5b5e29603ca8c94a77c65cf874718ceb60292c5a5c3e5f4ace041af462b9" +checksum = "43a6556ddebb638c2358714d853257ed226ece6023ef9364f23f0c70737ea984" dependencies = [ "actix-codec", "actix-http", @@ -159,7 +171,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web-codegen", - "ahash 0.8.3", + "ahash 0.8.11", "bytes", "bytestring", "cfg-if", @@ -179,8 +191,8 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.5.4", - "time 0.3.30", + "socket2 0.5.6", + "time 0.3.34", "url", ] @@ -193,22 +205,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.38", -] - -[[package]] -name = "actix-web-httpauth" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d613edf08a42ccc6864c941d30fe14e1b676a77d16f1dbadc1174d065a0a775" -dependencies = [ - "actix-utils", - "actix-web", - "base64 0.21.4", - "futures-core", - "futures-util", - "log", - "pin-project-lite", + "syn 2.0.58", ] [[package]] @@ -226,34 +223,71 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.12", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "const-random", + "getrandom 0.2.12", "once_cell", "version_check", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -342,6 +376,70 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -349,36 +447,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", - "event-listener", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +dependencies = [ + "concurrent-queue", + "event-listener 5.2.0", + "event-listener-strategy 0.5.1", "futures-core", + "pin-project-lite", ] [[package]] name = "async-executor" -version = "1.6.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0" +checksum = "10b3e585719c2358d2660232671ca8ca4ddb4be4ce8a1842d6c2dc8685303316" dependencies = [ - "async-lock", + "async-lock 3.3.0", "async-task", "concurrent-queue", - "fastrand 2.0.1", - "futures-lite", + "fastrand 2.0.2", + "futures-lite 2.3.0", "slab", ] [[package]] name = "async-global-executor" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel", + "async-channel 2.2.0", "async-executor", - "async-io", - "async-lock", + "async-io 2.3.2", + "async-lock 3.3.0", "blocking", - "futures-lite", + "futures-lite 2.3.0", "once_cell", ] @@ -399,27 +510,57 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ - "async-lock", + "async-lock 2.8.0", "autocfg", "cfg-if", "concurrent-queue", - "futures-lite", + "futures-lite 1.13.0", "log", "parking", - "polling", + "polling 2.8.0", "rustix 0.37.27", "slab", - "socket2 0.4.9", + "socket2 0.4.10", "waker-fn", ] +[[package]] +name = "async-io" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling 3.6.0", + "rustix 0.38.32", + "slab", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "async-lock" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ - "event-listener", + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", ] [[package]] @@ -428,7 +569,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6012d170ad00de56c9ee354aef2e358359deb1ec504254e0e5a3774771de0e" dependencies = [ - "async-io", + "async-io 1.13.0", "async-trait", "futures-core", "reactor-trait", @@ -436,19 +577,19 @@ dependencies = [ [[package]] name = "async-task" -version = "4.5.0" +version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.58", ] [[package]] @@ -460,6 +601,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -468,15 +618,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -495,9 +645,15 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.4" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[package]] name = "bitflags" @@ -507,9 +663,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" @@ -531,25 +687,25 @@ dependencies = [ [[package]] name = "blocking" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ - "async-channel", - "async-lock", + "async-channel 2.2.0", + "async-lock 3.3.0", "async-task", - "fastrand 2.0.1", + "fastrand 2.0.2", "futures-io", - "futures-lite", + "futures-lite 2.3.0", "piper", "tracing", ] [[package]] name = "brotli" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -558,9 +714,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -568,9 +724,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + +[[package]] +name = "bytecount" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" [[package]] name = "byteorder" @@ -580,19 +742,74 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bytestring" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" dependencies = [ "bytes", ] +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + +[[package]] +name = "casbin" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71063d3ee2f5ecc89229ccade0f3f8fb413b5e3978124a38b611216f91dd7c9" +dependencies = [ + "async-trait", + "fixedbitset", + "getrandom 0.2.12", + "mini-moka", + "once_cell", + "parking_lot 0.12.1", + "petgraph", + "regex", + "rhai", + "ritelinked", + "serde", + "slog", + "slog-async", + "slog-term", + "thiserror", + "tokio", +] + [[package]] name = "cbc" version = "0.1.2" @@ -604,9 +821,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ "jobserver", "libc", @@ -631,7 +848,7 @@ dependencies = [ "serde", "time 0.1.45", "wasm-bindgen", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -644,20 +861,80 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.58", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "concurrent-queue" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] [[package]] name = "config" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" dependencies = [ "async-trait", "json5", @@ -672,6 +949,26 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.12", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -685,21 +982,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ "percent-encoding", - "time 0.3.30", + "time 0.3.34", "version_check", ] [[package]] name = "cookie-factory" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -707,15 +1007,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -731,38 +1031,49 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] [[package]] -name = "crossbeam-queue" -version = "0.3.8" +name = "crossbeam-channel" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] -name = "crossbeam-utils" -version = "0.8.16" +name = "crossbeam-queue" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", + "crossbeam-utils", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -770,16 +1081,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] [[package]] -name = "darling" -version = "0.14.4" +name = "ctr" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "darling_core", + "cipher", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", "darling_macro", ] @@ -793,7 +1114,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 1.0.109", ] @@ -808,11 +1129,69 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "deadpool" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "retain_mut", + "tokio", +] + +[[package]] +name = "deadpool" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-lapin" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce11c0dc86703e59a8921bb9afee10b13c242e47624347bd3a3b545c41db556e" +dependencies = [ + "deadpool 0.10.0", + "lapin", + "tokio-executor-trait", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63dfa964fe2a66f3fde91fc70b267fe193d822c7e603e2a675a49a7f46ad3f49" +dependencies = [ + "tokio", +] + [[package]] name = "deranged" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] @@ -823,7 +1202,16 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" dependencies = [ - "derive_builder_macro", + "derive_builder_macro 0.12.0", +] + +[[package]] +name = "derive_builder" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f59169f400d8087f238c5c0c7db6a28af18681717f3b623227d92f397e938c7" +dependencies = [ + "derive_builder_macro 0.13.1", ] [[package]] @@ -838,13 +1226,35 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_builder_core" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ec317cc3e7ef0928b0ca6e4a634a4d6c001672ae210438cf114a83e56b018d" +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", + "derive_builder_core 0.12.0", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870368c3fb35b8031abb378861d4460f573b92238ec2152c927a21f77e3e0127" +dependencies = [ + "derive_builder_core 0.13.1", "syn 1.0.109", ] @@ -890,6 +1300,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -901,6 +1321,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dlv-list" version = "0.3.0" @@ -913,6 +1344,24 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "docker-compose-types" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6fdd6fa1c9e8e716f5f73406b868929f468702449621e7397066478b9bf89c" +dependencies = [ + "derive_builder 0.13.1", + "indexmap 2.2.6", + "serde", + "serde_yaml", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dotenvy" version = "0.15.7" @@ -921,9 +1370,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" dependencies = [ "serde", ] @@ -945,12 +1394,32 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", ] [[package]] @@ -959,6 +1428,48 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" +dependencies = [ + "event-listener 5.2.0", + "pin-project-lite", +] + [[package]] name = "executor-trait" version = "2.1.0" @@ -979,9 +1490,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "finl_unicode" @@ -989,6 +1500,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.28" @@ -1011,6 +1528,15 @@ dependencies = [ "spin 0.9.8", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1034,18 +1560,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1058,9 +1584,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1068,15 +1594,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1094,11 +1620,22 @@ dependencies = [ "parking_lot 0.11.2", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.12.1", +] + [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -1115,34 +1652,53 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand 2.0.2", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.58", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1178,20 +1734,43 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", ] [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -1201,9 +1780,10 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.24" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" + dependencies = [ "bytes", "fnv", @@ -1211,29 +1791,39 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.1.0", + "indexmap 2.2.6", + "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.6", + "ahash 0.7.8", ] [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.11", "allocator-api2", ] @@ -1243,7 +1833,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.1", + "hashbrown 0.14.3", ] [[package]] @@ -1255,11 +1845,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1269,9 +1865,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hkdf" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] @@ -1285,11 +1881,20 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1298,15 +1903,36 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", "pin-project-lite", ] +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel 1.9.0", + "base64 0.13.1", + "futures-lite 1.13.0", + "http", + "infer", + "pin-project-lite", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "url", +] + [[package]] name = "httparse" version = "1.8.0" @@ -1321,9 +1947,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1336,7 +1962,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -1358,9 +1984,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1387,9 +2013,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1403,20 +2029,25 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", - "serde", ] [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.3", "serde", ] +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + [[package]] name = "inout" version = "0.1.3" @@ -1444,53 +2075,55 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "ipnet" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] -name = "itertools" -version = "0.10.5" +name = "is-terminal" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "either", + "hermit-abi", + "libc", + "windows-sys 0.52.0", ] [[package]] name = "itertools" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1523,7 +2156,7 @@ dependencies = [ "async-reactor-trait", "async-trait", "executor-trait", - "flume", + "flume 0.10.14", "futures-core", "futures-io", "parking_lot 0.12.1", @@ -1543,9 +2176,19 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libredox" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] [[package]] name = "linked-hash-map" @@ -1561,15 +2204,15 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "local-channel" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a493488de5f18c8ffcba89eebb8532ffc562dc400490eb65b84893fae0b178" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" dependencies = [ "futures-core", "futures-sink", @@ -1578,9 +2221,9 @@ dependencies = [ [[package]] name = "local-waker" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" @@ -1594,9 +2237,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "matchers" @@ -1619,9 +2262,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mime" @@ -1629,6 +2272,21 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mini-moka" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "dashmap", + "skeptic", + "smallvec", + "tagptr", + "triomphe", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1637,25 +2295,31 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.48.0", ] +[[package]] +name = "mutually_exclusive_features" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d02c0b00610773bb7fc61d85e13d86c7858cbdf00e1a120bfc41bc055dbaa0e" + [[package]] name = "native-tls" version = "0.2.11" @@ -1694,11 +2358,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] @@ -1715,26 +2385,32 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -1751,7 +2427,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.58", ] [[package]] @@ -1762,9 +2438,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -1797,7 +2473,7 @@ dependencies = [ "cbc", "cipher", "des", - "getrandom", + "getrandom 0.2.12", "hmac", "lazy_static", "rc2", @@ -1856,7 +2532,7 @@ dependencies = [ "libc", "redox_syscall 0.4.1", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1873,15 +2549,15 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.4" +version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" +checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" dependencies = [ "memchr", "thiserror", @@ -1890,9 +2566,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.4" +version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" +checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" dependencies = [ "pest", "pest_generator", @@ -1900,53 +2576,63 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.4" +version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" +checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.58", ] [[package]] name = "pest_meta" -version = "2.7.4" +version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" +checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" dependencies = [ "once_cell", "pest", "sha2", ] +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.2.6", +] + [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.58", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1956,12 +2642,12 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pinky-swear" -version = "6.1.0" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d894b67aa7a4bf295db5e85349078c604edaa6fa5c8721e8eca3c7729a27f2ac" +checksum = "6cfae3ead413ca051a681152bd266438d3bfa301c9bdf836939a14c721bb2a21" dependencies = [ "doc-comment", - "flume", + "flume 0.11.0", "parking_lot 0.12.1", "tracing", ] @@ -1973,15 +2659,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", - "fastrand 2.0.1", + "fastrand 2.0.2", "futures-io", ] [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" @@ -1996,7 +2682,34 @@ dependencies = [ "libc", "log", "pin-project-lite", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 0.38.32", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", ] [[package]] @@ -2037,22 +2750,46 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags 2.5.0", + "memchr", + "unicase", +] + [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -2060,8 +2797,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -2071,7 +2818,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -2080,7 +2836,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.12", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -2104,19 +2869,31 @@ dependencies = [ ] [[package]] -name = "redox_syscall" -version = "0.2.16" +name = "redis" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "71d64e978fd98a0e6b105d066ba4889a7301fca65aeac850a877d8797343feeb" dependencies = [ - "bitflags 1.3.2", + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.6", + "tokio", + "tokio-util", + "url", ] [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags 1.3.2", ] @@ -2132,25 +2909,25 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom", - "redox_syscall 0.2.16", + "getrandom 0.2.12", + "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.10.2" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", - "regex-syntax 0.8.2", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", ] [[package]] @@ -2164,13 +2941,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -2181,17 +2958,17 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64 0.21.4", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -2209,9 +2986,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", @@ -2223,6 +3002,41 @@ dependencies = [ "winreg", ] +[[package]] +name = "retain_mut" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" + +[[package]] +name = "rhai" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6273372244d04a8a4b0bec080ea1e710403e88c5d9d83f9808b2bfa64f0982a" +dependencies = [ + "ahash 0.8.11", + "bitflags 2.5.0", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "serde", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_codegen" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9db7f8dc4c9d48183a17ce550574c42995252b82d267eaca3fcd1b979159856c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "ring" version = "0.16.20" @@ -2240,16 +3054,27 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.4" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fce3045ffa7c981a6ee93f640b538952e155f1ae3a1a02b84547fc7a56b7059a" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", - "getrandom", + "cfg-if", + "getrandom 0.2.12", "libc", "spin 0.9.8", "untrusted 0.9.0", - "windows-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "ritelinked" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f2771d255fd99f0294f13249fecd0cae6e074f86b4197ec1f1689d537b44d3" +dependencies = [ + "ahash 0.7.8", + "hashbrown 0.11.2", ] [[package]] @@ -2299,20 +3124,20 @@ dependencies = [ "io-lifetimes", "libc", "linux-raw-sys 0.3.8", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "rustix" -version = "0.38.19" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys 0.4.10", - "windows-sys", + "linux-raw-sys 0.4.13", + "windows-sys 0.52.0", ] [[package]] @@ -2329,24 +3154,24 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.8" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring 0.17.4", + "ring 0.17.8", "rustls-webpki", "sct", ] [[package]] name = "rustls-connector" -version = "0.18.3" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "060bcc1795b840d0e56d78f3293be5f652aa1611d249b0e63ffe19f4a8c9ae23" +checksum = "25da151615461c7347114b1ad1a7458b4cdebc69cb220cd140cd5cb324b1dd37" dependencies = [ "log", - "rustls 0.21.8", + "rustls 0.21.10", "rustls-native-certs", "rustls-webpki", ] @@ -2365,11 +3190,11 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.4", + "base64 0.21.7", ] [[package]] @@ -2378,23 +3203,38 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.4", + "ring 0.17.8", "untrusted 0.9.0", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2405,19 +3245,19 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -2428,9 +3268,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -2438,41 +3278,65 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +dependencies = [ + "serde", +] [[package]] name = "serde" -version = "1.0.189" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.58", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2487,12 +3351,12 @@ dependencies = [ [[package]] name = "serde_valid" -version = "0.16.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0adc7a19d45e581abc6d169c865a0b14b84bb43a9e966d1cca4d733e70f7f35a" +checksum = "70c0e00fab6460447391a1981c21341746bc2d0178a7c46a3bbf667f450ac6e4" dependencies = [ - "indexmap 1.9.3", - "itertools 0.10.5", + "indexmap 2.2.6", + "itertools", "num-traits", "once_cell", "paste", @@ -2507,23 +3371,23 @@ dependencies = [ [[package]] name = "serde_valid_derive" -version = "0.16.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071237362e267e2a76ffe4434094e089dcd8b5e9d8423ada499e5550dcb0181d" +checksum = "88c60a851514741a6088b2cd18eefb3f0d02ff3a1c87234de47153f2724d395d" dependencies = [ "paste", "proc-macro-error", "proc-macro2", "quote", - "strsim", - "syn 1.0.109", + "strsim 0.11.1", + "syn 2.0.58", ] [[package]] name = "serde_valid_literal" -version = "0.16.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f57df292b1d64449f90794fc7a67efca0b21acca91493e64a46418a29bbe36b4" +checksum = "aced4f1b31605a2b55eeacf2ec4dcbd96583263e9ded17eed1d41ab75915d12e" dependencies = [ "paste", "regex", @@ -2531,11 +3395,11 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.25" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -2553,6 +3417,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.10.8" @@ -2582,6 +3452,21 @@ dependencies = [ "libc", ] +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + [[package]] name = "slab" version = "0.4.9" @@ -2591,17 +3476,63 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" + +[[package]] +name = "slog-async" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c8038f898a2c79507940990f05386455b3a317d8f18d4caea7cbc3d5096b84" +dependencies = [ + "crossbeam-channel", + "slog", + "take_mut", + "thread_local", +] + +[[package]] +name = "slog-term" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8" +dependencies = [ + "is-terminal", + "slog", + "term", + "thread_local", + "time 0.3.34", +] + [[package]] name = "smallvec" -version = "1.11.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "serde", + "static_assertions", + "version_check", +] [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -2609,12 +3540,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2634,102 +3565,240 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" dependencies = [ - "itertools 0.11.0", + "itertools", "nom", "unicode_categories", ] [[package]] -name = "sqlx" -version = "0.6.3" +name = "sqlx" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" +dependencies = [ + "sqlx-core 0.6.3", + "sqlx-macros 0.6.3", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core 0.7.4", + "sqlx-macros 0.7.4", + "sqlx-postgres", +] + +[[package]] +name = "sqlx-adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3751ab2b1b81c2d78c513ed9ce99c0933da0b6cc1ea93707941d8e9bba34bfee" +dependencies = [ + "async-trait", + "casbin", + "dotenv", + "sqlx 0.7.4", +] + +[[package]] +name = "sqlx-core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" +dependencies = [ + "ahash 0.7.8", + "atoi 1.0.0", + "base64 0.13.1", + "bitflags 1.3.2", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "dirs", + "dotenvy", + "either", + "event-listener 2.5.3", + "futures-channel", + "futures-core", + "futures-intrusive 0.4.2", + "futures-util", + "hashlink", + "hex", + "hkdf", + "hmac", + "indexmap 1.9.3", + "itoa", + "libc", + "log", + "md-5", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rand 0.8.5", + "rustls 0.20.9", + "rustls-pemfile", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "tokio-stream", + "url", + "uuid", + "webpki-roots", + "whoami", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash 0.8.11", + "atoi 2.0.0", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener 2.5.3", + "futures-channel", + "futures-core", + "futures-intrusive 0.5.0", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.2.6", + "log", + "memchr", + "native-tls", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core 0.6.3", + "sqlx-rt", + "syn 1.0.109", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core 0.7.4", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ - "sqlx-core", - "sqlx-macros", + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core 0.7.4", + "sqlx-postgres", + "syn 1.0.109", + "tempfile", + "tokio", + "url", ] [[package]] -name = "sqlx-core" -version = "0.6.3" +name = "sqlx-postgres" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ - "ahash 0.7.6", - "atoi", - "base64 0.13.1", - "bitflags 1.3.2", + "atoi 2.0.0", + "base64 0.21.7", + "bitflags 2.5.0", "byteorder", - "bytes", - "chrono", "crc", - "crossbeam-queue", - "dirs", "dotenvy", - "either", - "event-listener", + "etcetera", "futures-channel", "futures-core", - "futures-intrusive", + "futures-io", "futures-util", - "hashlink", "hex", "hkdf", "hmac", - "indexmap 1.9.3", + "home", "itoa", - "libc", "log", "md-5", "memchr", "once_cell", - "paste", - "percent-encoding", - "rand", - "rustls 0.20.9", - "rustls-pemfile", + "rand 0.8.5", "serde", "serde_json", - "sha1", "sha2", "smallvec", - "sqlformat", - "sqlx-rt", + "sqlx-core 0.7.4", "stringprep", "thiserror", - "tokio-stream", - "url", - "uuid", - "webpki-roots", + "tracing", "whoami", ] -[[package]] -name = "sqlx-macros" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" -dependencies = [ - "dotenvy", - "either", - "heck", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-rt", - "syn 1.0.109", - "url", -] - [[package]] name = "sqlx-rt" version = "0.6.3" @@ -2745,41 +3814,58 @@ dependencies = [ name = "stacker" version = "0.1.0" dependencies = [ + "actix-casbin-auth", "actix-cors", "actix-http", "actix-web", - "actix-web-httpauth", + "aes-gcm", + "base64 0.22.0", + "brotli", + "casbin", "chrono", + "clap", "config", - "derive_builder", + "deadpool-lapin", + "derive_builder 0.12.0", + "docker-compose-types", "futures", - "futures-lite", + "futures-lite 2.3.0", "futures-util", "glob", "hmac", - "indexmap 2.1.0", + "indexmap 2.2.6", "lapin", - "rand", + "rand 0.8.5", + "redis", "regex", "reqwest", "serde", "serde_derive", "serde_json", + "serde_path_to_error", "serde_valid", "serde_yaml", "sha2", - "sqlx", + "sqlx 0.6.3", + "sqlx-adapter", "thiserror", "tokio", "tokio-stream", "tracing", "tracing-actix-web", "tracing-bunyan-formatter", - "tracing-log", + "tracing-log 0.1.4", "tracing-subscriber", "uuid", + "wiremock", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.4" @@ -2797,6 +3883,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.5.0" @@ -2816,15 +3908,21 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "system-configuration" version = "0.5.1" @@ -2846,6 +3944,18 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + [[package]] name = "tcp-stream" version = "0.26.1" @@ -2860,42 +3970,61 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand 2.0.1", - "redox_syscall 0.3.5", - "rustix 0.38.19", - "windows-sys", + "fastrand 2.0.2", + "rustix 0.38.32", + "windows-sys 0.52.0", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "thin-vec" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" +dependencies = [ + "serde", ] [[package]] name = "thiserror" -version = "1.0.49" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.58", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -2914,12 +4043,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -2934,13 +4064,23 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2958,9 +4098,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.33.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -2970,20 +4110,31 @@ dependencies = [ "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.4", + "socket2 0.5.6", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-executor-trait" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802ccf58e108fe16561f35348fabe15ff38218968f033d587e399a84937533cc" +dependencies = [ + "async-trait", + "executor-trait", + "tokio", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.58", ] [[package]] @@ -3009,9 +4160,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -3020,9 +4171,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -3061,11 +4212,12 @@ dependencies = [ [[package]] name = "tracing-actix-web" -version = "0.7.7" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94982c2ad939d5d0bfd71c2f9b7ed273c72348485c72bb87bb4db6bd69df10cb" +checksum = "fa069bd1503dd526ee793bb3fce408895136c95fc86d2edb2acf1c646d7f0684" dependencies = [ "actix-web", + "mutually_exclusive_features", "pin-project", "tracing", "uuid", @@ -3079,7 +4231,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.58", ] [[package]] @@ -3088,15 +4240,15 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.11", "gethostname", "log", "serde", "serde_json", - "time 0.3.30", + "time 0.3.34", "tracing", "tracing-core", - "tracing-log", + "tracing-log 0.1.4", "tracing-subscriber", ] @@ -3112,20 +4264,31 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", @@ -3136,14 +4299,20 @@ dependencies = [ "thread_local", "tracing", "tracing-core", - "tracing-log", + "tracing-log 0.2.0", ] +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" @@ -3157,11 +4326,20 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -3171,18 +4349,18 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode_categories" @@ -3190,11 +4368,21 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "untrusted" @@ -3210,22 +4398,29 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" -version = "1.5.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom", + "getrandom 0.2.12", "serde", ] @@ -3253,6 +4448,16 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3262,6 +4467,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -3274,11 +4485,17 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3286,24 +4503,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.58", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -3313,9 +4530,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3323,28 +4540,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -3356,7 +4573,7 @@ version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring 0.17.4", + "ring 0.17.8", "untrusted 0.9.0", ] @@ -3371,11 +4588,12 @@ dependencies = [ [[package]] name = "whoami" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ - "wasm-bindgen", + "redox_syscall 0.4.1", + "wasite", "web-sys", ] @@ -3395,6 +4613,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -3403,11 +4630,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.4", ] [[package]] @@ -3416,7 +4643,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", ] [[package]] @@ -3425,13 +4661,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -3440,42 +4691,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + [[package]] name = "winreg" version = "0.50.0" @@ -3483,7 +4776,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "wiremock" +version = "0.5.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a3a53eaf34f390dd30d7b1b078287dd05df2aa2e21a589ccb80f5c7253c2e9" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64 0.21.7", + "deadpool 0.9.5", + "futures", + "futures-timer", + "http-types", + "hyper", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", ] [[package]] @@ -3501,30 +4816,49 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "zstd" -version = "0.12.4" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "6.0.6" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" dependencies = [ - "libc", "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.10+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 44f6525..ae1f142 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,33 +2,38 @@ name = "stacker" version = "0.1.0" edition = "2021" +default-run= "server" [lib] path="src/lib.rs" [[bin]] path = "src/main.rs" -name = "stacker" +name = "server" + +[[bin]] +path = "src/console/main.rs" +name = "console" +required-features = ["explain"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] actix-web = "4.3.1" -chrono = { version = "0.4.26", features = ["time", "serde"] } -config = "0.13.3" -reqwest = { version = "0.11.17", features = ["json"] } -serde = { version = "1.0.162", features = ["derive"] } +chrono = { version = "0.4.29", features = ["time", "serde"] } +config = "0.13.4" +reqwest = { version = "0.11.23", features = ["json", "blocking"] } +serde = { version = "1.0.195", features = ["derive"] } tokio = { version = "1.28.1", features = ["full"] } tracing = { version = "0.1.40", features = ["log"] } tracing-bunyan-formatter = "0.3.8" -tracing-log = "0.1.3" -tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter"] } +tracing-log = "0.1.4" +tracing-subscriber = { version = "0.3.18", features = ["registry", "env-filter"] } uuid = { version = "1.3.4", features = ["v4", "serde"] } thiserror = "1.0" -serde_valid = "0.16.3" -serde_json = { version = "1.0.105", features = [] } -serde_derive = "1.0.188" -actix-web-httpauth = "0.8.1" +serde_valid = "0.18.0" +serde_json = { version = "1.0.111", features = [] } +serde_derive = "1.0.195" actix-cors = "0.6.4" tracing-actix-web = "0.7.7" regex = "1.10.2" @@ -39,13 +44,24 @@ tokio-stream = "0.1.14" actix-http = "3.4.0" hmac = "0.12.1" sha2 = "0.10.8" +sqlx-adapter = { version = "1.0.0", default-features = false, features = ["postgres", "runtime-tokio-native-tls"]} # 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" +futures-lite = "2.2.0" +clap = { version = "4.4.8", features = ["derive"] } +brotli = "3.4.0" +serde_path_to_error = "0.1.14" +deadpool-lapin = "0.11.0" +docker-compose-types = "0.7.0" +actix-casbin-auth = { git = "https://github.com/casbin-rs/actix-casbin-auth.git"} +casbin = "2.2.0" +aes-gcm = "0.10.3" +base64 = "0.22.0" +redis = { version = "0.25.2", features = ["tokio-comp"] } [dependencies.sqlx] version = "0.6.3" @@ -62,6 +78,8 @@ features = [ [features] default = ["indexmap"] indexmap = ["dep:indexmap"] +explain = ["actix-casbin-auth/explain", "actix-casbin-auth/logging"] [dev-dependencies] glob = "0.3" +wiremock = "0.5.22" diff --git a/Dockerfile b/Dockerfile index 666567e..3523dd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,60 +1,58 @@ FROM rust:bookworm as builder -RUN apt-get update; \ - #apt-get install --no-install-recommends -y libpq-dev libssl-dev pkg-config; \ - apt-get install --no-install-recommends -y libssl-dev; \ - rm -rf /var/lib/apt/lists/*; \ - USER=root cargo new --bin app; +#RUN apt-get update; \ +# apt-get install --no-install-recommends -y libssl-dev; \ +# rm -rf /var/lib/apt/lists/*; \ +# USER=root cargo new --bin app; RUN cargo install sqlx-cli WORKDIR /app # copy manifests -COPY ../Cargo.toml . -COPY ../Cargo.lock . -COPY ../rustfmt.toml . -COPY ../Makefile . -COPY ../docker/local/.env . -COPY ../docker/local/configuration.yaml . +COPY ./Cargo.toml . +COPY ./Cargo.lock . +COPY ./rustfmt.toml . +COPY ./Makefile . +COPY ./docker/local/.env . +COPY ./docker/local/configuration.yaml . # build this project to cache dependencies #RUN sqlx database create && sqlx migrate run -RUN cargo build --release; \ - rm src/*.rs +# build skeleton and remove src after +#RUN cargo build --release; \ +# rm src/*.rs -# add .env and secret.key for Docker env -#RUN touch .env; -# copy project source and necessary files -COPY ../src ./src +COPY ./src ./src + +# for ls output use BUILDKIT_PROGRESS=plain docker build . +#RUN ls -la /app/ >&2 #RUN sqlx migrate run #RUN cargo sqlx prepare -- --bin stacker -# rebuild app with project source -RUN rm -rf ./target/release/deps/stacker*; \ - cargo build --release +RUN apt-get update && apt-get install --no-install-recommends -y libssl-dev; \ + cargo build --bin=console --features="explain" && cargo build --release --features="explain" + +#RUN ls -la /app/target/release/ >&2 -# deploy stage -FROM debian:bookworm as production +# deploy production +FROM debian:bookworm-slim as production +RUN apt-get update && apt-get install --no-install-recommends -y libssl-dev ca-certificates; # create app directory WORKDIR /app RUN mkdir ./files && chmod 0777 ./files -# install libpq -RUN apt-get update; \ - apt-get install --no-install-recommends -y libssl-dev \ - && rm -rf /var/lib/apt/lists/* - # copy binary and configuration files -#COPY --from=builder ~/.cargo/bin/sqlx-cli sqlx-cli -COPY --from=builder /app/target/release/stacker . +COPY --from=builder /app/target/release/server . +COPY --from=builder /app/target/release/console . COPY --from=builder /app/.env . COPY --from=builder /app/configuration.yaml . COPY --from=builder /usr/local/cargo/bin/sqlx sqlx +COPY ./access_control.conf.dist /app EXPOSE 8000 # run the binary -ENTRYPOINT ["/app/stacker"] +ENTRYPOINT ["/app/server"] diff --git a/README.md b/README.md index 7520b6f..fe43153 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Stacker - is an application that helps users to create custom IT solutions based on dockerized open -source apps and user's custom applications docker containers. Users can build their own stack of applications, and +source apps and user's custom applications docker containers. Users can build their own project of applications, and deploy the final result to their favorite clouds using TryDirect API. Application development will include: @@ -39,9 +39,9 @@ Stacker (API) - Serves API clients Authentication made through TryDirect OAuth, here we have only client Database (Read only) Logging/Tracing (Files) / Quickwit for future -/stack (WebUI, as a result we have a JSON) -/stack/deploy -> sends deploy command to TryDirect Install service -/stack/deploy/status - get installation progress (rabbitmq client), +/project (WebUI, as a result we have a JSON) +/project/deploy -> sends deploy command to TryDirect Install service +/project/deploy/status - get installation progress (rabbitmq client), #### TODO Find out how to get user's token for queue @@ -79,7 +79,7 @@ sqlx migrate revert #### Deploy ``` -curl -X POST -H "Content-Type: application/json" -d @custom-stack-payload-2.json http://127.0.0.1:8000/stack +curl -X POST -H "Content-Type: application/json" -d @custom-stack-payload-2.json http://127.0.0.1:8000/project ``` #### Create API Client @@ -87,7 +87,12 @@ curl -X POST -H "Content-Type: application/json" -d @custom-stack-payload-2.json curl -X POST http://localhost:8000/client --header 'Content-Type: application/json' -H "Authorization: Bearer $TD_BEARER" ``` -Test client's app deploy -``` + +test client deploy http://localhost:8000/test/deploy + + +Test casbin rule +``` +cargo r --bin console --features=explain debug casbin --path /client --action POST --subject admin_petru ``` diff --git a/access_control.conf.dist b/access_control.conf.dist new file mode 100644 index 0000000..f164af1 --- /dev/null +++ b/access_control.conf.dist @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && r.act == p.act diff --git a/configuration.yaml b/configuration.yaml.dist similarity index 100% rename from configuration.yaml rename to configuration.yaml.dist diff --git a/docker-compose.yml b/docker-compose.yml index 26311c4..66b2c45 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,22 +4,21 @@ volumes: stackerdb: driver: local -networks: - backend: - driver: bridge - name: backend - external: true + redis-data: + driver: local + services: stacker: - image: trydirect/stacker:0.0.4 + image: trydirect/stacker:0.0.9 build: . container_name: stacker restart: always volumes: - ./files:/app/files - ./docker/local/configuration.yaml:/app/configuration.yaml + - ./access_control.conf:/app/access_control.conf - ./migrations:/app/migrations - ./docker/local/.env:/app/.env ports: @@ -29,28 +28,59 @@ services: environment: - RUST_LOG=debug - RUST_BACKTRACE=1 - depends_on: - stackerdb: - condition: service_healthy - networks: - - backend +# depends_on: +# stackerdb: +# condition: service_healthy + redis: + container_name: redis + image: redis + restart: always + ports: + - 6379:6379 + volumes: + - redis-data:/data +# - ./redis/rc.local:/etc/rc.local +# - ./redis/redis.conf:/usr/local/etc/redis/redis.conf + sysctls: + net.core.somaxconn: 1024 + logging: + driver: "json-file" + options: + max-size: "10m" + tag: "container_{{.Name}}" - stackerdb: - container_name: stackerdb - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - image: postgres:16.0 - restart: always - ports: - - 5432:5432 - env_file: - - ./docker/local/.env - volumes: - - stackerdb:/var/lib/postgresql/data - - ./docker/local/postgresql.conf:/etc/postgresql/postgresql.conf - networks: - - backend \ No newline at end of file + +# stacker_queue: +# image: trydirect/stacker:0.0.7 +# container_name: stacker_queue +# restart: always +# volumes: +# - ./configuration.yaml:/app/configuration.yaml +# - ./.env:/app/.env +# environment: +# - RUST_LOG=debug +# - RUST_BACKTRACE=1 +# env_file: +# - ./.env +# depends_on: +# stackerdb: +# condition: service_healthy +# entrypoint: /app/console mq listen + +# stackerdb: +# container_name: stackerdb +# healthcheck: +# test: ["CMD-SHELL", "pg_isready -U postgres"] +# interval: 10s +# timeout: 5s +# retries: 5 +# image: postgres:16.0 +# restart: always +# ports: +# - 5432:5432 +# env_file: +# - ./docker/local/.env +# volumes: +# - stackerdb:/var/lib/postgresql/data +# - ./docker/local/postgresql.conf:/etc/postgresql/postgresql.conf \ No newline at end of file diff --git a/docker/dev/.env b/docker/dev/.env index 6371a97..d60f266 100644 --- a/docker/dev/.env +++ b/docker/dev/.env @@ -1,5 +1,8 @@ +SECURITY_KEY=SECURITY_KEY_SHOULD_BE_OF_LEN_32 + DATABASE_URL=postgres://postgres:postgres@stackerdb:5432/stacker POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_DB=stacker -POSTGRES_PORT=5432 \ No newline at end of file +POSTGRES_PORT=5432 + diff --git a/docker/dev/configuration.yaml b/docker/dev/configuration.yaml index 5eef969..5538317 100644 --- a/docker/dev/configuration.yaml +++ b/docker/dev/configuration.yaml @@ -7,3 +7,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/dev/docker-compose.yml b/docker/dev/docker-compose.yml index bdfdd89..6f8c0ab 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -4,28 +4,61 @@ volumes: stackerdb: driver: local + stacker-redis-data: + driver: local + +networks: + backend: + driver: bridge + name: backend + external: true + + services: stacker: - image: trydirect/stacker:0.0.4 + image: trydirect/stacker:0.0.8 build: . container_name: stacker restart: always volumes: - ./stacker/files:/app/files - ./configuration.yaml:/app/configuration.yaml + - ./access_control.conf:/app/access_control.conf - ./migrations:/app/migrations - ./.env:/app/.env ports: - "8000:8000" env_file: - ./.env + environment: + - RUST_LOG=debug + - RUST_BACKTRACE=full + depends_on: + stackerdb: + condition: service_healthy + networks: + - backend + + + stacker_queue: + image: trydirect/stacker:0.0.7 + container_name: stacker_queue + restart: always + volumes: + - ./configuration.yaml:/app/configuration.yaml + - ./.env:/app/.env environment: - RUST_LOG=debug - RUST_BACKTRACE=1 + env_file: + - ./.env depends_on: stackerdb: condition: service_healthy + entrypoint: /app/console mq listen + networks: + - backend stackerdb: @@ -43,4 +76,26 @@ services: - ./.env volumes: - stackerdb:/var/lib/postgresql/data - - ./postgresql.conf:/etc/postgresql/postgresql.conf \ No newline at end of file + - ./postgresql.conf:/etc/postgresql/postgresql.conf + networks: + - backend + + stackerredis: + container_name: stackerredis + image: redis:latest + restart: always + ports: + - 127.0.0.1:6379:6379 + volumes: + - stacker-redis-data:/data + # - ./redis/rc.local:/etc/rc.local + # - ./redis/redis.conf:/usr/local/etc/redis/redis.conf + sysctls: + net.core.somaxconn: 1024 + logging: + driver: "json-file" + options: + max-size: "10m" + tag: "container_{{.Name}}" + + diff --git a/docker/local/configuration.yaml b/docker/local/configuration.yaml index 9c1848f..750f1cb 100644 --- a/docker/local/configuration.yaml +++ b/docker/local/configuration.yaml @@ -1,9 +1,17 @@ app_host: 0.0.0.0 app_port: 8000 auth_url: https://dev.try.direct/server/user/oauth_server/api/me +max_clients_number: 2 + database: host: 172.17.0.2 port: 5432 username: postgres password: postgres database_name: stacker + +amqp: + host: 127.0.0.1 + port: 5672 + username: guest + password: guest diff --git a/migrations/20230903063840_creating_rating_tables.down.sql b/migrations/20230903063840_creating_rating_tables.down.sql index e12e4ab..b32b52b 100644 --- a/migrations/20230903063840_creating_rating_tables.down.sql +++ b/migrations/20230903063840_creating_rating_tables.down.sql @@ -6,3 +6,5 @@ DROP INDEX idx_obj_id_rating_id; DROP table rating; DROP table product; + +DROP TYPE rate_category; diff --git a/migrations/20230903063840_creating_rating_tables.up.sql b/migrations/20230903063840_creating_rating_tables.up.sql index 579bef6..156c722 100644 --- a/migrations/20230903063840_creating_rating_tables.up.sql +++ b/migrations/20230903063840_creating_rating_tables.up.sql @@ -1,5 +1,17 @@ -- Add up migration script here +CREATE TYPE rate_category AS ENUM ( + 'application', + 'cloud', + 'project', + 'deploymentSpeed', + 'documentation', + 'design', + 'techSupport', + 'price', + 'memoryUsage' +); + CREATE TABLE product ( id integer NOT NULL, PRIMARY KEY(id), obj_id integer NOT NULL, @@ -12,7 +24,7 @@ CREATE TABLE rating ( id serial, user_id VARCHAR(50) NOT NULL, obj_id integer NOT NULL, - category VARCHAR(255) NOT NULL, + category rate_category NOT NULL, comment TEXT DEFAULT NULL, hidden BOOLEAN DEFAULT FALSE, rate INTEGER, diff --git a/migrations/20230905145525_creating_stack_tables.down.sql b/migrations/20230905145525_creating_stack_tables.down.sql index 203a95a..7f367df 100644 --- a/migrations/20230905145525_creating_stack_tables.down.sql +++ b/migrations/20230905145525_creating_stack_tables.down.sql @@ -1,3 +1,2 @@ -- Add down migration script here - -DROP TABLE user_stack; +DROP TABLE project; diff --git a/migrations/20230905145525_creating_stack_tables.up.sql b/migrations/20230905145525_creating_stack_tables.up.sql index b20b2cd..c002beb 100644 --- a/migrations/20230905145525_creating_stack_tables.up.sql +++ b/migrations/20230905145525_creating_stack_tables.up.sql @@ -1,13 +1,14 @@ -CREATE TABLE user_stack ( +CREATE TABLE project ( id serial4 NOT NULL, stack_id uuid NOT NULL, user_id VARCHAR(50) NOT NULL, - name TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, body JSON NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, - CONSTRAINT user_stack_pkey PRIMARY KEY (id) + CONSTRAINT project_pkey PRIMARY KEY (id) ); -CREATE INDEX idx_stack_id ON user_stack(stack_id); -CREATE INDEX idx_stack_user_id ON user_stack(user_id); \ No newline at end of file +CREATE INDEX idx_project_stack_id ON project(stack_id); +CREATE INDEX idx_project_user_id ON project(user_id); +CREATE INDEX idx_project_name ON project(name); diff --git a/migrations/20230917162549_creating_test_product.down.sql b/migrations/20230917162549_creating_test_product.down.sql index f9f6339..eafea95 100644 --- a/migrations/20230917162549_creating_test_product.down.sql +++ b/migrations/20230917162549_creating_test_product.down.sql @@ -1 +1 @@ -delete from product where id=1; +DELETE FROM product WHERE id=1; diff --git a/migrations/20230917162549_creating_test_product.up.sql b/migrations/20230917162549_creating_test_product.up.sql index 7a1d8d6..9aae3c5 100644 --- a/migrations/20230917162549_creating_test_product.up.sql +++ b/migrations/20230917162549_creating_test_product.up.sql @@ -1 +1 @@ -INSERT INTO public.product (id, obj_id, obj_type, created_at, updated_at) VALUES(1, 1, 'Application', '2023-09-17 10:30:02.579', '2023-09-17 10:30:02.579'); \ No newline at end of file +INSERT INTO product (id, obj_id, obj_type, created_at, updated_at) VALUES(1, 1, 'Application', '2023-09-17 10:30:02.579', '2023-09-17 10:30:02.579'); diff --git a/migrations/20231028161917_client.up.sql b/migrations/20231028161917_client.up.sql index fcb9065..e0470c3 100644 --- a/migrations/20231028161917_client.up.sql +++ b/migrations/20231028161917_client.up.sql @@ -1,5 +1,5 @@ -- Add up migration script here -CREATE TABLE public.client ( +CREATE TABLE client ( id serial4 NOT NULL, user_id varchar(50) NOT NULL, secret varchar(255), diff --git a/migrations/20240128174529_casbin_rule.down.sql b/migrations/20240128174529_casbin_rule.down.sql new file mode 100644 index 0000000..ef4c417 --- /dev/null +++ b/migrations/20240128174529_casbin_rule.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP TABLE casbin_rule; diff --git a/migrations/20240128174529_casbin_rule.up.sql b/migrations/20240128174529_casbin_rule.up.sql new file mode 100644 index 0000000..15b9914 --- /dev/null +++ b/migrations/20240128174529_casbin_rule.up.sql @@ -0,0 +1,12 @@ +-- Add up migration script here +CREATE TABLE casbin_rule ( + id SERIAL PRIMARY KEY, + ptype VARCHAR NOT NULL, + v0 VARCHAR NOT NULL, + v1 VARCHAR NOT NULL, + v2 VARCHAR NOT NULL, + v3 VARCHAR NOT NULL, + v4 VARCHAR NOT NULL, + v5 VARCHAR NOT NULL, + CONSTRAINT unique_key_sqlx_adapter UNIQUE(ptype, v0, v1, v2, v3, v4, v5) +) diff --git a/migrations/20240228125751_creating_deployments.down.sql b/migrations/20240228125751_creating_deployments.down.sql new file mode 100644 index 0000000..228cc13 --- /dev/null +++ b/migrations/20240228125751_creating_deployments.down.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +DROP table deployment; \ No newline at end of file diff --git a/migrations/20240228125751_creating_deployments.up.sql b/migrations/20240228125751_creating_deployments.up.sql new file mode 100644 index 0000000..7a06d3b --- /dev/null +++ b/migrations/20240228125751_creating_deployments.up.sql @@ -0,0 +1,14 @@ +-- Add up migration script here +CREATE TABLE deployment ( + id serial4 NOT NULL, + project_id integer NOT NULL, + body JSON NOT NULL, + deleted BOOLEAN DEFAULT FALSE, + status VARCHAR(32) NOT NULL, + created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL, + CONSTRAINT fk_project FOREIGN KEY(project_id) REFERENCES project(id), + CONSTRAINT deployment_pkey PRIMARY KEY (id) +); + +CREATE INDEX idx_deployment_project_id ON deployment(project_id); diff --git a/migrations/20240229072555_creating_cloud.down.sql b/migrations/20240229072555_creating_cloud.down.sql new file mode 100644 index 0000000..2a04e92 --- /dev/null +++ b/migrations/20240229072555_creating_cloud.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP table cloud; diff --git a/migrations/20240229072555_creating_cloud.up.sql b/migrations/20240229072555_creating_cloud.up.sql new file mode 100644 index 0000000..c842d3f --- /dev/null +++ b/migrations/20240229072555_creating_cloud.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE cloud ( + id serial4 NOT NULL, + user_id VARCHAR(50) NOT NULL, + provider VARCHAR(50) NOT NULL, + cloud_token VARCHAR(255) , + cloud_key VARCHAR(255), + cloud_secret VARCHAR(255), + save_token BOOLEAN DEFAULT FALSE, + created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL, + CONSTRAINT user_cloud_pkey PRIMARY KEY (id) +); + +CREATE INDEX idx_deployment_user_cloud_user_id ON cloud(user_id); \ No newline at end of file diff --git a/migrations/20240229075843_creating_user_stack_cloud_relation.down.sql b/migrations/20240229075843_creating_user_stack_cloud_relation.down.sql new file mode 100644 index 0000000..02d2fe5 --- /dev/null +++ b/migrations/20240229075843_creating_user_stack_cloud_relation.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +ALTER table project DROP COLUMN cloud_id; \ No newline at end of file diff --git a/migrations/20240229075843_creating_user_stack_cloud_relation.up.sql b/migrations/20240229075843_creating_user_stack_cloud_relation.up.sql new file mode 100644 index 0000000..5f65c66 --- /dev/null +++ b/migrations/20240229075843_creating_user_stack_cloud_relation.up.sql @@ -0,0 +1,3 @@ +-- Add up migration script here +ALTER table project ADD COLUMN cloud_id INT CONSTRAINT project_cloud_id REFERENCES cloud(id) ON UPDATE CASCADE ON DELETE CASCADE; + diff --git a/migrations/20240229080559_creating_cloud_server.down.sql b/migrations/20240229080559_creating_cloud_server.down.sql new file mode 100644 index 0000000..f0fa982 --- /dev/null +++ b/migrations/20240229080559_creating_cloud_server.down.sql @@ -0,0 +1,3 @@ +DROP INDEX idx_server_user_id; +DROP INDEX idx_server_cloud_id; +DROP table server; diff --git a/migrations/20240229080559_creating_cloud_server.up.sql b/migrations/20240229080559_creating_cloud_server.up.sql new file mode 100644 index 0000000..e4ed91b --- /dev/null +++ b/migrations/20240229080559_creating_cloud_server.up.sql @@ -0,0 +1,22 @@ +-- Add up migration script here + +CREATE TABLE server ( + id serial4 NOT NULL, + user_id VARCHAR(50) NOT NULL, + cloud_id integer NOT NULL, + project_id integer NOT NULL, + region VARCHAR(50) NOT NULL, + zone VARCHAR(50), + server VARCHAR(255) NOT NULL, + os VARCHAR(100) NOT NULL, + disk_type VARCHAR(100), + created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL, + CONSTRAINT user_server_pkey PRIMARY KEY (id), + CONSTRAINT fk_server FOREIGN KEY(cloud_id) REFERENCES cloud(id), + CONSTRAINT fk_server_project FOREIGN KEY(project_id) REFERENCES project(id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE INDEX idx_server_user_id ON server(user_id); +CREATE INDEX idx_server_cloud_id ON server(cloud_id); +CREATE INDEX idx_server_project_id ON server(project_id); diff --git a/migrations/20240302081015_creating_original_request_column_project.down.sql b/migrations/20240302081015_creating_original_request_column_project.down.sql new file mode 100644 index 0000000..93549b5 --- /dev/null +++ b/migrations/20240302081015_creating_original_request_column_project.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +ALTER table project DROP COLUMN request_json; diff --git a/migrations/20240302081015_creating_original_request_column_project.up.sql b/migrations/20240302081015_creating_original_request_column_project.up.sql new file mode 100644 index 0000000..2c1ba74 --- /dev/null +++ b/migrations/20240302081015_creating_original_request_column_project.up.sql @@ -0,0 +1 @@ +ALTER table project ADD COLUMN request_json JSON NOT NULL DEFAULT '{}'; \ No newline at end of file diff --git a/migrations/20240307113718_alter_cloud_alter_project.down.sql b/migrations/20240307113718_alter_cloud_alter_project.down.sql new file mode 100644 index 0000000..06f51ab --- /dev/null +++ b/migrations/20240307113718_alter_cloud_alter_project.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +ALTER table project ADD COLUMN cloud_id INT CONSTRAINT project_cloud_id REFERENCES cloud(id) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER table cloud DROP COLUMN project_id; \ No newline at end of file diff --git a/migrations/20240307113718_alter_cloud_alter_project.up.sql b/migrations/20240307113718_alter_cloud_alter_project.up.sql new file mode 100644 index 0000000..554a24a --- /dev/null +++ b/migrations/20240307113718_alter_cloud_alter_project.up.sql @@ -0,0 +1,3 @@ +-- Add up migration script here +ALTER table project DROP COLUMN cloud_id; +ALTER table cloud ADD COLUMN project_id INT CONSTRAINT cloud_project_id REFERENCES project(id) ON UPDATE CASCADE ON DELETE CASCADE; diff --git a/migrations/20240315143712_remove_cloud_id_from_server.down.sql b/migrations/20240315143712_remove_cloud_id_from_server.down.sql new file mode 100644 index 0000000..72dd11e --- /dev/null +++ b/migrations/20240315143712_remove_cloud_id_from_server.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +DROP INDEX idx_server_cloud_id; +alter table server ADD column cloud_id integer NOT NULL; diff --git a/migrations/20240315143712_remove_cloud_id_from_server.up.sql b/migrations/20240315143712_remove_cloud_id_from_server.up.sql new file mode 100644 index 0000000..be9027c --- /dev/null +++ b/migrations/20240315143712_remove_cloud_id_from_server.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +alter table server drop column cloud_id; diff --git a/migrations/20240401103123_casbin_initial_rules.down.sql b/migrations/20240401103123_casbin_initial_rules.down.sql new file mode 100644 index 0000000..d2f607c --- /dev/null +++ b/migrations/20240401103123_casbin_initial_rules.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/migrations/20240401103123_casbin_initial_rules.up.sql b/migrations/20240401103123_casbin_initial_rules.up.sql new file mode 100644 index 0000000..effa703 --- /dev/null +++ b/migrations/20240401103123_casbin_initial_rules.up.sql @@ -0,0 +1,42 @@ +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (1, 'g', 'anonym', 'group_anonymous', '', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (2, 'g', 'group_admin', 'group_anonymous', '', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (3, 'g', 'group_user', 'group_anonymous', '', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (4, 'g', 'user', 'group_user', '', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (5, 'g', 'admin_petru', 'group_admin', '', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (6, 'g', 'user_petru', 'group_user', '', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (7, 'p', 'group_anonymous', '/health_check', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (8, 'p', 'group_anonymous', '/rating/:id', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (9, 'p', 'group_anonymous', '/rating', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (10, 'p', 'group_admin', '/client', 'POST', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (11, 'p', 'group_admin', '/rating', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (12, 'p', 'group_admin', '/admin/client/:id/disable', 'PUT', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (13, 'p', 'group_admin', '/admin/client/:id/enable', 'PUT', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (14, 'p', 'group_admin', '/admin/client/:id', 'PUT', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (15, 'p', 'group_admin', '/admin/project/user/:userid', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (16, 'p', 'group_admin', '/rating/:id', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (17, 'p', 'group_user', '/client/:id/enable', 'PUT', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (18, 'p', 'group_user', '/client/:id', 'PUT', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (19, 'p', 'group_user', '/client/:id/disable', 'PUT', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (20, 'p', 'group_user', '/rating/:id', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (21, 'p', 'group_user', '/rating', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (22, 'p', 'group_user', '/rating', 'POST', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (23, 'p', 'group_user', '/project', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (24, 'p', 'group_user', '/project', 'POST', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (25, 'p', 'group_user', '/project/:id', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (26, 'p', 'group_user', '/project/:id', 'POST', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (27, 'p', 'group_user', '/project/:id', 'PUT', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (28, 'p', 'group_user', '/project/:id', 'DELETE', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (29, 'p', 'group_user', '/project/:id/compose', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (30, 'p', 'group_user', '/project/:id/compose', 'POST', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (31, 'p', 'group_user', '/project/:id/deploy', 'POST', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (32, 'p', 'group_user', '/project/:id/deploy/:cloud_id', 'POST', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (33, 'p', 'group_user', '/server', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (34, 'p', 'group_user', '/server', 'POST', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (35, 'p', 'group_user', '/server/:id', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (36, 'p', 'group_user', '/server/:id', 'PUT', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (37, 'p', 'group_user', '/cloud', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (38, 'p', 'group_user', '/cloud', 'POST', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (39, 'p', 'group_user', '/cloud/:id', 'GET', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (40, 'p', 'group_user', '/cloud/:id', 'PUT', '', '', ''); +INSERT INTO public.casbin_rule (id, ptype, v0, v1, v2, v3, v4, v5) VALUES (41, 'p', 'group_user', '/cloud/:id', 'DELETE', '', '', ''); + diff --git a/migrations/20240401184313_remove_project_id_from_cloud.down.sql b/migrations/20240401184313_remove_project_id_from_cloud.down.sql new file mode 100644 index 0000000..3b99d4c --- /dev/null +++ b/migrations/20240401184313_remove_project_id_from_cloud.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +ALTER table cloud ADD COLUMN project_id INT CONSTRAINT cloud_project_id REFERENCES project(id) ON UPDATE CASCADE ON DELETE CASCADE; diff --git a/migrations/20240401184313_remove_project_id_from_cloud.up.sql b/migrations/20240401184313_remove_project_id_from_cloud.up.sql new file mode 100644 index 0000000..4974d95 --- /dev/null +++ b/migrations/20240401184313_remove_project_id_from_cloud.up.sql @@ -0,0 +1,3 @@ +-- Add up migration script here + +alter table cloud DROP column project_id; diff --git a/migrations/20240412141011_casbin_user_rating_edit.down.sql b/migrations/20240412141011_casbin_user_rating_edit.down.sql new file mode 100644 index 0000000..41c5e57 --- /dev/null +++ b/migrations/20240412141011_casbin_user_rating_edit.down.sql @@ -0,0 +1,18 @@ +-- Add down migration script here +DELETE FROM casbin_rule +WHERE ptype = 'p' and v0 = 'group_user' and v1 = '/rating/:id' and v2 = 'PUT'; + +DELETE FROM casbin_rule +WHERE ptype = 'p' and v0 = 'group_admin' and v1 = '/admin/rating/:id' and v2 = 'PUT'; + +DELETE FROM casbin_rule +WHERE ptype = 'p' and v0 = 'group_user' and v1 = '/rating/:id' and v2 = 'DELETE'; + +DELETE FROM casbin_rule +WHERE ptype = 'p' and v0 = 'group_admin' and v1 = '/admin/rating/:id' and v2 = 'DELETE'; + +DELETE FROM casbin_rule +WHERE ptype = 'p' and v0 = 'group_admin' and v1 = '/admin/rating/:id' and v2 = 'GET'; + +DELETE FROM casbin_rule +WHERE ptype = 'p' and v0 = 'group_admin' and v1 = '/admin/rating' and v2 = 'GET'; diff --git a/migrations/20240412141011_casbin_user_rating_edit.up.sql b/migrations/20240412141011_casbin_user_rating_edit.up.sql new file mode 100644 index 0000000..527b64f --- /dev/null +++ b/migrations/20240412141011_casbin_user_rating_edit.up.sql @@ -0,0 +1,28 @@ +-- Add up migration script here +BEGIN TRANSACTION; + +INSERT INTO casbin_rule +(id, ptype, v0, v1, v2, v3, v4, v5) +VALUES((select max(id) + 1 from casbin_rule cr), 'p', 'group_user', '/rating/:id', 'PUT', '', '', ''); + +INSERT INTO casbin_rule +(id, ptype, v0, v1, v2, v3, v4, v5) +VALUES((select max(id) + 1 from casbin_rule cr), 'p', 'group_admin', '/admin/rating/:id', 'PUT', '', '', ''); + +INSERT INTO casbin_rule +(id, ptype, v0, v1, v2, v3, v4, v5) +VALUES((select max(id) + 1 from casbin_rule cr), 'p', 'group_user', '/rating/:id', 'DELETE', '', '', ''); + +INSERT INTO casbin_rule +(id, ptype, v0, v1, v2, v3, v4, v5) +VALUES((select max(id) + 1 from casbin_rule cr), 'p', 'group_admin', '/admin/rating/:id', 'DELETE', '', '', ''); + +INSERT INTO casbin_rule +(id, ptype, v0, v1, v2, v3, v4, v5) +VALUES((select max(id) + 1 from casbin_rule cr), 'p', 'group_admin', '/admin/rating/:id', 'GET', '', '', ''); + +INSERT INTO casbin_rule +(id, ptype, v0, v1, v2, v3, v4, v5) +VALUES((select max(id) + 1 from casbin_rule cr), 'p', 'group_admin', '/admin/rating', 'GET', '', '', ''); + +COMMIT TRANSACTION; diff --git a/scripts/init_db.sh b/scripts/init_db.sh index 8d84403..06693cd 100755 --- a/scripts/init_db.sh +++ b/scripts/init_db.sh @@ -38,4 +38,5 @@ export DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@localhost:${DB_PORT}/${ sqlx database create sqlx migrate run ->&2 echo "Postgres has been migrated, ready to go!" \ No newline at end of file +>&2 echo "Postgres has been migrated, ready to go!" + diff --git a/src/console/commands/appclient/mod.rs b/src/console/commands/appclient/mod.rs new file mode 100644 index 0000000..b6a00cd --- /dev/null +++ b/src/console/commands/appclient/mod.rs @@ -0,0 +1,3 @@ +mod new; + +pub use new::*; diff --git a/src/console/commands/appclient/new.rs b/src/console/commands/appclient/new.rs new file mode 100644 index 0000000..dfafa9f --- /dev/null +++ b/src/console/commands/appclient/new.rs @@ -0,0 +1,41 @@ +use crate::configuration::get_configuration; +use actix_web::rt; +use actix_web::web; +use sqlx::PgPool; + +pub struct NewCommand { + user_id: i32, +} + +impl NewCommand { + pub fn new(user_id: i32) -> Self { + Self { user_id } + } +} + +impl crate::console::commands::CallableTrait for NewCommand { + fn call(&self) -> Result<(), Box> { + rt::System::new().block_on(async { + let settings = get_configuration().expect("Failed to read configuration."); + let db_pool = PgPool::connect(&settings.database.connection_string()) + .await + .expect("Failed to connect to database."); + + let settings = web::Data::new(settings); + let db_pool = web::Data::new(db_pool); + + //todo get user from TryDirect + let user = crate::models::user::User { + id: format!("{}", self.user_id), + first_name: "first_name".to_string(), + last_name: "last_name".to_string(), + email: "email".to_string(), + email_confirmed: true, + role: "role".to_string() + }; + crate::routes::client::add_handler_inner(&user.id, settings, db_pool).await?; + + Ok(()) + }) + } +} diff --git a/src/console/commands/callable.rs b/src/console/commands/callable.rs new file mode 100644 index 0000000..45e7124 --- /dev/null +++ b/src/console/commands/callable.rs @@ -0,0 +1,3 @@ +pub trait CallableTrait { + fn call(&self) -> Result<(), Box>; +} diff --git a/src/console/commands/debug/casbin.rs b/src/console/commands/debug/casbin.rs new file mode 100644 index 0000000..afc685e --- /dev/null +++ b/src/console/commands/debug/casbin.rs @@ -0,0 +1,49 @@ +use crate::configuration::get_configuration; +use actix_web::{rt, post, web, HttpResponse, Result, http::header::ContentType}; +use crate::middleware; +use casbin::CoreApi; +use sqlx::PgPool; + +pub struct CasbinCommand { + action: String, + path: String, + subject: String +} + +impl CasbinCommand { + pub fn new(action: String, path: String, subject: String) -> Self { + Self { action, path, subject } + } +} + +impl crate::console::commands::CallableTrait for CasbinCommand { + fn call(&self) -> Result<(), Box> { + rt::System::new().block_on(async { + let settings = get_configuration().expect("Failed to read configuration."); + let db_pool = PgPool::connect(&settings.database.connection_string()) + .await + .expect("Failed to connect to database."); + + let settings = web::Data::new(settings); + let db_pool = web::Data::new(db_pool); + + + let mut authorizationService = middleware::authorization::try_new(settings.database.connection_string()).await?; + let casbin_enforcer = authorizationService.get_enforcer(); + + let mut lock = casbin_enforcer.write().await; + let policies = lock.get_model().get_model().get("p").unwrap().get("p").unwrap().get_policy(); + for (pos, policy) in policies.iter().enumerate() { + println!("{pos}: {policy:?}"); + } + + #[cfg(feature = "explain")] + { + lock.enable_log(true); + } + lock.enforce_mut(vec![self.subject.clone(), self.path.clone(), self.action.clone()]); + + Ok(()) + }) + } +} diff --git a/src/console/commands/debug/dockerhub.rs b/src/console/commands/debug/dockerhub.rs new file mode 100644 index 0000000..7067ce9 --- /dev/null +++ b/src/console/commands/debug/dockerhub.rs @@ -0,0 +1,36 @@ +use actix_web::{rt, Result}; +use crate::helpers::dockerhub::DockerHub; +use crate::forms::project::DockerImage; + +use tracing_subscriber::FmtSubscriber; + +pub struct DockerhubCommand { + json: String, +} + +impl DockerhubCommand { + pub fn new(json: String) -> Self { + Self { json } + } +} + +impl crate::console::commands::CallableTrait for DockerhubCommand { + fn call(&self) -> Result<(), Box> { + let subscriber = FmtSubscriber::builder() + .with_max_level(tracing::Level::DEBUG) + .finish(); + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + + + rt::System::new().block_on(async { + println!("{}", self.json); + let dockerImage: DockerImage = serde_json::from_str(&self.json)?; + let mut dockerhub = DockerHub::try_from(&dockerImage)?; + let isActive = dockerhub.is_active().await?; + + println!("image is active: {isActive}"); + + Ok(()) + }) + } +} diff --git a/src/console/commands/debug/json.rs b/src/console/commands/debug/json.rs new file mode 100644 index 0000000..e05e3b0 --- /dev/null +++ b/src/console/commands/debug/json.rs @@ -0,0 +1,46 @@ +use actix_web::{Result}; + +pub struct JsonCommand { + line: usize, + column: usize, + payload: String +} + +impl JsonCommand { + pub fn new(line: usize, column: usize, payload: String) -> Self { + Self { line, column, payload } + } +} + +impl crate::console::commands::CallableTrait for JsonCommand { + fn call(&self) -> Result<(), Box> { + let payload: String = std::fs::read_to_string(&self.payload)?; + let index = line_column_to_index(payload.as_ref(), self.line, self.column); + let prefix = String::from_utf8(>::as_ref(&payload)[..index].to_vec()).unwrap(); + + println!("{}", prefix); + Ok(()) + } +} + +fn line_column_to_index(u8slice: &[u8], line: usize, column: usize) -> usize { + let mut l = 1; + let mut c = 0; + let mut i = 0; + for ch in u8slice { + i += 1; + match ch { + b'\n' => { + l += 1; + c = 0; + } + _ => { + c += 1; + } + } + if line == l && c == column { + break; + } + } + return i; +} diff --git a/src/console/commands/debug/mod.rs b/src/console/commands/debug/mod.rs new file mode 100644 index 0000000..0b5119d --- /dev/null +++ b/src/console/commands/debug/mod.rs @@ -0,0 +1,7 @@ +mod json; +mod casbin; +mod dockerhub; + +pub use json::*; +pub use casbin::*; +pub use dockerhub::*; diff --git a/src/console/commands/mod.rs b/src/console/commands/mod.rs new file mode 100644 index 0000000..41e5329 --- /dev/null +++ b/src/console/commands/mod.rs @@ -0,0 +1,7 @@ +pub mod appclient; +pub mod debug; +mod callable; +pub mod mq; + +pub use callable::*; +pub use mq::*; diff --git a/src/console/commands/mq/listener.rs b/src/console/commands/mq/listener.rs new file mode 100644 index 0000000..5d4b0c7 --- /dev/null +++ b/src/console/commands/mq/listener.rs @@ -0,0 +1,124 @@ +use crate::configuration::get_configuration; +use actix_web::rt; +use actix_web::web; +use chrono::Utc; +use lapin::options::{BasicAckOptions, BasicConsumeOptions}; +use lapin::types::FieldTable; +use sqlx::PgPool; +use db::deployment; +use crate::db; +use crate::helpers::mq_manager::MqManager; +use futures_lite::stream::StreamExt; +use serde_derive::{Deserialize, Serialize}; + +pub struct ListenCommand { +} + +#[derive(Serialize, Deserialize, Debug)] +struct ProgressMessage { + id: String, + deploy_id: Option, + alert: i32, + message: String, + status: String, + progress: String +} + +impl ListenCommand { + pub fn new() -> Self { + Self {} + } +} + +impl crate::console::commands::CallableTrait for ListenCommand { + + fn call(&self) -> Result<(), Box> { + rt::System::new().block_on(async { + let settings = get_configuration().expect("Failed to read configuration."); + let db_pool = PgPool::connect(&settings.database.connection_string()) + .await + .expect("Failed to connect to database."); + + let db_pool = web::Data::new(db_pool); + + println!("Declare exchange"); + let mq_manager = MqManager::try_new(settings.amqp.connection_string())?; + let queue_name = "stacker_listener"; + // let queue_name = "install_progress_m383emvfP9zQKs8lkgSU_Q"; + // let queue_name = "install_progress_hy181TZa4DaabUZWklsrxw"; + let consumer_channel= mq_manager + .consume( + "install_progress", + queue_name, + "install.progress.*.*.*" + ) + .await?; + + + println!("Declare queue"); + let mut consumer = consumer_channel + .basic_consume( + queue_name, + "console_listener", + BasicConsumeOptions::default(), + FieldTable::default(), + ) + .await + .expect("Basic consume"); + + println!("Waiting for messages .."); + while let Some(delivery) = consumer.next().await { + // println!("checking messages delivery {:?}", delivery); + let delivery = delivery.expect("error in consumer"); + let s:String = match String::from_utf8(delivery.data.to_owned()) { + //delivery.data is of type Vec + Ok(v) => v, + Err(e) => panic!("Invalid UTF-8 sequence: {}", e), + }; + + let statuses = vec![ + "completed", + "paused", + "failed", + "in_progress", + "error", + "wait_resume", + "wait_start", + "confirmed" + ]; + match serde_json::from_str::(&s) { + Ok(msg) => { + println!("message {:?}", s); + + if statuses.contains(&(msg.status.as_ref())) && msg.deploy_id.is_some() { + println!("Update DB on status change .."); + let id = msg.deploy_id.unwrap() + .parse::() + .map_err(|_err| "Could not parse deployment id".to_string())?; + + match deployment::fetch( + db_pool.get_ref(), id + ) + .await? { + Some(mut row) => { + row.status = msg.status; + row.updated_at = Utc::now(); + println!("Deployment {} updated with status {}", + &id, &row.status + ); + deployment::update(db_pool.get_ref(), row).await?; + } + None => println!("Deployment record was not found in db") + } + } + } + Err(_err) => { tracing::debug!("Invalid message format {:?}", _err)} + } + + delivery.ack(BasicAckOptions::default()).await.expect("ack"); + } + + Ok(()) + }) + } +} diff --git a/src/console/commands/mq/mod.rs b/src/console/commands/mq/mod.rs new file mode 100644 index 0000000..0d4c7ef --- /dev/null +++ b/src/console/commands/mq/mod.rs @@ -0,0 +1,2 @@ +mod listener; +pub use listener::*; \ No newline at end of file diff --git a/src/console/main.rs b/src/console/main.rs new file mode 100644 index 0000000..0bdc1f4 --- /dev/null +++ b/src/console/main.rs @@ -0,0 +1,93 @@ +use clap::{Parser, Subcommand}; + +#[derive(Parser, Debug)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Debug, Subcommand)] +enum Commands { + AppClient { + #[command(subcommand)] + command: AppClientCommands, + }, + Debug { + #[command(subcommand)] + command: DebugCommands, + }, + MQ { + #[command(subcommand)] + command: AppMqCommands, + } +} + +#[derive(Debug, Subcommand)] +enum AppClientCommands { + New { + #[arg(long)] + user_id: i32, + }, +} + +#[derive(Debug, Subcommand)] +enum DebugCommands { + Json { + #[arg(long)] + line: usize, + #[arg(long)] + column: usize, + #[arg(long)] + payload: String, + }, + Casbin { + #[arg(long)] + action: String, + #[arg(long)] + path: String, + #[arg(long)] + subject: String, + }, + Dockerhub { + #[arg(long)] + json: String, + } +} + +#[derive(Debug, Subcommand)] +enum AppMqCommands { + Listen { + }, +} + +fn main() -> Result<(), Box> { + let cli = Cli::parse(); + + get_command(cli)?.call() +} + +fn get_command(cli: Cli) -> Result, String> { + match cli.command { + Commands::AppClient { command } => match command { + AppClientCommands::New { user_id } => Ok(Box::new( + stacker::console::commands::appclient::NewCommand::new(user_id), + )), + }, + Commands::Debug { command } => match command { + DebugCommands::Json { line, column, payload } => Ok(Box::new( + stacker::console::commands::debug::JsonCommand::new(line, column, payload), + )), + DebugCommands::Casbin { action, path, subject } => Ok(Box::new( + stacker::console::commands::debug::CasbinCommand::new(action, path, subject), + )), + DebugCommands::Dockerhub { json } => Ok(Box::new( + stacker::console::commands::debug::DockerhubCommand::new(json), + )), + }, + Commands::MQ { command} => match command { + AppMqCommands::Listen {} => Ok(Box::new( + stacker::console::commands::mq::ListenCommand::new(), + )), + } + } +} diff --git a/src/console/mod.rs b/src/console/mod.rs new file mode 100644 index 0000000..82b6da3 --- /dev/null +++ b/src/console/mod.rs @@ -0,0 +1 @@ +pub mod commands; diff --git a/src/db/client.rs b/src/db/client.rs new file mode 100644 index 0000000..8f13d9a --- /dev/null +++ b/src/db/client.rs @@ -0,0 +1,105 @@ +use sqlx::PgPool; +use crate::models; +use tracing::Instrument; + +pub async fn update(pool: &PgPool, client: models::Client) -> Result { + let query_span = tracing::info_span!("Updating client into the database"); + sqlx::query!( + r#" + UPDATE client + SET + secret=$1, + updated_at=NOW() at time zone 'utc' + WHERE id = $2 + "#, + client.secret, + client.id + ) + .execute(pool) + .instrument(query_span) + .await + .map(|_|{ + tracing::info!("Client {} has been saved to the database", client.id); + client + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "".to_string() + }) +} + +pub async fn fetch(pool: &PgPool, id: i32) -> Result, String> { + let query_span = tracing::info_span!("Fetching the client by ID"); + sqlx::query_as!( + models::Client, + r#" + SELECT + id, + user_id, + secret + FROM client c + WHERE c.id = $1 + LIMIT 1 + "#, + id, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|client| Some(client)) + .or_else(|e| { + match e { + sqlx::Error::RowNotFound => Ok(None), + s => { + tracing::error!("Failed to execute fetch query: {:?}", s); + Err("".to_string()) + } + } + }) +} + +pub async fn count_by_user(pool: &PgPool , user_id: &String) -> Result { + let query_span = tracing::info_span!("Counting the user's clients"); + + sqlx::query!( + r#" + SELECT + count(*) as client_count + FROM client c + WHERE c.user_id = $1 + "#, + user_id.clone(), + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|result| {result.client_count.unwrap()}) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "Internal Server Error".to_string() + }) +} + +pub async fn insert(pool: &PgPool, mut client: models::Client) -> Result { + let query_span = tracing::info_span!("Saving new client into the database"); + sqlx::query!( + r#" + INSERT INTO client (user_id, secret, created_at, updated_at) + VALUES ($1, $2, NOW() at time zone 'utc', NOW() at time zone 'utc') + RETURNING id + "#, + client.user_id.clone(), + client.secret, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + client.id = result.id; + client + }) + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} diff --git a/src/db/cloud.rs b/src/db/cloud.rs new file mode 100644 index 0000000..92f79d1 --- /dev/null +++ b/src/db/cloud.rs @@ -0,0 +1,155 @@ +use crate::models; +use sqlx::PgPool; +use tracing::Instrument; + +pub async fn fetch(pool: &PgPool, id: i32) -> Result, String> { + tracing::info!("Fetch cloud {}", id); + sqlx::query_as!( + models::Cloud, + r#"SELECT * FROM cloud WHERE id=$1 LIMIT 1 "#, id + ) + .fetch_one(pool) + .await + .map(|cloud| Some(cloud)) + .or_else(|err| match err { + sqlx::Error::RowNotFound => Ok(None), + e => { + tracing::error!("Failed to fetch cloud, error: {:?}", e); + Err("Could not fetch data".to_string()) + } + }) +} + +pub async fn fetch_by_user(pool: &PgPool, user_id: &str) -> Result, String> { + let query_span = tracing::info_span!("Fetch clouds by user id."); + sqlx::query_as!( + models::Cloud, + r#" + SELECT + * + FROM cloud + WHERE user_id=$1 + "#, + user_id + ) + .fetch_all(pool) + .instrument(query_span) + .await + .map_err(|err| { + tracing::error!("Failed to fetch cloud, error: {:?}", err); + "".to_string() + }) +} + + +pub async fn insert(pool: &PgPool, mut cloud: models::Cloud) -> Result { + let query_span = tracing::info_span!("Saving user's cloud data into the database"); + sqlx::query!( + r#" + INSERT INTO cloud ( + user_id, + provider, + cloud_token, + cloud_key, + cloud_secret, + save_token, + created_at, + updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') + RETURNING id; + "#, + cloud.user_id, + cloud.provider, + cloud.cloud_token, + cloud.cloud_key, + cloud.cloud_secret, + cloud.save_token + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + cloud.id = result.id; + cloud + }) + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} + +pub async fn update(pool: &PgPool, mut cloud: models::Cloud) -> Result { + let query_span = tracing::info_span!("Updating user cloud"); + sqlx::query_as!( + models::Cloud, + r#" + UPDATE cloud + SET + user_id=$2, + provider=$3, + cloud_token=$4, + cloud_key=$5, + cloud_secret=$6, + save_token=$7, + updated_at=NOW() at time zone 'utc' + WHERE id = $1 + RETURNING * + "#, + cloud.id, + cloud.user_id, + cloud.provider, + cloud.cloud_token, + cloud.cloud_key, + cloud.cloud_secret, + cloud.save_token + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|result|{ + tracing::info!("Cloud info {} have been saved", cloud.id); + cloud.updated_at = result.updated_at; + cloud + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "".to_string() + }) +} + +#[tracing::instrument(name = "Delete cloud of a user.")] +pub async fn delete(pool: &PgPool, id: i32) -> Result { + tracing::info!("Delete cloud {}", id); + let mut tx = match pool.begin().await { + Ok(result) => result, + Err(err) => { + tracing::error!("Failed to begin transaction: {:?}", err); + return Err("".to_string()); + } + }; + + let delete_query = " DELETE FROM cloud WHERE id = $1; "; + + match sqlx::query(delete_query) + .bind(id) + .execute(&mut tx) + .await + .map_err(|err| { + println!("{:?}", err) + }) + { + Ok(_) => { + let _ = tx.commit().await.map_err(|err| { + tracing::error!("Failed to commit transaction: {:?}", err); + false + }); + Ok(true) + } + Err(_err) => { + let _ = tx.rollback().await.map_err(|err| println!("{:?}", err)); + Ok(false) + } + } + +} diff --git a/src/db/deployment.rs b/src/db/deployment.rs new file mode 100644 index 0000000..7f78f0c --- /dev/null +++ b/src/db/deployment.rs @@ -0,0 +1,92 @@ +use crate::models; +use sqlx::PgPool; +use tracing::Instrument; + + +pub async fn fetch(pool: &PgPool, id: i32) -> Result, String> { + tracing::info!("Fetch deployment {}", id); + sqlx::query_as!( + models::Deployment, + r#" + SELECT + * + FROM deployment + WHERE id=$1 + LIMIT 1 + "#, + id + ) + .fetch_one(pool) + .await + .map(|deployment| Some(deployment)) + .or_else(|err| match err { + sqlx::Error::RowNotFound => Ok(None), + e => { + tracing::error!("Failed to fetch deployment, error: {:?}", e); + Err("Could not fetch data".to_string()) + } + }) +} + +pub async fn insert(pool: &PgPool, mut deployment: models::Deployment) -> Result { + let query_span = tracing::info_span!("Saving new deployment into the database"); + sqlx::query!( + r#" + INSERT INTO deployment (project_id, deleted, status, body, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id; + "#, + deployment.project_id, + deployment.deleted, + deployment.status, + deployment.body, + deployment.created_at, + deployment.updated_at, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + deployment.id = result.id; + deployment + }) + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} + +pub async fn update(pool: &PgPool, mut deployment: models::Deployment) -> Result { + let query_span = tracing::info_span!("Updating user deployment into the database"); + sqlx::query_as!( + models::Deployment, + r#" + UPDATE deployment + SET + project_id=$2, + deleted=$3, + status=$4, + body=$5, + updated_at=NOW() at time zone 'utc' + WHERE id = $1 + RETURNING * + "#, + deployment.id, + deployment.project_id, + deployment.deleted, + deployment.status, + deployment.body, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|result|{ + tracing::info!("Deployment {} has been updated", deployment.id); + deployment.updated_at = result.updated_at; + deployment + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "".to_string() + }) +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..3585327 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,7 @@ +pub mod client; +pub mod product; +pub mod rating; +pub mod project; +pub(crate) mod deployment; +pub(crate) mod cloud; +pub(crate) mod server; diff --git a/src/db/product.rs b/src/db/product.rs new file mode 100644 index 0000000..e9c591a --- /dev/null +++ b/src/db/product.rs @@ -0,0 +1,30 @@ +use sqlx::PgPool; +use crate::models; +use tracing::Instrument; + +pub async fn fetch_by_obj(pg_pool: &PgPool, obj_id: i32) -> Result, String> { + let query_span = tracing::info_span!("Check product existence by id."); + sqlx::query_as!( + models::Product, + r#"SELECT + * + FROM product + WHERE obj_id = $1 + LIMIT 1 + "#, + obj_id + ) + .fetch_one(pg_pool) + .instrument(query_span) + .await + .map(|product| Some(product)) + .or_else(|e| { + match e { + sqlx::Error::RowNotFound => Ok(None), + s => { + tracing::error!("Failed to execute fetch query: {:?}", s); + Err("".to_string()) + } + } + }) +} diff --git a/src/db/project.rs b/src/db/project.rs new file mode 100644 index 0000000..0e8e24c --- /dev/null +++ b/src/db/project.rs @@ -0,0 +1,183 @@ +use crate::models; +use sqlx::PgPool; +use tracing::Instrument; + +pub async fn fetch(pool: &PgPool, id: i32) -> Result, String> { + tracing::info!("Fetch project {}", id); + sqlx::query_as!( + models::Project, + r#" + SELECT + * + FROM project + WHERE id=$1 + LIMIT 1 + "#, + id + ) + .fetch_one(pool) + .await + .map(|project| Some(project)) + .or_else(|err| match err { + sqlx::Error::RowNotFound => Ok(None), + e => { + tracing::error!("Failed to fetch project, error: {:?}", e); + Err("Could not fetch data".to_string()) + } + }) +} + +pub async fn fetch_by_user(pool: &PgPool, user_id: &str) -> Result, String> { + let query_span = tracing::info_span!("Fetch projects by user id."); + sqlx::query_as!( + models::Project, + r#" + SELECT + * + FROM project + WHERE user_id=$1 + "#, + user_id + ) + .fetch_all(pool) + .instrument(query_span) + .await + .map_err(|err| { + tracing::error!("Failed to fetch project, error: {:?}", err); + "".to_string() + }) +} + +pub async fn fetch_one_by_name(pool: &PgPool, name: &str) -> Result, String> { + let query_span = tracing::info_span!("Fetch one project by name."); + sqlx::query_as!( + models::Project, + r#" + SELECT + * + FROM project + WHERE name=$1 + LIMIT 1 + "#, + name + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|project| Some(project)) + .or_else(|err| match err { + sqlx::Error::RowNotFound => Ok(None), + err => { + tracing::error!("Failed to fetch one project by name, error: {:?}", err); + Err("".to_string()) + } + }) +} + +pub async fn insert(pool: &PgPool, mut project: models::Project) -> Result { + let query_span = tracing::info_span!("Saving new project into the database"); + sqlx::query!( + r#" + INSERT INTO project (stack_id, user_id, name, body, created_at, updated_at, request_json) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id; + "#, + project.stack_id, + project.user_id, + project.name, + project.body, + project.created_at, + project.updated_at, + project.request_json, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + project.id = result.id; + project + }) + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} + +pub async fn update(pool: &PgPool, mut project: models::Project) -> Result { + let query_span = tracing::info_span!("Updating project"); + sqlx::query_as!( + models::Project, + r#" + UPDATE project + SET + stack_id=$2, + user_id=$3, + name=$4, + body=$5, + request_json=$6, + updated_at=NOW() at time zone 'utc' + WHERE id = $1 + RETURNING * + "#, + project.id, + project.stack_id, + project.user_id, + project.name, + project.body, + project.request_json + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|result|{ + tracing::info!("Project {} has been saved to database", project.id); + project.updated_at = result.updated_at; + project + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "".to_string() + }) +} + +#[tracing::instrument(name = "Delete user's project.")] +pub async fn delete(pool: &PgPool, id: i32) -> Result { + tracing::info!("Delete project {}", id); + let mut tx = match pool.begin().await { + Ok(result) => result, + Err(err) => { + tracing::error!("Failed to begin transaction: {:?}", err); + return Err("".to_string()); + } + }; + + // Combine delete queries into a single query + let delete_query = " + --DELETE FROM deployment WHERE project_id = $1; // on delete cascade + --DELETE FROM server WHERE project_id = $1; // on delete cascade + DELETE FROM project WHERE id = $1; + "; + + match sqlx::query(delete_query) + .bind(id) + .execute(&mut tx) + .await + .map_err(|err| { + println!("{:?}", err) + }) + { + Ok(_) => { + let _ = tx.commit().await.map_err(|err| { + tracing::error!("Failed to commit transaction: {:?}", err); + false + }); + Ok(true) + } + Err(_err) => { + let _ = tx.rollback().await.map_err(|err| println!("{:?}", err)); + Ok(false) + } + // todo, when empty commit() + } +} + diff --git a/src/db/rating.rs b/src/db/rating.rs new file mode 100644 index 0000000..2a3192e --- /dev/null +++ b/src/db/rating.rs @@ -0,0 +1,215 @@ +use sqlx::PgPool; +use crate::models; +use tracing::Instrument; + +pub async fn fetch_all(pool: &PgPool) -> Result, String> { + let query_span = tracing::info_span!("Fetch all ratings."); + sqlx::query_as!( + models::Rating, + r#"SELECT + id, + user_id, + obj_id, + category as "category: _", + comment, + hidden, + rate, + created_at, + updated_at + FROM rating + ORDER BY id DESC + "# + ) + .fetch_all(pool) + .instrument(query_span) + .await + .map_err(|e| { + tracing::error!("Failed to execute fetch query: {:?}", e); + "".to_string() + }) +} + +pub async fn fetch(pool: &PgPool, id: i32) -> Result, String> { + let query_span = tracing::info_span!("Fetch rating by id"); + sqlx::query_as!( + models::Rating, + r#"SELECT + id, + user_id, + obj_id, + category as "category: _", + comment, + hidden, + rate, + created_at, + updated_at + FROM rating + WHERE id=$1 + LIMIT 1"#, + id + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|rating| Some(rating)) + .or_else(|e| { + match e { + sqlx::Error::RowNotFound => Ok(None), + s => { + tracing::error!("Failed to execute fetch query: {:?}", s); + Err("".to_string()) + } + } + }) +} + +pub async fn fetch_by_obj_and_user_and_category( + pool: &PgPool, + obj_id: i32, + user_id: String, + category: models::RateCategory, +) -> Result, String> { + let query_span = tracing::info_span!("Fetch rating by obj, user and category."); + sqlx::query_as!( + models::Rating, + r#"SELECT + id, + user_id, + obj_id, + category as "category: _", + comment, + hidden, + rate, + created_at, + updated_at + FROM rating + WHERE user_id=$1 + AND obj_id=$2 + AND category=$3 + LIMIT 1"#, + user_id, + obj_id, + category as _ + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|rating| Some(rating)) + .or_else(|e| { + match e { + sqlx::Error::RowNotFound => Ok(None), + s => { + tracing::error!("Failed to execute fetch query: {:?}", s); + Err("".to_string()) + } + } + }) +} + +pub async fn insert(pool: &PgPool, mut rating: models::Rating) -> Result { + let query_span = tracing::info_span!("Saving new rating details into the database"); + sqlx::query!( + r#" + INSERT INTO rating (user_id, obj_id, category, comment, hidden, rate, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') + RETURNING id + "#, + rating.user_id, + rating.obj_id, + rating.category as _, + rating.comment, + rating.hidden, + rating.rate + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + rating.id = result.id; + rating + }) + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} + +pub async fn update(pool: &PgPool, rating: models::Rating) -> Result { + let query_span = tracing::info_span!("Updating rating into the database"); + sqlx::query!( + r#" + UPDATE rating + SET + comment=$1, + rate=$2, + hidden=$3, + updated_at=NOW() at time zone 'utc' + WHERE id = $4 + "#, + rating.comment, + rating.rate, + rating.hidden, + rating.id + ) + .execute(pool) + .instrument(query_span) + .await + .map(|_|{ + tracing::info!("Rating {} has been saved to the database", rating.id); + rating + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "".to_string() + }) +} + +pub async fn fetch_all_visible(pool: &PgPool) -> Result, String> { + let query_span = tracing::info_span!("Fetch all ratings."); + sqlx::query_as!( + models::Rating, + r#"SELECT + id, + user_id, + obj_id, + category as "category: _", + comment, + hidden, + rate, + created_at, + updated_at + FROM rating + WHERE hidden = false + ORDER BY id DESC + "#, + ) + .fetch_all(pool) + .instrument(query_span) + .await + .map_err(|e| { + tracing::error!("Failed to execute fetch query: {:?}", e); + "".to_string() + }) +} + +pub async fn delete(pool: &PgPool, rating: models::Rating) -> Result<(), String> { + let query_span = tracing::info_span!("Deleting rating from the database"); + sqlx::query!( + r#" + DELETE FROM rating + WHERE id = $1 + "#, + rating.id + ) + .execute(pool) + .instrument(query_span) + .await + .map(|_|{ + tracing::info!("Rating {} has been deleted from the database", rating.id); + () + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "".to_string() + }) +} diff --git a/src/db/server.rs b/src/db/server.rs new file mode 100644 index 0000000..60eafb1 --- /dev/null +++ b/src/db/server.rs @@ -0,0 +1,189 @@ +use crate::models; +use sqlx::PgPool; +use tracing::Instrument; + +pub async fn fetch(pool: &PgPool, id: i32) -> Result, String> { + tracing::info!("Fetch server {}", id); + sqlx::query_as!( + models::Server, + r#"SELECT * FROM server WHERE id=$1 LIMIT 1 "#, id + ) + .fetch_one(pool) + .await + .map(|server| Some(server)) + .or_else(|err| match err { + sqlx::Error::RowNotFound => Ok(None), + e => { + tracing::error!("Failed to fetch server, error: {:?}", e); + Err("Could not fetch data".to_string()) + } + }) +} + +pub async fn fetch_by_user(pool: &PgPool, user_id: &str) -> Result, String> { + let query_span = tracing::info_span!("Fetch servers by user id."); + sqlx::query_as!( + models::Server, + r#" + SELECT + * + FROM server + WHERE user_id=$1 + "#, + user_id + ) + .fetch_all(pool) + .instrument(query_span) + .await + .map_err(|err| { + tracing::error!("Failed to fetch server, error: {:?}", err); + "".to_string() + }) +} + + +pub async fn fetch_by_project(pool: &PgPool, project_id: i32) -> Result, String> { + let query_span = tracing::info_span!("Fetch servers by project/project id."); + sqlx::query_as!( + models::Server, + r#" + SELECT + * + FROM server + WHERE project_id=$1 + "#, + project_id + ) + .fetch_all(pool) + .instrument(query_span) + .await + .map_err(|err| { + tracing::error!("Failed to fetch servers, error: {:?}", err); + "".to_string() + }) +} + + +pub async fn insert(pool: &PgPool, mut server: models::Server) -> Result { + let query_span = tracing::info_span!("Saving user's server data into the database"); + sqlx::query!( + r#" + INSERT INTO server ( + user_id, + project_id, + region, + zone, + server, + os, + disk_type, + created_at, + updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, NOW() at time zone 'utc',NOW() at time zone 'utc') + RETURNING id; + "#, + server.user_id, + server.project_id, + server.region, + server.zone, + server.server, + server.os, + server.disk_type + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(move |result| { + server.id = result.id; + server + }) + .map_err(|e| { + + // match err { + // sqlx::error::ErrorKind::ForeignKeyViolation => { + // return JsonResponse::::build().bad_request(""); + // } + // _ => { + // return JsonResponse::::build().internal_server_error("Failed to insert"); + // } + // }) + tracing::error!("Failed to execute query: {:?}", e); + "Failed to insert".to_string() + }) +} + +pub async fn update(pool: &PgPool, mut server: models::Server) -> Result { + let query_span = tracing::info_span!("Updating user server"); + sqlx::query_as!( + models::Server, + r#" + UPDATE server + SET + user_id=$2, + project_id=$3, + region=$4, + zone=$5, + server=$6, + os=$7, + disk_type=$8, + updated_at=NOW() at time zone 'utc' + WHERE id = $1 + RETURNING * + "#, + server.id, + server.user_id, + server.project_id, + server.region, + server.zone, + server.server, + server.os, + server.disk_type, + ) + .fetch_one(pool) + .instrument(query_span) + .await + .map(|result|{ + tracing::info!("Server info {} have been saved", server.id); + server.updated_at = result.updated_at; + server + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + "".to_string() + }) +} + +#[tracing::instrument(name = "Delete user's server.")] +pub async fn delete(pool: &PgPool, id: i32) -> Result { + tracing::info!("Delete server {}", id); + let mut tx = match pool.begin().await { + Ok(result) => result, + Err(err) => { + tracing::error!("Failed to begin transaction: {:?}", err); + return Err("".to_string()); + } + }; + + let delete_query = " DELETE FROM server WHERE id = $1; "; + + match sqlx::query(delete_query) + .bind(id) + .execute(&mut tx) + .await + .map_err(|err| { + println!("{:?}", err) + }) + { + Ok(_) => { + let _ = tx.commit().await.map_err(|err| { + tracing::error!("Failed to commit transaction: {:?}", err); + false + }); + Ok(true) + } + Err(_err) => { + let _ = tx.rollback().await.map_err(|err| println!("{:?}", err)); + Ok(false) + } + } + +} diff --git a/src/forms/cloud.rs b/src/forms/cloud.rs new file mode 100644 index 0000000..fe4cdf6 --- /dev/null +++ b/src/forms/cloud.rs @@ -0,0 +1,210 @@ +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; +use crate::helpers::cloud::security::Secret; +use tracing::Instrument; +use chrono::Utc; + + +fn hide_parts(value: String) -> String { + value.chars().into_iter().take(6).collect::() + "****" +} + +#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct CloudForm { + pub user_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub project_id: Option, + #[validate(min_length = 2)] + #[validate(max_length = 50)] + pub provider: String, + pub cloud_token: Option, + pub cloud_key: Option, + pub cloud_secret: Option, + pub save_token: Option, +} + +impl CloudForm { + #[tracing::instrument(name = "impl CloudForm::decode()")] + pub(crate) fn decode(secret: &mut Secret, encrypted_value: String) -> String { + // tracing::error!("encrypted_value {:?}", &encrypted_value); + let b64_decoded = Secret::b64_decode(&encrypted_value).unwrap(); + // tracing::error!("decoded {:?}", &b64_decoded); + match secret.decrypt(b64_decoded) { + Ok(decoded) => decoded, + Err(_err) => { + tracing::error!("🟥 Could not decode {:?},{:?}",secret.field,_err); + // panic!("Could not decode "); + "".to_owned() + } + } + } + + pub(crate) fn decrypt_field( + secret: &mut Secret, + field_name: &str, + encrypted_value: Option, + reveal: bool, + ) -> Option { + if let Some(val) = encrypted_value { + secret.field = field_name.to_owned(); + let decoded_value = CloudForm::decode(secret, val); + if reveal { + return Some(decoded_value); + } else { + return Some(hide_parts(decoded_value)); + } + } + None + } + + // @todo should be refactored, may be moved to cloud.into() or Secret::from() + #[tracing::instrument(name = "decode_model")] + pub fn decode_model(mut cloud: models::Cloud, reveal:bool) -> models::Cloud { + + let mut secret = Secret::new(); + secret.user_id = cloud.user_id.clone(); + secret.provider = cloud.provider.clone(); + cloud.cloud_token = CloudForm::decrypt_field(&mut secret, "cloud_token", cloud.cloud_token.clone(), reveal); + cloud.cloud_secret = CloudForm::decrypt_field(&mut secret, "cloud_secret", cloud.cloud_secret.clone(), reveal); + cloud.cloud_key = CloudForm::decrypt_field(&mut secret, "cloud_key", cloud.cloud_key.clone(), reveal); + + cloud + } +} + +impl std::fmt::Debug for CloudForm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let cloud_key: String = match self.cloud_key.as_ref() { + Some(val) => + { + val.chars().take(4).collect::() + "****" + }, + None => "".to_string(), + }; + let cloud_token: String = match self.cloud_token.as_ref() { + Some(val) => { + eprintln!("cloud token {val:?}"); + val.chars().take(4).collect::() + "****" + }, + None => "".to_string(), + }; + + let cloud_secret: String = match self.cloud_secret.as_ref() { + Some(val) => { + val.chars().take(4).collect::() + "****" + } + None => "".to_string(), + }; + + write!(f, "{} cloud creds: cloud_key : {} cloud_token: {} cloud_secret: {} project_id: {:?}", + self.provider, + cloud_key, + cloud_token, + cloud_secret, + self.project_id + ) + } +} + +fn encrypt_field( + secret: &mut Secret, + field_name: &str, + value: Option, +) -> Option { + if let Some(val) = value { + secret.field = field_name.to_owned(); + if let Ok(encrypted) = secret.encrypt(val) { + return Some(Secret::b64_encode(&encrypted)); + } + } + None +} + +impl Into for &CloudForm { + #[tracing::instrument(name = "impl Into for &CloudForm")] + fn into(self) -> models::Cloud { + let mut cloud = models::Cloud::default(); + cloud.provider = self.provider.clone(); + cloud.user_id = self.user_id.clone().unwrap(); + + if Some(true) == self.save_token { + let mut secret = Secret::new(); + secret.user_id = self.user_id.clone().unwrap(); + secret.provider = self.provider.clone(); + + cloud.cloud_token = encrypt_field(&mut secret, "cloud_token", self.cloud_token.clone()); + cloud.cloud_key = encrypt_field(&mut secret, "cloud_key", self.cloud_key.clone()); + cloud.cloud_secret = encrypt_field(&mut secret, "cloud_secret", self.cloud_secret.clone()); + } else { + cloud.cloud_token = self.cloud_token.clone(); + cloud.cloud_key = self.cloud_key.clone(); + cloud.cloud_secret = self.cloud_secret.clone(); + } + cloud.save_token = self.save_token.clone(); + cloud.created_at = Utc::now(); + cloud.updated_at = Utc::now(); + cloud + } + +} + + +// on deploy +impl Into for models::Cloud { + #[tracing::instrument(name = "Into for models::Cloud .")] + fn into(self) -> CloudForm { + let mut form = CloudForm::default(); + form.provider = self.provider.clone(); + + if Some(true) == self.save_token { + let mut secret = Secret::new(); + secret.user_id = self.user_id.clone(); + secret.provider = self.provider; + secret.field = "cloud_token".to_string(); + + let value = match self.cloud_token { + Some(value) => { + CloudForm::decode(&mut secret, value) + } + None => { + tracing::debug!("Skip {}", secret.field); + "".to_string() + } + }; + form.cloud_token = Some(value); + + secret.field = "cloud_key".to_string(); + let value = match self.cloud_key { + Some(value) => { + CloudForm::decode(&mut secret, value) + } + None => { + tracing::debug!("Skipp {}", secret.field); + "".to_string() + } + }; + form.cloud_key = Some(value); + + secret.field = "cloud_secret".to_string(); + let value = match self.cloud_secret { + Some(value) => { + CloudForm::decode(&mut secret, value) + } + None => { + tracing::debug!("Skipp {}", secret.field); + "".to_string() + } + }; + form.cloud_secret = Some(value); + + } else { + form.cloud_token = self.cloud_token; + form.cloud_key = self.cloud_key; + form.cloud_secret = self.cloud_secret; + } + + form.save_token = self.save_token; + form + } +} diff --git a/src/forms/mod.rs b/src/forms/mod.rs index 957c91d..a5651bf 100644 --- a/src/forms/mod.rs +++ b/src/forms/mod.rs @@ -1,8 +1,9 @@ -mod rating; - -pub(crate) mod stack; +pub mod rating; +pub mod project; pub mod user; +pub(crate) mod cloud; +pub(crate) mod server; -pub use rating::*; - -pub use stack::*; +pub use cloud::*; +pub use server::*; +pub use user::UserForm; diff --git a/src/forms/project/app.rs b/src/forms/project/app.rs new file mode 100644 index 0000000..b246829 --- /dev/null +++ b/src/forms/project/app.rs @@ -0,0 +1,164 @@ +use crate::forms; +use docker_compose_types as dctypes; +use indexmap::IndexMap; +use serde_json::Value; +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; +use crate::forms::project::network::Network; +use crate::forms::project::{DockerImage, replace_id_with_name}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct App { + #[serde(rename = "_etag")] + #[validate(min_length = 3)] + #[validate(max_length = 255)] + pub etag: Option, + #[serde(rename = "_id")] + pub id: String, + #[serde(rename = "_created")] + pub created: Option, + #[serde(rename = "_updated")] + pub updated: Option, + #[validate(min_length = 3)] + #[validate(max_length = 50)] + pub name: String, + #[validate(min_length = 3)] + #[validate(max_length = 50)] + pub code: String, + #[validate(min_length = 3)] + #[validate(max_length = 50)] + #[serde(rename = "type")] + pub type_field: String, + #[serde(flatten)] + pub role: forms::project::Role, + pub default: Option, + pub versions: Option>, + #[serde(flatten)] + #[validate] + pub docker_image: DockerImage, + #[serde(flatten)] + #[validate] + pub requirements: forms::project::Requirements, + #[validate(minimum = 1)] + pub popularity: Option, + pub commercial: Option, + pub subscription: Option, + pub autodeploy: Option, + pub suggested: Option, + pub dependency: Option, + pub avoid_render: Option, + pub price: Option, + pub icon: Option, + pub domain: Option, + pub category_id: Option, + pub parent_app_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub descr: Option, + pub full_description: Option, + pub description: Option, + pub plan_type: Option, + pub ansible_var: Option, + pub repo_dir: Option, + pub url_app: Option, + pub url_git: Option, + #[validate(enumerate("always", "no", "unless-stopped", "on-failure"))] + pub restart: String, + pub command: Option, + pub volumes: Option>, + #[serde(flatten)] + pub environment: forms::project::Environment, + #[serde(flatten)] + pub network: forms::project::ServiceNetworks, + #[validate] + pub shared_ports: Option>, +} + +impl App { + #[tracing::instrument(name = "named_volumes")] + pub fn named_volumes(&self) -> IndexMap> { + let mut named_volumes = IndexMap::default(); + + if self.volumes.is_none() { + return named_volumes; + } + + for volume in self.volumes.as_ref().unwrap() { + if !volume.is_named_docker_volume() { + continue; + } + + let k = volume.host_path.as_ref().unwrap().clone(); + let v = dctypes::MapOrEmpty::Map(volume.into()); + named_volumes.insert(k, v); + } + + tracing::debug!("Named volumes: {:?}", named_volumes); + named_volumes + } + + + pub(crate) fn try_into_service(&self, all_networks: &Vec) -> Result { + + let mut service = dctypes::Service { + image: Some(self.docker_image.to_string()), + ..Default::default() + }; + + let networks = dctypes::Networks::try_from(&self.network).unwrap_or_default(); + + let networks = replace_id_with_name(networks, all_networks); + service.networks = dctypes::Networks::Simple(networks); + + let ports: Vec = match &self.shared_ports { + Some(ports) => { + let mut collector = vec![]; + for port in ports { + collector.push(port.try_into()?); + } + collector + } + None => vec![] + }; + + let volumes: Vec = match &self.volumes { + Some(volumes) => { + let mut collector = vec![]; + for volume in volumes { + collector.push(dctypes::Volumes::Advanced(volume.try_into()?)); + } + + collector + }, + None => vec![] + }; + + let mut envs = IndexMap::new(); + for item in self.environment.environment.clone() { + let items = item + .into_iter() + .map(|env_var| (env_var.key, Some(dctypes::SingleValue::String(env_var.value.clone())))) + .collect::>(); + + envs.extend(items); + } + + + service.ports = dctypes::Ports::Long(ports); + service.restart = Some(self.restart.clone()); + if let Some(cmd) = self.command.as_deref() { + if !cmd.is_empty() { + service.command = Some(dctypes::Command::Simple(cmd.to_owned())); + } + } + service.volumes = volumes; + service.environment = dctypes::Environment::KvPair(envs); + + Ok(service) + } +} + +impl AsRef for App { + fn as_ref(&self) -> &forms::project::DockerImage { + &self.docker_image + } +} diff --git a/src/forms/project/compose_networks.rs b/src/forms/project/compose_networks.rs new file mode 100644 index 0000000..b38eb8f --- /dev/null +++ b/src/forms/project/compose_networks.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; +use docker_compose_types as dctypes; +use indexmap::IndexMap; +use crate::forms::project::network::Network; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ComposeNetworks { + pub networks: Option>, +} + +impl Into>> for ComposeNetworks { + fn into(self) -> IndexMap> { + // let mut default_networks = vec![Network::default()]; + let mut default_networks = vec![]; + + let networks = match self.networks { + None => { + default_networks + } + Some(mut nets) => { + if !nets.is_empty() { + nets.append(&mut default_networks); + } + nets + } + }; + + let networks = networks + .into_iter() + .map(|net| { + (net.name.clone(), dctypes::MapOrEmpty::Map(net.into())) + } + ) + .collect::>(); + + tracing::debug!("networks collected {:?}", &networks); + + networks + } +} + diff --git a/src/forms/project/custom.rs b/src/forms/project/custom.rs new file mode 100644 index 0000000..0a4eac7 --- /dev/null +++ b/src/forms/project/custom.rs @@ -0,0 +1,112 @@ +use serde::{Deserialize, Serialize}; +use crate::forms; +use indexmap::IndexMap; +use docker_compose_types as dctypes; +use crate::forms::project::Network; +use serde_valid::Validate; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Custom { + #[validate] + pub web: Vec, + #[validate] + pub feature: Option>, + #[validate] + pub service: Option>, + #[validate(min_length = 3)] + #[validate(max_length = 50)] + pub custom_stack_code: String, + #[validate(min_length = 3)] + #[validate(max_length = 255)] + pub project_git_url: Option, + pub custom_stack_category: Option>, + pub custom_stack_short_description: Option, + pub custom_stack_description: Option, + // #[validate(min_length = 3)] + // #[validate(max_length = 255)] + pub project_name: Option, + pub project_overview: Option, + pub project_description: Option, + #[serde(flatten)] + pub networks: forms::project::ComposeNetworks, // all networks +} + + +fn matches_network_by_id(id: &String, networks: &Vec) -> Option { + + for n in networks.into_iter() { + if id == &n.id { + tracing::debug!("matches: {:?}", n.name); + return Some(n.name.clone()); + } + } + None +} + +pub fn replace_id_with_name(service_networks: dctypes::Networks, all_networks: &Vec) -> Vec { + + match service_networks { + dctypes::Networks::Simple(nets) => { + nets + .iter() + .map(|id| { + if let Some(name) = matches_network_by_id(&id, all_networks) { + name + } else { "".to_string() } + }) + .collect::>() + }, + _ => vec![] + } +} + +impl Custom { + pub fn services(&self) -> Result>, String> { + let mut services = IndexMap::new(); + + let all_networks = self.networks.networks.clone().unwrap_or(vec![]); + + for app_type in &self.web { + let service = app_type.app.try_into_service(&all_networks)?; + services.insert(app_type.app.code.clone().to_owned(), Some(service)); + } + + if let Some(srvs) = &self.service { + for app_type in srvs { + let service = app_type.app.try_into_service(&all_networks)?; + services.insert(app_type.app.code.clone().to_owned(), Some(service)); + } + } + + if let Some(features) = &self.feature { + for app_type in features { + let service = app_type.app.try_into_service(&all_networks)?; + services.insert(app_type.app.code.clone().to_owned(), Some(service)); + } + } + + Ok(services) + } + + pub fn named_volumes(&self) -> Result>, String> { + let mut named_volumes = IndexMap::new(); + + for app_type in &self.web { + named_volumes.extend(app_type.app.named_volumes()); + } + + if let Some(srvs) = &self.service { + for app_type in srvs { + named_volumes.extend(app_type.app.named_volumes()); + } + } + + if let Some(features) = &self.feature { + for app_type in features { + named_volumes.extend(app_type.app.named_volumes()); + } + } + + Ok(named_volumes) + } +} diff --git a/src/forms/project/deploy.rs b/src/forms/project/deploy.rs new file mode 100644 index 0000000..e300a18 --- /dev/null +++ b/src/forms/project/deploy.rs @@ -0,0 +1,27 @@ +use serde_derive::{Deserialize, Serialize}; +use serde_json::Value; +use serde_valid::Validate; +use crate::forms; +use crate::forms::{CloudForm, ServerForm}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Deploy { + #[validate] + pub(crate) stack: Stack, + #[validate] + pub(crate) server: ServerForm, + #[validate] + pub(crate) cloud: CloudForm, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Stack { + #[validate(min_length = 2)] + #[validate(max_length = 255)] + pub stack_code: Option, + pub vars: Option>, + pub integrated_features: Option>, + pub extended_features: Option>, + pub subscriptions: Option>, + pub form_app: Option>, +} \ No newline at end of file diff --git a/src/forms/project/docker_image.rs b/src/forms/project/docker_image.rs new file mode 100644 index 0000000..acfa3d0 --- /dev/null +++ b/src/forms/project/docker_image.rs @@ -0,0 +1,55 @@ +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; +use std::fmt; +use crate::helpers::dockerhub::DockerHub; + + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct DockerImage { + // #[validate(min_length = 3)] + #[validate(max_length = 50)] + // @todo conditional check, if not empty + // #[validate(pattern = r"^[a-z0-9]+([-_.][a-z0-9]+)*$")] + pub dockerhub_user: Option, + // #[validate(min_length = 3)] + #[validate(max_length = 50)] + // @todo conditional check, if not empty + // #[validate(pattern = r"^[a-z0-9]+([-_.][a-z0-9]+)*$")] + pub dockerhub_name: Option, + // #[validate(min_length = 3)] + #[validate(max_length = 100)] + pub dockerhub_image: Option, + pub dockerhub_password: Option, +} + +impl fmt::Display for DockerImage { + // dh_image = trydirect/postgres:latest + // dh_nmsp = trydirect, dh_repo_name=postgres + // dh_nmsp = trydirect dh_repo_name=postgres:v8 + // namespace/repo_name/tag + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let dh_image = self.dockerhub_image.as_ref().map(String::as_str).unwrap_or(""); + println!("{:?}", &dh_image); + let dh_nmspc = self.dockerhub_user.as_ref().map(String::as_str).unwrap_or(""); + println!("{:?}", &dh_nmspc); + let dh_repo_name = self.dockerhub_name.as_ref().map(String::as_str).unwrap_or(""); + println!("{:?}", &dh_repo_name); + + write!( + f, + "{}{}{}", + if !dh_nmspc.is_empty() { format!("{}/", dh_nmspc) } else { String::new() }, + if !dh_repo_name.is_empty() { dh_repo_name } else { dh_image }, + if !dh_repo_name.contains(":") && dh_image.is_empty() { ":latest".to_string() } else { String::new() }, + ) + } +} + +impl DockerImage { + #[tracing::instrument(name = "is_active")] + pub async fn is_active(&self) -> Result { + DockerHub::try_from(self)?.is_active().await + } +} + + diff --git a/src/forms/project/domain_list.rs b/src/forms/project/domain_list.rs new file mode 100644 index 0000000..cf359ec --- /dev/null +++ b/src/forms/project/domain_list.rs @@ -0,0 +1,5 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DomainList {} diff --git a/src/forms/project/environment.rs b/src/forms/project/environment.rs new file mode 100644 index 0000000..071d159 --- /dev/null +++ b/src/forms/project/environment.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Environment { + pub(crate) environment: Option>, +} +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct EnvVar { + pub(crate) key: String, + pub(crate) value: String, +} + diff --git a/src/forms/project/feature.rs b/src/forms/project/feature.rs new file mode 100644 index 0000000..d540572 --- /dev/null +++ b/src/forms/project/feature.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; +use crate::forms::project::*; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Feature { + // #[serde(rename(deserialize = "sharedPorts"))] + // #[serde(rename(serialize = "shared_ports"))] + // #[serde(alias = "shared_ports")] + // pub shared_ports: Option>, + #[serde(flatten)] + pub app: App, + pub custom: Option, +} diff --git a/src/forms/project/form.rs b/src/forms/project/form.rs new file mode 100644 index 0000000..b849abb --- /dev/null +++ b/src/forms/project/form.rs @@ -0,0 +1,65 @@ +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; +use crate::models; +use crate::forms; +use std::str; + + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct ProjectForm { + pub custom: forms::project::Custom +} + +impl TryFrom<&models::Project> for ProjectForm { + type Error = String; + + fn try_from(project: &models::Project) -> Result { + serde_json::from_value::(project.body.clone()).map_err(|err| format!("{:?}", err)) + } +} + + +#[derive(Serialize, Default)] +pub struct DockerImageReadResult { + pub(crate) id: String, + pub(crate) readable: bool +} + +impl ProjectForm { + pub async fn is_readable_docker_image(&self) -> Result { + for app in &self.custom.web { + if !app.app.docker_image.is_active().await? { + return Ok(DockerImageReadResult{ + id: app.app.id.clone(), + readable: false + }); + } + } + + if let Some(service) = &self.custom.service { + for app in service { + if !app.app.docker_image.is_active().await? { + return Ok(DockerImageReadResult{ + id: app.app.id.clone(), + readable: false + }); + } + } + } + + if let Some(features) = &self.custom.feature { + for app in features { + if !app.app.docker_image.is_active().await? { + return Ok(DockerImageReadResult{ + id: app.app.id.clone(), + readable: false + }); + } + } + } + Ok(DockerImageReadResult{ + id: "".to_owned(), + readable: true + }) + } +} \ No newline at end of file diff --git a/src/forms/project/icon.rs b/src/forms/project/icon.rs new file mode 100644 index 0000000..2f1c83c --- /dev/null +++ b/src/forms/project/icon.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; +use crate::forms::project::*; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Icon { + pub light: IconLight, + pub dark: IconDark, +} diff --git a/src/forms/project/icon_dark.rs b/src/forms/project/icon_dark.rs new file mode 100644 index 0000000..d488f6a --- /dev/null +++ b/src/forms/project/icon_dark.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct IconDark { + width: Option, + height: Option, + image: Option +} diff --git a/src/forms/project/icon_light.rs b/src/forms/project/icon_light.rs new file mode 100644 index 0000000..90b2c6a --- /dev/null +++ b/src/forms/project/icon_light.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct IconLight { + pub width: Option, + pub height: Option, + pub image: Option, +} diff --git a/src/forms/project/mod.rs b/src/forms/project/mod.rs new file mode 100644 index 0000000..d83fecc --- /dev/null +++ b/src/forms/project/mod.rs @@ -0,0 +1,54 @@ +mod app; +mod custom; +pub(crate) mod form; +mod port; +mod payload; +mod volumes; +mod volume; +mod role; +mod requirements; +mod docker_image; +mod domain_list; +mod var; +mod price; +mod network; +mod environment; +mod service_networks; +mod compose_networks; +mod web; +mod feature; +mod service; +mod icon; +mod icon_light; +mod icon_dark; +mod version; + +mod network_driver; +mod deploy; + +pub use app::*; +pub use custom::*; +pub use form::*; +pub use port::*; +pub use payload::*; +pub use volumes::*; +pub use volume::*; +pub use role::*; +pub use requirements::*; +pub use docker_image::*; +pub use domain_list::*; +pub use var::*; +pub use price::*; +pub use network::*; +pub use environment::*; +pub use service_networks::*; +pub use compose_networks::*; +pub use network_driver::*; +pub use web::*; +pub use feature::*; +pub use service::*; +pub use icon::*; +pub use icon_light::*; +pub use icon_dark::*; +pub use version::*; +pub use deploy::*; \ No newline at end of file diff --git a/src/forms/project/network.rs b/src/forms/project/network.rs new file mode 100644 index 0000000..2e0e183 --- /dev/null +++ b/src/forms/project/network.rs @@ -0,0 +1,60 @@ +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; +use docker_compose_types as dctypes; +use crate::forms::project::NetworkDriver; + + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Network { + pub(crate) id: String, + pub(crate) attachable: Option, + pub(crate) driver: Option, + pub(crate) driver_opts: Option, + pub(crate) enable_ipv6: Option, + pub(crate) internal: Option, + pub(crate) external: Option, + pub(crate) ipam: Option, + pub(crate) labels: Option, + pub(crate) name: String, +} + + +impl Default for Network { + fn default() -> Self { + // The case when we need at least one external network to be preconfigured + Network { + id: "default_network".to_string(), + attachable: None, + driver: None, + driver_opts: Default::default(), + enable_ipv6: None, + internal: None, + external: Some(true), + ipam: None, + labels: None, + name: "default_network".to_string(), + } + } +} + +impl Into for Network { + + fn into(self) -> dctypes::NetworkSettings { + + // default_network is always external=true + let is_default = self.name == String::from("default_network"); + let external = is_default || self.external.unwrap_or(false); + + dctypes::NetworkSettings { + attachable: self.attachable.unwrap_or(false), + driver: self.driver.clone(), + driver_opts: self.driver_opts.unwrap_or_default().into(), // @todo + enable_ipv6: self.enable_ipv6.unwrap_or(false), + internal: self.internal.unwrap_or(false), + external: Some(dctypes::ComposeNetwork::Bool(external)), + ipam: None, // @todo + labels: Default::default(), + name: Some(self.name.clone()), + } + } +} diff --git a/src/forms/project/network_driver.rs b/src/forms/project/network_driver.rs new file mode 100644 index 0000000..0b8a46a --- /dev/null +++ b/src/forms/project/network_driver.rs @@ -0,0 +1,15 @@ +use docker_compose_types::SingleValue; +use indexmap::IndexMap; +use serde_derive::{Deserialize, Serialize}; +use serde_valid::Validate; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct NetworkDriver { + // not implemented +} + +impl Into>> for NetworkDriver { + fn into(self) -> IndexMap> { + IndexMap::new() + } +} diff --git a/src/forms/project/payload.rs b/src/forms/project/payload.rs new file mode 100644 index 0000000..6a2c868 --- /dev/null +++ b/src/forms/project/payload.rs @@ -0,0 +1,38 @@ +use std::convert::TryFrom; +use crate::models; +use crate::forms; +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +#[serde(rename_all = "snake_case")] +pub struct Payload { + pub(crate) id: Option, + pub(crate) project_id: Option, + pub(crate) user_token: Option, + pub(crate) user_email: Option, + #[serde(flatten)] + pub cloud: Option, + #[serde(flatten)] + pub server: Option, + #[serde(flatten)] + pub stack: forms::project::Stack, + pub custom: forms::project::Custom, + pub docker_compose: Option>, +} + +impl TryFrom<&models::Project> for Payload { + type Error = String; + + fn try_from(project: &models::Project) -> Result { + // tracing::debug!("project body: {:?}", project.body.clone()); + let mut project_data = serde_json::from_value::(project.body.clone()) + .map_err(|err| { + format!("{:?}", err) + })?; + + project_data.project_id = Some(project.id); + + Ok(project_data) + } +} diff --git a/src/forms/project/port.rs b/src/forms/project/port.rs new file mode 100644 index 0000000..06c3020 --- /dev/null +++ b/src/forms/project/port.rs @@ -0,0 +1,85 @@ +use serde::{Deserialize, Serialize}; +use docker_compose_types as dctypes; +use regex::Regex; +use serde_valid::Validate; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Port { + #[validate(custom(|v| validate_non_empty(v)))] + pub host_port: Option, + #[validate(pattern = r"^\d{2,6}+$")] + pub container_port: String, + #[validate(enumerate("tcp", "udp"))] + pub protocol: Option, +} + +fn validate_non_empty(v: &Option) -> Result<(), serde_valid::validation::Error> { + if v.is_none() { + return Ok(()); + } + + if let Some(value) = v { + if value.is_empty() { + return Ok(()); + } + + let re = Regex::new(r"^\d{2,6}$").unwrap(); + + if !re.is_match(value.as_str()) { + return Err(serde_valid::validation::Error::Custom("Port is not valid.".to_owned())); + } + } + + Ok(()) +} + + + +// impl Default for Port{ +// fn default() -> Self { +// Port { +// target: 80, +// host_ip: None, +// published: None, +// protocol: None, +// mode: None, +// } +// } +// } + +impl TryInto for &Port { + type Error = String; + fn try_into(self) -> Result { + let cp = self.container_port + .clone() + .parse::() + .map_err(|_err| "Could not parse container port".to_string() )?; + + let hp = match self.host_port.clone() { + Some(hp) => { + if hp.is_empty() { + None + } else { + match hp.parse::() { + Ok(port) => Some(dctypes::PublishedPort::Single(port)), + Err(_) => { + tracing::debug!("Could not parse host port: {}", hp); + None + } + } + } + } + _ => None + }; + + tracing::debug!("Port conversion result: cp: {:?} hp: {:?}", cp, hp); + + Ok(dctypes::Port { + target: cp, + host_ip: None, + published: hp, + protocol: None, + mode: None, + }) + } +} diff --git a/src/forms/project/price.rs b/src/forms/project/price.rs new file mode 100644 index 0000000..06bbaee --- /dev/null +++ b/src/forms/project/price.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Price { + pub value: f64, +} diff --git a/src/forms/project/requirements.rs b/src/forms/project/requirements.rs new file mode 100644 index 0000000..402f80d --- /dev/null +++ b/src/forms/project/requirements.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Requirements { + #[validate(min_length = 1)] + #[validate(max_length = 10)] + #[validate(pattern = r"^[0-9]*\.?[0-9]+$")] + pub cpu: Option, + #[validate(min_length = 1)] + #[validate(max_length = 10)] + #[validate(pattern = r"^[0-9]*\.?[0-9]+Gb?$")] + #[serde(rename = "disk_size")] + pub disk_size: Option, + #[serde(rename = "ram_size")] + #[validate(min_length = 1)] + #[validate(max_length = 10)] + #[validate(pattern = r"^[0-9]*\.?[0-9]+Gb?$")] + pub ram_size: Option, +} diff --git a/src/forms/project/role.rs b/src/forms/project/role.rs new file mode 100644 index 0000000..5f5406a --- /dev/null +++ b/src/forms/project/role.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Role { + pub role: Option>, +} diff --git a/src/forms/project/service.rs b/src/forms/project/service.rs new file mode 100644 index 0000000..706e0be --- /dev/null +++ b/src/forms/project/service.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; +use crate::forms::project::*; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Service { + // #[serde(rename(deserialize = "sharedPorts"))] + // #[serde(rename(serialize = "shared_ports"))] + // #[serde(alias = "shared_ports")] + // pub shared_ports: Option>, + #[serde(flatten)] + pub(crate) app: App, + pub custom: Option, +} diff --git a/src/forms/project/service_networks.rs b/src/forms/project/service_networks.rs new file mode 100644 index 0000000..39f03b0 --- /dev/null +++ b/src/forms/project/service_networks.rs @@ -0,0 +1,58 @@ +use serde::{Deserialize, Serialize}; +use docker_compose_types as dctypes; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ServiceNetworks { + pub network: Option>, +} + +impl TryFrom<&ServiceNetworks> for dctypes::Networks { + type Error = (); + + fn try_from(service_networks: &ServiceNetworks) -> Result { + let nets = match service_networks.network.as_ref() { + Some(_nets) => { + _nets.clone() + } + None => { + vec![] + } + }; + Ok(dctypes::Networks::Simple(nets.into())) + } +} + +// IndexMap +// +// impl Into>> for project::ComposeNetworks { +// fn into(self) -> IndexMap> { +// +// // let mut default_networks = vec![Network::default()]; +// let mut default_networks = vec![]; +// +// let networks = match self.networks { +// None => { +// default_networks +// } +// Some(mut nets) => { +// if !nets.is_empty() { +// nets.append(&mut default_networks); +// } +// nets +// } +// }; +// +// let networks = networks +// .into_iter() +// .map(|net| { +// (net.name.clone(), MapOrEmpty::Map(net.into())) +// } +// ) +// .collect::>(); +// +// tracing::debug!("networks collected {:?}", &networks); +// +// networks +// } +// } + diff --git a/src/forms/project/var.rs b/src/forms/project/var.rs new file mode 100644 index 0000000..2072147 --- /dev/null +++ b/src/forms/project/var.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Var {} + diff --git a/src/forms/project/version.rs b/src/forms/project/version.rs new file mode 100644 index 0000000..9e7dfb3 --- /dev/null +++ b/src/forms/project/version.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Version { + #[serde(rename = "_etag")] + pub etag: Option, + #[serde(rename = "_id")] + pub id: u32, + #[serde(rename = "_created")] + pub created: Option, + #[serde(rename = "_updated")] + pub updated: Option, + pub app_id: Option, + pub name: String, + #[validate(min_length = 3)] + #[validate(max_length = 20)] + pub version: String, + #[serde(rename = "update_status")] + pub update_status: Option, + #[validate(min_length = 3)] + #[validate(max_length = 20)] + pub tag: String, +} diff --git a/src/forms/project/volume.rs b/src/forms/project/volume.rs new file mode 100644 index 0000000..2b30a59 --- /dev/null +++ b/src/forms/project/volume.rs @@ -0,0 +1,74 @@ +use serde::{Deserialize, Serialize}; +use docker_compose_types as dctypes; +use indexmap::IndexMap; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Volume { + pub host_path: Option, + pub container_path: Option, +} + +impl Volume { + pub fn is_named_docker_volume(&self) -> bool { + // Docker named volumes typically don't contain special characters or slashes + // They are alphanumeric and may include underscores or hyphens + self + .host_path + .as_ref() + .unwrap() + .chars() + .all(|c| c.is_alphanumeric() || c == '_' || c == '-') + } +} + +impl TryInto for &Volume { + type Error = String; + fn try_into(self) -> Result { + let source = self.host_path.clone(); + let target = self.container_path.clone(); + tracing::debug!( + "Volume conversion result: source: {:?} target: {:?}", + source, + target + ); + + let _type = if self.is_named_docker_volume() { + "volume" + } else { + "bind" + }; + + Ok(dctypes::AdvancedVolumes { + source: source, + target: target.unwrap_or("".to_string()), + _type: _type.to_string(), + read_only: false, + bind: None, + volume: None, + tmpfs: None, + }) + } +} + +impl Into for &Volume { + fn into(self) -> dctypes::ComposeVolume { + // let's create a symlink to /var/docker/volumes in project docroot + let mut driver_opts = IndexMap::default(); + let host_path = self.host_path.clone().unwrap_or_else(String::default); + // @todo check if host_path is required argument + driver_opts.insert(String::from("type"), Some(dctypes::SingleValue::String("none".to_string()))); + driver_opts.insert(String::from("o"), Some(dctypes::SingleValue::String("bind".to_string()))); + // @todo move to config project docroot on host + let path = format!("/root/project/{}", &host_path); + driver_opts.insert(String::from("device"), Some(dctypes::SingleValue::String(path))); + + dctypes::ComposeVolume { + driver: Some(String::from("local")), + driver_opts: driver_opts, + external: None, + labels: Default::default(), + name: Some(host_path) + } + } +} + diff --git a/src/forms/project/volumes.rs b/src/forms/project/volumes.rs new file mode 100644 index 0000000..27548a7 --- /dev/null +++ b/src/forms/project/volumes.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; +use crate::forms::project::*; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Volumes { + volumes: Vec, +} diff --git a/src/forms/project/web.rs b/src/forms/project/web.rs new file mode 100644 index 0000000..2d80cd5 --- /dev/null +++ b/src/forms/project/web.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; +use crate::forms::project::*; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Web { + #[serde(flatten)] + pub app: App, + pub custom: Option, +} diff --git a/src/forms/rating.rs b/src/forms/rating/add.rs similarity index 54% rename from src/forms/rating.rs rename to src/forms/rating/add.rs index 76efca4..a2c90d2 100644 --- a/src/forms/rating.rs +++ b/src/forms/rating/add.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use serde_valid::Validate; #[derive(Serialize, Deserialize, Debug, Validate)] -pub struct Rating { +pub struct AddRating { pub obj_id: i32, // product external id pub category: models::RateCategory, // rating of product | rating of service etc #[validate(max_length = 1000)] @@ -12,3 +12,16 @@ pub struct Rating { #[validate(maximum = 10)] pub rate: i32, // } + +impl Into for AddRating { + fn into(self) -> models::Rating { + let mut rating = models::Rating::default(); + rating.obj_id = self.obj_id; + rating.category = self.category.into(); + rating.hidden = Some(false); + rating.rate = Some(self.rate); + rating.comment = self.comment; + + rating + } +} diff --git a/src/forms/rating/adminedit.rs b/src/forms/rating/adminedit.rs new file mode 100644 index 0000000..bf6baea --- /dev/null +++ b/src/forms/rating/adminedit.rs @@ -0,0 +1,30 @@ +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; + +#[derive(Serialize, Deserialize, Debug, Validate)] +pub struct AdminEditRating { + #[validate(max_length = 1000)] + pub comment: Option, // always linked to a product + #[validate(minimum = 0)] + #[validate(maximum = 10)] + pub rate: Option, + pub hidden: Option, +} + +impl AdminEditRating { + pub fn update(self, rating: &mut models::Rating) + { + if let Some(comment) = self.comment { + rating.comment.insert(comment); + } + + if let Some(rate) = self.rate { + rating.rate.insert(rate); + } + + if let Some(hidden) = self.hidden { + rating.hidden.insert(hidden); + } + } +} diff --git a/src/forms/rating/mod.rs b/src/forms/rating/mod.rs new file mode 100644 index 0000000..af230ab --- /dev/null +++ b/src/forms/rating/mod.rs @@ -0,0 +1,7 @@ +mod add; +mod useredit; +mod adminedit; + +pub use add::AddRating as Add; +pub use useredit::UserEditRating as UserEdit; +pub use adminedit::AdminEditRating as AdminEdit; diff --git a/src/forms/rating/useredit.rs b/src/forms/rating/useredit.rs new file mode 100644 index 0000000..4f5ae02 --- /dev/null +++ b/src/forms/rating/useredit.rs @@ -0,0 +1,25 @@ +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; + +#[derive(Serialize, Deserialize, Debug, Validate)] +pub struct UserEditRating { + #[validate(max_length = 1000)] + pub comment: Option, // always linked to a product + #[validate(minimum = 0)] + #[validate(maximum = 10)] + pub rate: Option, // +} + +impl UserEditRating { + pub fn update(self, rating: &mut models::Rating) + { + if let Some(comment) = self.comment { + rating.comment.insert(comment); + } + + if let Some(rate) = self.rate { + rating.rate.insert(rate); + } + } +} diff --git a/src/forms/server.rs b/src/forms/server.rs new file mode 100644 index 0000000..134973a --- /dev/null +++ b/src/forms/server.rs @@ -0,0 +1,46 @@ +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_valid::Validate; +use chrono::{Utc}; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct ServerForm { + // pub cloud_id: i32, + // pub project_id: i32, + pub region: String, + pub zone: Option, + pub server: String, + pub os: String, + pub disk_type: Option, +} + +impl Into for &ServerForm { + fn into(self) -> models::Server { + let mut server = models::Server::default(); + server.disk_type = self.disk_type.clone(); + server.region = self.region.clone(); + server.server = self.server.clone(); + server.zone = self.zone.clone(); + server.os = self.os.clone(); + server.created_at = Utc::now(); + server.updated_at = Utc::now(); + + server + } +} + +impl Into for models::Server { + + fn into(self) -> ServerForm { + let mut form = ServerForm::default(); + // form.cloud_id = self.cloud_id; + // form.project_id = self.project_id; + form.disk_type = self.disk_type; + form.region = self.region; + form.server = self.server; + form.zone = self.zone; + form.os = self.os; + + form + } +} diff --git a/src/forms/stack.rs b/src/forms/stack.rs deleted file mode 100644 index 78a0847..0000000 --- a/src/forms/stack.rs +++ /dev/null @@ -1,271 +0,0 @@ -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)] -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, - pub zone: Option, - pub server: String, - pub os: String, - pub ssl: String, - pub vars: Option>, - #[serde(rename = "integrated_features")] - pub integrated_features: Option>, - #[serde(rename = "extended_features")] - pub extended_features: Option>, - pub subscriptions: Option>, - pub form_app: Option>, - pub disk_type: Option, - #[serde(rename = "save_token")] - pub save_token: bool, - #[serde(rename = "cloud_token")] - pub cloud_token: String, - pub provider: String, - pub stack_code: String, - #[serde(rename = "selected_plan")] - pub selected_plan: String, - pub custom: Custom, -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DomainList { -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Var { -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Price { - pub value: f64 -} -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] -#[serde(rename_all = "camelCase")] -pub struct Custom { - pub web: Vec, - pub feature: Option>, - pub service: Option>, - #[serde(rename = "servers_count")] - pub servers_count: u32, - #[serde(rename = "custom_stack_code")] - pub custom_stack_code: String, - #[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")] - pub custom_stack_short_description: Option, - #[serde(rename = "custom_stack_description")] - pub custom_stack_description: Option, - #[serde(rename = "project_name")] - pub project_name: String, - #[serde(rename = "project_overview")] - pub project_overview: Option, - #[serde(rename = "project_description")] - pub project_description: Option, -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] -pub struct App { - #[serde(rename = "_etag")] - pub etag: Option, - #[serde(rename = "_id")] - pub id: u32, - #[serde(rename = "_created")] - pub created: Option, - #[serde(rename = "_updated")] - pub updated: Option, - pub name: String, - pub code: String, - #[serde(rename = "type")] - pub type_field: String, - #[serde(flatten)] - pub role: Role, - pub default: 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, - pub avoid_render: Option, - pub price: Option, - pub icon: Option, - pub domain: Option, - pub main: bool, - pub category_id: Option, - pub parent_app_id: Option, - pub descr: Option, - pub full_description: Option, - pub description: Option, - pub plan_type: Option, - pub ansible_var: Option, - pub repo_dir: Option, - pub url_app: Option, - pub url_git: Option, -} - -#[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")] -pub struct Icon { - pub light: IconLight, - pub dark: IconDark, -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct IconLight { - pub width: i64, - pub height: i64, - pub image: String, -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct IconDark { -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Version { - #[serde(rename = "_etag")] - pub etag: Option, - #[serde(rename = "_id")] - pub id: u32, - #[serde(rename = "_created")] - pub created: Option, - #[serde(rename = "_updated")] - pub updated: Option, - #[serde(rename = "app_id")] - pub app_id: u32, - pub name: String, - pub version: String, - #[serde(rename = "update_status")] - pub update_status: Option, - pub tag: String, -} - - diff --git a/src/forms/user.rs b/src/forms/user.rs index 8ff0b4c..5cf6735 100644 --- a/src/forms/user.rs +++ b/src/forms/user.rs @@ -1,7 +1,6 @@ use serde_derive::{Serialize, Deserialize}; use serde_json::Value; use serde_valid::{Validate}; -use tracing_subscriber::fmt::format; use crate::models::user::User as UserModel; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -10,6 +9,8 @@ pub struct UserForm { pub user: User, } +//todo deref for UserForm. userForm.id, userForm.first_name + #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] #[serde(rename_all = "camelCase")] pub struct User { @@ -24,7 +25,7 @@ pub struct User { pub email: String, #[serde(rename = "email_confirmed")] pub email_confirmed: bool, - pub social: bool, + pub social: Option, pub website: Option, pub currency: Value, pub phone: Option, @@ -55,6 +56,7 @@ pub struct User { pub deployments_left: Value, #[serde(rename = "suspension_hints")] pub suspension_hints: Option, + pub role: String } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -127,16 +129,14 @@ pub struct SuspensionHints { impl TryInto for UserForm { type Error = String; fn try_into(self) -> Result { - // let id = self.id.parse::().map_err( - // |msg| { format!("{:?}", msg) } - // )?; Ok(UserModel { id: self.user.id, 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, + role: self.user.role }) } -} \ No newline at end of file +} diff --git a/src/helpers/cloud/mod.rs b/src/helpers/cloud/mod.rs new file mode 100644 index 0000000..96224c8 --- /dev/null +++ b/src/helpers/cloud/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod security; +pub use security::Secret; \ No newline at end of file diff --git a/src/helpers/cloud/security.rs b/src/helpers/cloud/security.rs new file mode 100644 index 0000000..ddb6d1b --- /dev/null +++ b/src/helpers/cloud/security.rs @@ -0,0 +1,138 @@ +use aes_gcm::{ + aead::{Aead, AeadCore, KeyInit, OsRng}, + Aes256Gcm, Nonce, Key // Or `Aes128Gcm` +}; +use base64::{engine::general_purpose, Engine as _}; +use redis::{Commands, Connection}; +use tracing::Instrument; + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Secret { + pub(crate) user_id: String, + pub(crate) provider: String, + pub(crate) field: String, // cloud_token/cloud_key/cloud_secret + pub(crate) nonce: Vec, +} + + +impl Secret { + pub fn new() -> Self { + + Secret { + user_id: "".to_string(), + provider: "".to_string(), + field: "".to_string(), + nonce: vec![], + } + } + #[tracing::instrument(name = "Secret::connect_storage")] + fn connect_storage() -> Connection { + + let storage_url = std::env::var("REDIS_URL") + .unwrap_or("redis://127.0.0.1/".to_string()); + + match redis::Client::open(storage_url){ + Ok(client) => { + match client.get_connection() { + Ok(connection) => connection, + Err(_err) => panic!("Error connecting Redis") + } + } + Err(err) => panic!("Could not connect to Redis, {:?}", err) + } + } + + #[tracing::instrument(name = "Secret::save")] + fn save(&self, value: &[u8]) -> &Self { + let mut conn = Secret::connect_storage(); + let key = format!("{}_{}_{}", self.user_id, self.provider, self.field); + tracing::debug!("Saving into storage.."); + let _: () = match conn.set(key, value) { + Ok(s) => s, + Err(e) => panic!("Could not save to storage {}", e) + }; + self + } + + pub fn b64_encode(value: &Vec) -> String { + general_purpose::STANDARD.encode(value) + } + + pub fn b64_decode(value: &String) -> Result, String> { + general_purpose::STANDARD.decode(value) + .map_err(|e| format!("b64_decode error {}", e)) + } + + #[tracing::instrument(name = "Secret::get")] + fn get(&mut self, key: String) -> &mut Self { + let mut conn = Secret::connect_storage(); + let nonce: Vec = match conn.get(&key) { + Ok(value) => { + tracing::debug!("Got value from storage {:?}", &value); + value + }, + Err(_e) => { + tracing::error!("Could not get value from storage by key {:?} {:?}", &key, _e); + vec![] + } + }; + + self.nonce = nonce; + self + } + + #[tracing::instrument(name = "encrypt.")] + pub fn encrypt(&self, token: String) -> Result, String> { + + let sec_key = std::env::var("SECURITY_KEY") + .expect("SECURITY_KEY environment variable is not set") + .clone(); + + // let key = Aes256Gcm::generate_key(OsRng); + let key: &Key:: = Key::::from_slice(&sec_key.as_bytes()); + // eprintln!("encrypt key {key:?}"); + // eprintln!("encrypt: from slice key {key:?}"); + let cipher = Aes256Gcm::new(&key); + // eprintln!("encrypt: Cipher str {cipher:?}"); + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); // 96-bits; unique per message + eprintln!("Nonce bytes {nonce:?}"); + // let nonce_b64: String = general_purpose::STANDARD.encode(nonce); + // eprintln!("Nonce b64 {nonce_b64:?}"); + eprintln!("token {token:?}"); + + let cipher_vec = cipher.encrypt(&nonce, token.as_ref()) + .map_err(|e| format!("{:?}", e))?; + + // store nonce for a limited amount of time + // self.save(cipher_vec.clone()); + self.save(nonce.as_slice()); + + eprintln!("Cipher {cipher_vec:?}"); + Ok(cipher_vec) + } + + #[tracing::instrument(name = "decrypt.")] + pub fn decrypt(&mut self, encrypted_data: Vec) -> Result { + let sec_key = std::env::var("SECURITY_KEY") + .expect("SECURITY_KEY environment variable is not set") + .clone(); + let key: &Key:: = Key::::from_slice(&sec_key.as_bytes()); + // eprintln!("decrypt: Key str {key:?}"); + let rkey = format!("{}_{}_{}", self.user_id, self.provider, self.field); + eprintln!("decrypt: Key str {rkey:?}"); + self.get(rkey); + // eprintln!("decrypt: nonce b64:decoded {nonce:?}"); + + let nonce = Nonce::from_slice(self.nonce.as_slice()); + eprintln!("decrypt: nonce {nonce:?}"); + + let cipher = Aes256Gcm::new(&key); + // eprintln!("decrypt: Cipher str {cipher:?}"); + eprintln!("decrypt: str {encrypted_data:?}"); + + let plaintext = cipher.decrypt(&nonce, encrypted_data.as_ref()) + .map_err(|e| format!("{:?}", e))?; + + Ok(String::from_utf8(plaintext).map_err(|e| format!("{:?}", e))?) + } +} \ No newline at end of file diff --git a/src/helpers/compressor.rs b/src/helpers/compressor.rs new file mode 100644 index 0000000..ec126fc --- /dev/null +++ b/src/helpers/compressor.rs @@ -0,0 +1,13 @@ +use brotli::{CompressorWriter}; +use std::io::{Write}; + +pub fn compress(input: &str) -> Vec { + let mut compressed = Vec::new(); + let mut compressor = CompressorWriter::new( + &mut compressed, 4096, 11, 22 + ); + compressor.write_all(input.as_bytes()).unwrap(); + compressor.flush().unwrap(); + drop(compressor); + compressed +} \ No newline at end of file diff --git a/src/helpers/dockerhub.rs b/src/helpers/dockerhub.rs new file mode 100644 index 0000000..5b54d9a --- /dev/null +++ b/src/helpers/dockerhub.rs @@ -0,0 +1,411 @@ +use crate::forms::project::DockerImage; +use reqwest::RequestBuilder; +use serde_derive::{Deserialize, Serialize}; +use serde_json::Value; +use serde_valid::Validate; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct DockerHubToken { + pub token: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct DockerHubCreds<'a> { + pub(crate) username: &'a str, + pub(crate) password: &'a str, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +struct Image { + architecture: String, + digest: Option, + features: Option, + last_pulled: Option, + last_pushed: Option, + os: String, + os_features: Option, + os_version: Option, + size: i64, + status: String, + variant: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +struct Tag { + pub content_type: String, + pub creator: i64, + pub digest: Option, + pub full_size: i64, + pub id: i64, + pub images: Vec, + pub last_updated: String, + pub last_updater: i64, + pub last_updater_username: String, + pub media_type: String, + pub name: String, + pub repository: i64, + pub tag_last_pulled: Option, + pub tag_last_pushed: Option, + pub tag_status: String, + pub v2: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +struct TagResult { + pub count: Option, + next: Option, + previous: Option, + results: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct RepoResults { + pub count: Option, + pub next: Option, + pub previous: Option, + pub results: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct OfficialRepoResults { + pub count: Option, + pub next: Option, + pub previous: Option, + pub results: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RepoResult { + pub name: String, + pub namespace: Option, + pub repository_type: Option, + pub status: Option, + pub status_description: Option, + pub description: Option, + pub is_private: Option, + pub star_count: Option, + pub pull_count: Option, + pub last_updated: String, + pub date_registered: Option, + pub affiliation: Option, + pub media_types: Option>, + pub content_types: Option>, +} + + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Validate)] +pub struct DockerHub<'a> { + pub(crate) creds: DockerHubCreds<'a>, + //#[validate(pattern = r"^[^:]+(:[^:]*)?$")] + #[validate(pattern = r"^([a-z-_0-9]+)(:[a-z-_0-9\.]+)?$")] + pub(crate) repos: String, + pub(crate) image: String, + pub(crate) tag: Option, +} + +impl<'a> DockerHub<'a> { + + #[tracing::instrument(name = "Dockerhub login.")] + pub async fn login(&'a self) -> Result { + if self.creds.password.is_empty() { + return Err("Password is empty".to_string()); + } + + if self.creds.username.is_empty() { + return Err("Username is empty".to_string()); + } + + let url = "https://hub.docker.com/v2/users/login"; + reqwest::Client::new() + .post(url) + .json(&self.creds) + .send() + .await + .map_err(|err| format!("{:?}", err))? + .json::() + .await + .map(|dockerHubToken| dockerHubToken.token) + .map_err(|err| format!("🟥 {:?}", err)) + } + + #[tracing::instrument(name = "Lookup public repos")] + pub async fn lookup_public_repos(&'a self) -> Result { + if !self.creds.username.is_empty() { + return Ok(false); + } + let url = format!("https://hub.docker.com/v2/repositories/{}", self.repos); + let client = reqwest::Client::new() + .get(&url) + .header("Accept", "application/json"); + + client.send() + .await + .map_err(|err| { + let msg = format!("🟥Error response {:?}", err); + tracing::debug!(msg); + msg + })? + .json::() + .await + .map_err(|err| { + let msg = format!("🟥Error on getting results:: {} url: {}", &err, &url); + tracing::error!(msg); + msg + }) + .map(|repositories| { + tracing::debug!("Get public image repo {:?} response {:?}", &url, repositories); + if repositories.count.unwrap_or(0) > 0 { + // let's find at least one active repo + let active = repositories + .results + .into_iter() + .any(|repo| { + repo.status == Some(1) + } ); + tracing::debug!("✅ Public image is active. url: {:?}", &url); + active + } else { + tracing::debug!("🟥 Public image tag is not active, url: {:?}", &url); + false + } + }) + } + + #[tracing::instrument(name = "Lookup official repos")] + pub async fn lookup_official_repos(&'a self) -> Result { + let url = format!("https://hub.docker.com/v2/repositories/library/{}/tags", self.repos); + let client = reqwest::Client::new() + .get(url) + .header("Accept", "application/json"); + + client.send() + .await + .map_err(|err| format!("🟥{}", err))? + .json::() + .await + .map_err(|err| { + tracing::debug!("🟥Error response {:?}", err); + format!("{}", err) + }) + .map(|tags| { + tracing::debug!("Validate official image response {:?}", tags); + if tags.count.unwrap_or(0) > 0 { + // let's find at least one active tag + let result = tags + .results + .into_iter() + .any(|tag| { + tracing::debug!("official: {:?}", tag); + if "active".to_string() == tag.tag_status && tag.name.eq(self.tag.as_deref().unwrap_or("latest")) { + true + } else { + false + } + }); + tracing::debug!("✅ Official mage is active. url: {:?}", result); + result + } else { + tracing::debug!("🟥 Official image tag is not active"); + false + } + }) + } + + #[tracing::instrument(name = "Lookup vendor's public repos")] + pub async fn lookup_vendor_public_repos(&'a self) -> Result { + + let url = format!( + "https://hub.docker.com/v2/namespaces/{}/repositories/{}/tags", + &self.creds.username, &self.repos + ); + + tracing::debug!("Search vendor's public repos {:?}", url); + let client = reqwest::Client::new() + .get(url) + .header("Accept", "application/json"); + + client + .send() + .await + .map_err(|err| format!("🟥{}", err))? + .json::() + .await + .map_err(|err| { + tracing::debug!("🟥Error response {:?}", err); + format!("{}", err) + }) + .map(|tags| { + tracing::debug!("Validate vendor's public image response {:?}", tags); + if tags.count.unwrap_or(0) > 0 { + // let's find at least one active tag + let t = match self.tag.clone() { + Some(s) if !s.is_empty() => s, + _ => String::from("latest"), + }; + tracing::debug!("🟥 🟥 🟥 t={:?}", t); + + let active = tags + .results + .into_iter() + .any(|tag| tag.tag_status.contains("active") && tag.name.eq(&t)); + return active; + } else { + tracing::debug!("🟥 Image tag is not active"); + false + } + }) + } + #[tracing::instrument(name = "Lookup private repos")] + pub async fn lookup_private_repo(&'a self) -> Result { + let token = self.login().await?; + + let url = format!( + "https://hub.docker.com/v2/namespaces/{}/repositories/{}/tags", + &self.creds.username, &self.repos + ); + + tracing::debug!("Search private repos {:?}", url); + let client = reqwest::Client::new() + .get(url) + .header("Accept", "application/json"); + + client.bearer_auth(token) + .send() + .await + .map_err(|err| format!("🟥{}", err))? + .json::() + .await + .map_err(|err| { + tracing::debug!("🟥Error response {:?}", err); + format!("{}", err) + }) + .map(|tags| { + tracing::debug!("Validate private image response {:?}", tags); + if tags.count.unwrap_or(0) > 0 { + // let's find at least one active tag + let t = match self.tag.clone() { + Some(s) if !s.is_empty() => s, + _ => String::from("latest") + }; + + let active = tags + .results + .into_iter() + .any(|tag| tag.tag_status.contains("active") && tag.name.eq(&t)); + return active; + } else { + tracing::debug!("🟥 Image tag is not active"); + false + } + }) + } + + pub async fn is_active(&'a self) -> Result { + // if namespace/user is not set change endpoint and return a different response + + // let n = self.repos + // .split(':') + // .map(|x| x.to_string()) + // .collect::>(); + // + // match n.len() { + // 1 => { + // self.repos = n.first().unwrap().into(); + // } + // 2 => { + // self.repos = n.first().unwrap().to_string(); + // self.tag = n.last().map(|s| s.to_string()); + // } + // _ => { + // return Err(format!("Wrong format of repository name")); + // } + // } + + tokio::select! { + Ok(true) = self.lookup_official_repos() => { + tracing::debug!("official: true"); + println!("official: true"); + return Ok(true); + } + + Ok(true) = self.lookup_public_repos() => { + tracing::debug!("public: true"); + println!("public: true"); + return Ok(true); + } + + Ok(true) = self.lookup_vendor_public_repos() => { + tracing::debug!("public: true"); + println!("public: true"); + return Ok(true); + } + + Ok(true) = self.lookup_private_repo() => { + tracing::debug!("private: true"); + println!("private: true"); + return Ok(true); + } + + else => { return Ok(false); } + } + } +} + + +impl<'a> TryFrom<&'a DockerImage> for DockerHub<'a> { + type Error = String; + + fn try_from(image: &'a DockerImage) -> Result { + + let username = match image.dockerhub_user { + Some(ref username) => username, + None => "", + }; + let password = match image.dockerhub_password { + Some(ref password) => password, + None => "", + }; + + let name = image.dockerhub_name.clone().unwrap_or("".to_string()); + let n = name + .split(':') + .map(|x| x.to_string()) + .collect::>(); + + let (name, tag) = match n.len() { + 1 => { + ( + n.first().unwrap().into(), + Some("".to_string()) + ) + } + 2 => { + ( + n.first().unwrap().to_string(), + n.last().map(|s| s.to_string()) + ) + } + _ => { + return Err("Wrong format of repository name".to_owned()); + } + }; + + let hub = DockerHub { + creds: DockerHubCreds { + username: username, + password: password, + }, + repos: name, + image: format!("{}", image), + tag: tag, + }; + + if let Err(errors) = hub.validate() { + let msg = "DockerHub image properties are not valid. Please verify repository name"; + tracing::debug!("{:?} {:?}", msg, errors); + return Err(format!("{:?}", msg)); + } + + Ok(hub) + } +} diff --git a/src/helpers/json.rs b/src/helpers/json.rs index 87e7327..ebb9df1 100644 --- a/src/helpers/json.rs +++ b/src/helpers/json.rs @@ -1,15 +1,17 @@ -use actix_web::error::{ErrorBadRequest, ErrorConflict, ErrorNotFound, ErrorInternalServerError}; -use serde_derive::Serialize; +use actix_web::error::{ErrorBadRequest, ErrorConflict, ErrorInternalServerError, ErrorNotFound, ErrorUnauthorized}; use actix_web::web::Json; use actix_web::Error; -use actix_web::Result; +use serde_derive::Serialize; #[derive(Serialize)] pub(crate) struct JsonResponse { pub(crate) message: String, + #[serde(skip_serializing_if = "Option::is_none")] pub(crate) id: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub(crate) item: Option, - pub(crate) list: Option> + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) list: Option>, } #[derive(Serialize, Default)] @@ -17,6 +19,7 @@ pub struct JsonResponseBuilder where T: serde::Serialize + Default, { + message: String, id: Option, item: Option, list: Option>, @@ -26,6 +29,11 @@ impl JsonResponseBuilder where T: serde::Serialize + Default, { + pub(crate) fn set_msg>(mut self, msg: I) -> Self { + self.message = msg.into(); + self + } + pub(crate) fn set_item(mut self, item: T) -> Self { self.item = Some(item); self @@ -41,76 +49,57 @@ where self } - fn to_json_response(self, msg: String) -> JsonResponse { + fn to_json_response(self) -> JsonResponse { JsonResponse { - message: msg, + message: self.message, id: self.id, item: self.item, list: self.list, } } - pub(crate) fn ok>(self, msg: I) -> Result>, Error> { - Ok(Json(self.to_json_response(msg.into()))) + pub(crate) fn to_string(self) -> String { + let json_response = self.to_json_response(); + serde_json::to_string(&json_response).unwrap() } - pub(crate) fn err>(self, msg: I) -> Result>, Error> { - let json_response = self.to_json_response(msg.into()); - - Err(ErrorBadRequest( - serde_json::to_string(&json_response).unwrap(), - )) + pub(crate) fn ok>(self, msg: I) -> Json> { + Json(self.set_msg(msg).to_json_response()) } - 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 bad_request>( + self, + msg: I, + ) -> Error { + ErrorBadRequest(self.set_msg(msg).to_string()) } - 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 form_error(self, msg: String) -> Error { + ErrorBadRequest(msg) } - 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 not_found>(self, msg: I) -> Error { + ErrorNotFound(self.set_msg(msg).to_string()) } - pub(crate) fn err_internal_server_error>( + pub(crate) fn internal_server_error>( self, msg: I, - ) -> Result>, Error> { - let json_response = self.to_json_response(msg.into()); - - Err(ErrorInternalServerError( - serde_json::to_string(&json_response).unwrap(), - )) + ) -> Error { + ErrorInternalServerError(self.set_msg(msg).to_string()) } + + // not used + // pub(crate) fn unauthorized>( + // self, + // msg: I, + // ) -> Error { + // ErrorUnauthorized(self.set_msg(msg).to_string()) + // } + // + // pub(crate) fn conflict>(self, msg: I) -> Error { + // ErrorConflict(self.set_msg(msg).to_string()) + // } } impl JsonResponse @@ -120,75 +109,18 @@ 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, - } +impl JsonResponse { + pub fn bad_request>(msg: I) -> Error { + JsonResponse::::build().bad_request( msg.into()) } - // 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 fn internal_server_error>(msg: I) -> Error { + JsonResponse::::build().internal_server_error( msg.into()) + } - // 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, - // } - // } + pub fn not_found>(msg: I) -> Error { + JsonResponse::::build().not_found(msg.into()) + } } diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs index e5439f7..368eafd 100644 --- a/src/helpers/mod.rs +++ b/src/helpers/mod.rs @@ -1,5 +1,14 @@ pub mod client; pub(crate) mod json; -pub(crate) mod stack; +pub mod mq_manager; +pub mod project; pub use json::*; +pub use mq_manager::*; +pub mod dockerhub; +pub(crate) mod compressor; +pub(crate) mod cloud; + +pub use dockerhub::*; + +pub use cloud::*; \ No newline at end of file diff --git a/src/helpers/mq_manager.rs b/src/helpers/mq_manager.rs new file mode 100644 index 0000000..f38604a --- /dev/null +++ b/src/helpers/mq_manager.rs @@ -0,0 +1,158 @@ +use deadpool_lapin::{Config, CreatePoolError, Object, Pool, Runtime}; +use lapin::{options::*, publisher_confirm::{Confirmation, PublisherConfirm}, BasicProperties, Channel, ExchangeKind}; +use lapin::types::{AMQPValue, FieldTable}; +use serde::ser::Serialize; + +#[derive(Debug)] +pub struct MqManager { + pool: Pool, +} + +impl MqManager { + pub fn try_new(url: String) -> Result { + let mut cfg = Config::default(); + cfg.url = Some(url); + let pool = cfg.create_pool(Some(Runtime::Tokio1)).map_err(|err| { + tracing::error!("{:?}", err); + + match err { + CreatePoolError::Config(_) => { + std::io::Error::new(std::io::ErrorKind::Other, "config error") + } + CreatePoolError::Build(_) => { + std::io::Error::new(std::io::ErrorKind::Other, "build error") + } + } + })?; + + Ok(Self { pool }) + } + + async fn get_connection(&self) -> Result { + self.pool.get().await.map_err(|err| { + let msg = format!("getting connection from pool {:?}", err); + tracing::error!(msg); + msg + }) + } + + async fn create_channel(&self) -> Result { + self.get_connection() + .await? + .create_channel() + .await + .map_err(|err| { + let msg = format!("creating RabbitMQ channel {:?}", err); + tracing::error!(msg); + msg + }) + } + + pub async fn publish( + &self, + exchange: String, + routing_key: String, + msg: &T, + ) -> Result { + let payload = serde_json::to_string::(msg).map_err(|err| { + format!("{:?}", err) + })?; + + self.create_channel() + .await? + .basic_publish( + exchange.as_str(), + routing_key.as_str(), + BasicPublishOptions::default(), + payload.as_bytes(), + BasicProperties::default(), + ) + .await + .map_err(|err| { + tracing::error!("publishing message {:?}", err); + format!("publishing message {:?}", err) + }) + } + + pub async fn publish_and_confirm( + &self, + exchange: String, + routing_key: String, + msg: &T + ) -> Result<(), String> { + self.publish(exchange, routing_key, msg) + .await? + .await + .map_err(|err| { + let msg = format!("confirming the publication {:?}", err); + tracing::error!(msg); + msg + + }) + .and_then(|confirm| match confirm { + Confirmation::NotRequested => { + let msg = format!("confirmation is NotRequested"); + tracing::error!(msg); + Err(msg) + } + _ => Ok(()), + }) + } + + pub async fn consume( + &self, + exchange_name: &str, + queue_name: &str, + routing_key: &str, + ) -> Result { + + let channel = self.create_channel().await?; + + channel + .exchange_declare( + exchange_name, + ExchangeKind::Topic, + ExchangeDeclareOptions { + passive: false, + durable: true, + auto_delete: false, + internal: false, + nowait: false, + }, + FieldTable::default() + ) + .await + .expect("Exchange declare failed"); + + let mut args = FieldTable::default(); + args.insert("x-expires".into(), AMQPValue::LongUInt(3600000)); + + let _queue = channel.queue_declare( + queue_name, + QueueDeclareOptions { + passive: false, + durable: false, + exclusive: false, + auto_delete: true, + nowait: false, + }, + args, + ) + .await + .expect("Queue declare failed"); + + let _ = channel + .queue_bind( + queue_name, + exchange_name, + routing_key, + QueueBindOptions::default(), + FieldTable::default(), + ) + .await + .map_err(|err| format!("error {:?}", err)); + + let channel = self.create_channel().await?; + Ok(channel) + } +} diff --git a/src/helpers/project/builder.rs b/src/helpers/project/builder.rs new file mode 100644 index 0000000..9c2a33a --- /dev/null +++ b/src/helpers/project/builder.rs @@ -0,0 +1,58 @@ +use crate::forms; +use docker_compose_types as dctypes; +use crate::models; +use serde_yaml; +// use crate::helpers::project::*; + + +/// A builder for constructing docker compose. +#[derive(Clone, Debug)] +pub struct DcBuilder { + // config: Config, + pub(crate) project: models::Project, +} + + +impl DcBuilder { + pub fn new(project: models::Project) -> Self { + DcBuilder { + // config: Config::default(), + project, + } + } + + #[tracing::instrument(name = "building project")] + pub fn build(&self) -> Result { + let mut compose_content = dctypes::Compose { + version: Some("3.8".to_string()), + ..Default::default() + }; + + let apps = forms::project::ProjectForm::try_from(&self.project)?; + tracing::debug!("apps {:?}", &apps); + let services = apps.custom.services()?; + tracing::debug!("services {:?}", &services); + let named_volumes = apps.custom.named_volumes()?; + + tracing::debug!("named volumes {:?}", &named_volumes); + // let all_networks = &apps.custom.networks.networks.clone().unwrap_or(vec![]); + let networks = apps.custom.networks.clone(); + compose_content.networks = dctypes::ComposeNetworks(networks.into()); + + if !named_volumes.is_empty() { + compose_content.volumes = dctypes::TopLevelVolumes(named_volumes); + } + + compose_content.services = dctypes::Services(services); + + let fname = format!("./files/{}.yml", self.project.stack_id); + tracing::debug!("Saving docker compose to file {:?}", fname); + let target_file = std::path::Path::new(fname.as_str()); + let serialized = serde_yaml::to_string(&compose_content) + .map_err(|err| format!("Failed to serialize docker-compose file: {}", err))?; + + std::fs::write(target_file, serialized.clone()).map_err(|err| format!("{}", err))?; + + Ok(serialized) + } +} diff --git a/src/helpers/project/builder_config.rs b/src/helpers/project/builder_config.rs new file mode 100644 index 0000000..7f50a24 --- /dev/null +++ b/src/helpers/project/builder_config.rs @@ -0,0 +1,9 @@ +#[derive(Clone, Debug)] +pub struct Config {} + +impl Default for Config { + fn default() -> Self { + Config {} + } +} + diff --git a/src/helpers/project/mod.rs b/src/helpers/project/mod.rs new file mode 100644 index 0000000..72ce537 --- /dev/null +++ b/src/helpers/project/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod builder; +mod builder_config; + +pub use builder_config::*; diff --git a/src/helpers/stack/builder.rs b/src/helpers/stack/builder.rs deleted file mode 100644 index c319d8a..0000000 --- a/src/helpers/stack/builder.rs +++ /dev/null @@ -1,167 +0,0 @@ -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 deleted file mode 100644 index 931deab..0000000 --- a/src/helpers/stack/dctypes.rs +++ /dev/null @@ -1,842 +0,0 @@ -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 deleted file mode 100644 index 7da2d2e..0000000 --- a/src/helpers/stack/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod builder; -pub(crate) mod dctypes; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 56bd1f0..7885288 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,11 @@ pub mod configuration; +pub mod console; +pub mod db; pub mod forms; pub mod helpers; mod middleware; pub mod models; +pub mod views; pub mod routes; pub mod services; pub mod startup; diff --git a/src/main.rs b/src/main.rs index ebf4987..8132f58 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ async fn main() -> std::io::Result<()> { let settings = get_configuration().expect("Failed to read configuration."); - let db_pool = PgPool::connect(&settings.database.connection_string()) + let pg_pool = PgPool::connect(&settings.database.connection_string()) .await .expect("Failed to connect to database."); @@ -20,5 +20,5 @@ async fn main() -> std::io::Result<()> { let listener = TcpListener::bind(address).expect(&format!("failed to bind to {}", settings.app_port)); - run(listener, db_pool, settings).await?.await + run(listener, pg_pool, settings).await?.await } diff --git a/src/middleware/authentication/getheader.rs b/src/middleware/authentication/getheader.rs new file mode 100644 index 0000000..d810400 --- /dev/null +++ b/src/middleware/authentication/getheader.rs @@ -0,0 +1,24 @@ +use actix_web::{ http::header::HeaderName, dev::ServiceRequest}; +use std::str::FromStr; + +pub fn get_header(req: &ServiceRequest, header_name: &'static str) -> Result, String> +where + T: FromStr, +{ + let header_value = req + .headers() + .get(HeaderName::from_static(header_name)); + + if header_value.is_none() { + return Ok(None); + } + + header_value + .unwrap() + .to_str() + .map_err(|_| format!("header {header_name} can't be converted to string"))? + .parse::() + .map_err(|_| format!("header {header_name} has wrong type")) + .map(|v| Some(v)) +} + diff --git a/src/middleware/authentication/manager.rs b/src/middleware/authentication/manager.rs new file mode 100644 index 0000000..2b8e09d --- /dev/null +++ b/src/middleware/authentication/manager.rs @@ -0,0 +1,37 @@ +use crate::middleware::authentication::*; + +use std::sync::Arc; +use std::future::{ready, Ready}; +use futures::lock::Mutex; + +use actix_web::{ + Error, + dev::{Service, ServiceRequest, ServiceResponse, Transform}, +}; + +pub struct Manager {} + +impl Manager { + pub fn new() -> Self { + Self {} + } +} + +impl Transform for Manager +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = ManagerMiddleware; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(ManagerMiddleware { + service: Arc::new(Mutex::new(service)), + })) + } +} diff --git a/src/middleware/authentication/manager_middleware.rs b/src/middleware/authentication/manager_middleware.rs new file mode 100644 index 0000000..7b9dc6b --- /dev/null +++ b/src/middleware/authentication/manager_middleware.rs @@ -0,0 +1,51 @@ +use crate::middleware::authentication::*; +use actix_web::{error::ErrorBadRequest, HttpMessage, Error, dev::{ServiceRequest, ServiceResponse, Service}}; +use crate::helpers::JsonResponse; +use futures::{task::{Poll, Context}, future::{FutureExt, LocalBoxFuture}, lock::Mutex}; +use crate::models; +use std::sync::Arc; + +pub struct ManagerMiddleware { + pub service: Arc>, +} + +impl Service for ManagerMiddleware +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = S::Error; + type Future = LocalBoxFuture<'static, Result, Error>>; + + fn poll_ready(&self, ctx: &mut Context<'_>) -> Poll> { + self.service + .try_lock() + .expect("Authentication ManagerMiddleware was called allready") + .poll_ready(ctx) + } + + fn call(&self, mut req: ServiceRequest) -> Self::Future { + let service = self.service.clone(); + async move { + let _ = method::try_oauth(&mut req).await? + || method::try_hmac(&mut req).await? + || method::anonym(&mut req)?; + + Ok(req) + } + .then(|req: Result| async move { + match req { + Ok(req) => { + let service = service.lock().await; + service.call(req).await + } + Err(msg) => Err(ErrorBadRequest( + JsonResponse::::build().set_msg(msg).to_string(), + )), + } + }) + .boxed_local() + } +} diff --git a/src/middleware/authentication/method/f_anonym.rs b/src/middleware/authentication/method/f_anonym.rs new file mode 100644 index 0000000..fa7c288 --- /dev/null +++ b/src/middleware/authentication/method/f_anonym.rs @@ -0,0 +1,15 @@ +use actix_web::dev::ServiceRequest; +use actix_web::HttpMessage; + +#[tracing::instrument(name = "authenticate as anonym")] +pub fn anonym(req: &mut ServiceRequest) -> Result { + let accesscontrol_vals = actix_casbin_auth::CasbinVals { + subject: "anonym".to_string(), + domain: None, + }; + if req.extensions_mut().insert(accesscontrol_vals).is_some() { + return Err("sth wrong with access control".to_string()); + } + + Ok(true) +} diff --git a/src/middleware/authentication/method/f_hmac.rs b/src/middleware/authentication/method/f_hmac.rs new file mode 100644 index 0000000..e385f8f --- /dev/null +++ b/src/middleware/authentication/method/f_hmac.rs @@ -0,0 +1,102 @@ +use hmac::{Hmac, Mac}; +use sha2::Sha256; +use sqlx::{Pool, Postgres}; +use tracing::Instrument; +use std::sync::Arc; +use crate::models; +use actix_web::{web, dev::ServiceRequest, HttpMessage}; +use crate::middleware::authentication::get_header; //todo move to helpers +use actix_http::header::CONTENT_LENGTH; +use futures::StreamExt; + +async fn db_fetch_client(db_pool: &Pool, client_id: i32) -> Result { //todo + let query_span = tracing::info_span!("Fetching the client by ID"); + + sqlx::query_as!( + models::Client, + r#"SELECT id, user_id, secret FROM client c WHERE c.id = $1"#, + client_id, + ) + .fetch_one(db_pool) + .instrument(query_span) + .await + .map_err(|err| { + match err { + sqlx::Error::RowNotFound => "the client is not found".to_string(), + e => { + tracing::error!("Failed to execute fetch query: {:?}", e); + String::new() + } + } + }) +} + +async fn compute_body_hash(req: &mut ServiceRequest, client_secret: &[u8]) -> Result { + let content_length: usize = get_header(req, CONTENT_LENGTH.as_str())?.unwrap(); + let mut body = web::BytesMut::with_capacity(content_length); + let mut payload = req.take_payload(); + while let Some(chunk) = payload.next().await { + body.extend_from_slice(&chunk.expect("can't unwrap the chunk")); + } + + let mut mac = + match Hmac::::new_from_slice(client_secret) { + Ok(mac) => mac, + Err(err) => { + tracing::error!("error generating hmac {err:?}"); + return Err("".to_string()); + } + }; + + mac.update(body.as_ref()); + let (_, mut payload) = actix_http::h1::Payload::create(true); + payload.unread_data(body.into()); + req.set_payload(payload.into()); + + Ok(format!("{:x}", mac.finalize().into_bytes())) +} + +#[tracing::instrument(name = "try authenticate via hmac")] +pub async fn try_hmac(req: &mut ServiceRequest) -> Result { + let client_id = get_header::(&req, "stacker-id")?; + if client_id.is_none() { + return Ok(false); + } + let client_id = client_id.unwrap(); + + let header_hash = get_header::(&req, "stacker-hash")?; + if header_hash.is_none() { + return Err("stacker-hash header is not set".to_string()); + } //todo + let header_hash = header_hash.unwrap(); + + let db_pool = req.app_data::>>().unwrap().get_ref(); + let client: models::Client = db_fetch_client(db_pool, client_id).await?; + if client.secret.is_none() { + return Err("client is not active".to_string()); + } + + let client_secret = client.secret.as_ref().unwrap().as_bytes(); + let body_hash = compute_body_hash(req, client_secret).await?; + if header_hash != body_hash { + return Err("hash is wrong".to_string()); + } + + match req.extensions_mut().insert(Arc::new(client)) { + Some(_) => { + tracing::error!("client middleware already called once"); + return Err("".to_string()); + } + None => {} + } + + let accesscontrol_vals = actix_casbin_auth::CasbinVals { + subject: client_id.to_string(), + domain: None, + }; + if req.extensions_mut().insert(accesscontrol_vals).is_some() { + return Err("sth wrong with access control".to_string()); + } + + Ok(true) +} diff --git a/src/middleware/authentication/method/f_oauth.rs b/src/middleware/authentication/method/f_oauth.rs new file mode 100644 index 0000000..1b861a7 --- /dev/null +++ b/src/middleware/authentication/method/f_oauth.rs @@ -0,0 +1,75 @@ +use crate::middleware::authentication::get_header; +use actix_web::{web, dev::{ServiceRequest}, HttpMessage}; +use crate::configuration::Settings; +use crate::models; +use crate::forms; +use reqwest::header::{ACCEPT, CONTENT_TYPE}; +use std::sync::Arc; + +fn try_extract_token(authentication: String) -> Result { + let mut authentication_parts = authentication.splitn(2, ' '); + match authentication_parts.next() { + Some("Bearer") => {} + _ => return Err("Bearer missing scheme".to_string()) + } + let token = authentication_parts.next(); + if token.is_none() { + tracing::error!("Bearer token is missing"); + return Err("Authentication required".to_string()); + } + + Ok(token.unwrap().into()) +} + +#[tracing::instrument(name = "Authenticate with bearer token")] +pub async fn try_oauth(req: &mut ServiceRequest) -> Result { + let authentication = get_header::(&req, "authorization")?; + if authentication.is_none() { + return Ok(false); + } + + let token = try_extract_token(authentication.unwrap())?; + let settings = req.app_data::>().unwrap(); + let user = fetch_user(settings.auth_url.as_str(), &token) + .await + .map_err(|err| format!("{err}"))?; + + // control access using user role + tracing::debug!("ACL check for role: {}", user.role.clone()); + let acl_vals = actix_casbin_auth::CasbinVals { + subject: user.role.clone(), + domain: None, + }; + + if req.extensions_mut().insert(Arc::new(user)).is_some() { + return Err("user already logged".to_string()); + } + + if req.extensions_mut().insert(acl_vals).is_some() { + return Err("Something wrong with access control".to_string()); + } + + Ok(true) +} + +async fn fetch_user(auth_url: &str, token: &str) -> Result { + let client = reqwest::Client::new(); + let resp = client + .get(auth_url) + .bearer_auth(token) + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .send() + .await + .map_err(|_err| "No response from OAuth server".to_string())?; + + if !resp.status().is_success() { + return Err("401 Unauthorized".to_string()); + } + + resp + .json::() + .await + .map_err(|_err| "can't parse the response body".to_string())? + .try_into() +} diff --git a/src/middleware/authentication/method/mod.rs b/src/middleware/authentication/method/mod.rs new file mode 100644 index 0000000..3d55881 --- /dev/null +++ b/src/middleware/authentication/method/mod.rs @@ -0,0 +1,7 @@ +mod f_oauth; +mod f_anonym; +mod f_hmac; + +pub use f_oauth::try_oauth; +pub use f_anonym::anonym; +pub use f_hmac::try_hmac; diff --git a/src/middleware/authentication/mod.rs b/src/middleware/authentication/mod.rs new file mode 100644 index 0000000..5338d6d --- /dev/null +++ b/src/middleware/authentication/mod.rs @@ -0,0 +1,8 @@ +mod getheader; +mod manager; +mod manager_middleware; +mod method; + +pub use getheader::*; +pub use manager::*; +pub use manager_middleware::*; diff --git a/src/middleware/authorization.rs b/src/middleware/authorization.rs new file mode 100644 index 0000000..f251e9d --- /dev/null +++ b/src/middleware/authorization.rs @@ -0,0 +1,32 @@ +use actix_casbin_auth::{ + CasbinService, + casbin::{ + DefaultModel, + CoreApi, + function_map::key_match2 + } +}; +use std::io::{Error, ErrorKind}; +use sqlx_adapter::SqlxAdapter; + +pub async fn try_new(db_connection_address: String) -> Result { + let m = DefaultModel::from_file("access_control.conf") + .await + .map_err(|err| Error::new(ErrorKind::Other, format!("{err:?}")))?; + let a = SqlxAdapter::new(db_connection_address, 8) + .await + .map_err(|err| Error::new(ErrorKind::Other, format!("{err:?}")))?; + + let casbin_service = CasbinService::new(m, a) + .await + .map_err(|err| Error::new(ErrorKind::Other, format!("{err:?}")))?; + + casbin_service + .write() + .await + .get_role_manager() + .write() + .matching_fn(Some(key_match2), None); + + Ok(casbin_service) +} diff --git a/src/middleware/client.rs b/src/middleware/client.rs deleted file mode 100644 index dc76c44..0000000 --- a/src/middleware/client.rs +++ /dev/null @@ -1,176 +0,0 @@ -use crate::models::Client; -use actix_http::header::CONTENT_LENGTH; -use actix_web::error::{ErrorForbidden, ErrorInternalServerError, ErrorNotFound, PayloadError}; -use actix_web::web::BytesMut; -use actix_web::HttpMessage; -use futures::future::{FutureExt, LocalBoxFuture}; -use futures::lock::Mutex; -use futures::task::{Context, Poll}; -use futures::StreamExt; -use hmac::{Hmac, Mac}; -use sha2::Sha256; -use std::future::{ready, Ready}; -use std::str::FromStr; -use std::sync::Arc; -use tracing::Instrument; - -use actix_web::{ - dev::{Service, ServiceRequest, ServiceResponse, Transform}, - error::ErrorBadRequest, - http::header::HeaderName, - web, Error, -}; -use sqlx::{Pool, Postgres}; - -pub struct Guard {} - -impl Guard { - pub fn new() -> Self { - Self {} - } -} - -impl Transform for Guard -where - S: Service, Error = Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type InitError = (); - type Transform = GuardMiddleware; - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - ready(Ok(GuardMiddleware { - service: Arc::new(Mutex::new(service)), - })) - } -} - -pub struct GuardMiddleware { - service: Arc>, -} - -impl Service for GuardMiddleware -where - S: Service, Error = Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = S::Error; - type Future = LocalBoxFuture<'static, Result, Error>>; - - fn poll_ready(&self, ctx: &mut Context<'_>) -> Poll> { - self.service - .try_lock() - .expect("GuardMiddleware was called allready") - .poll_ready(ctx) - } - - fn call(&self, mut req: ServiceRequest) -> Self::Future { - let service = self.service.clone(); - async move { - let client_id: i32 = get_header(&req, "stacker-id").map_err(|m| ErrorBadRequest(m))?; - let hash: String = get_header(&req, "stacker-hash").map_err(|m| ErrorBadRequest(m))?; - - let query_span = tracing::info_span!("Fetching the client by ID"); - let db_pool = req.app_data::>>().unwrap(); - - let mut client: Client = match sqlx::query_as!( - Client, - r#" - SELECT - id, user_id, secret - FROM client c - WHERE c.id = $1 - "#, - client_id, - ) - .fetch_one(db_pool.get_ref()) - .instrument(query_span) - .await - { - Ok(client) if client.secret.is_some() => client, - Ok(_client) => { - return Err(ErrorForbidden("client is not active")); - } - Err(sqlx::Error::RowNotFound) => { - return Err(ErrorNotFound("the client is not found")); - } - Err(e) => { - tracing::error!("Failed to execute fetch query: {:?}", e); - - return Err(ErrorInternalServerError("")); - } - }; - - let content_length: usize = - get_header(&req, CONTENT_LENGTH.as_str()).map_err(|m| ErrorBadRequest(m))?; - let body = req - .take_payload() - .fold( - BytesMut::with_capacity(content_length), - |mut body, chunk| { - let chunk = chunk.unwrap(); //todo process the potential error of unwrap - body.extend_from_slice(&chunk); //todo - - ready(body) - }, - ) - .await; - - let mut mac = - match Hmac::::new_from_slice(client.secret.as_ref().unwrap().as_bytes()) { - Ok(mac) => mac, - Err(err) => { - tracing::error!("error generating hmac {err:?}"); - - return Err(ErrorInternalServerError("")); - } - }; - - mac.update(body.as_ref()); - let computed_hash = format!("{:x}", mac.finalize().into_bytes()); - if hash != computed_hash { - return Err(ErrorBadRequest("hash is wrong")); - } - - let (_, mut payload) = actix_http::h1::Payload::create(true); - payload.unread_data(body.into()); - req.set_payload(payload.into()); - - match req.extensions_mut().insert(Arc::new(client)) { - Some(_) => { - tracing::error!("client middleware already called once"); - return Err(ErrorInternalServerError("")); - } - None => {} - } - - let service = service.lock().await; - service.call(req).await - } - .boxed_local() - } -} - -fn get_header(req: &ServiceRequest, header_name: &'static str) -> Result -where - T: FromStr, -{ - let header_value = req - .headers() - .get(HeaderName::from_static(header_name)) - .ok_or(format!("header {header_name} not found"))?; - - let header_value: &str = header_value - .to_str() - .map_err(|_| format!("header {header_name} can't be converted to string"))?; //map_err - // - header_value - .parse::() - .map_err(|_| format!("header {header_name} has wrong type")) -} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 20dda30..6d34250 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,2 +1,2 @@ -pub mod client; -pub mod trydirect; +pub mod authentication; +pub mod authorization; diff --git a/src/middleware/trydirect.rs b/src/middleware/trydirect.rs deleted file mode 100644 index 1d01c50..0000000 --- a/src/middleware/trydirect.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::configuration::Settings; -use crate::forms::user::UserForm; -use actix_web::dev::ServiceRequest; -use actix_web::error::{ErrorInternalServerError, ErrorUnauthorized}; -use actix_web::web::{self}; -use actix_web::Error; -use actix_web::HttpMessage; -use actix_web_httpauth::extractors::bearer::BearerAuth; -use reqwest::header::{ACCEPT, CONTENT_TYPE}; -use std::sync::Arc; - -use crate::models::user::User; - -#[tracing::instrument(name = "Trydirect bearer guard.")] -pub async fn bearer_guard( - req: ServiceRequest, - credentials: BearerAuth, -) -> Result { - let settings = req.app_data::>>().unwrap(); - let client = reqwest::Client::new(); - let resp = client - .get(&settings.auth_url) - .bearer_auth(credentials.token()) - .header(CONTENT_TYPE, "application/json") - .header(ACCEPT, "application/json") - .send() - .await; - - let resp = match resp { - Ok(resp) if resp.status().is_success() => resp, - Ok(resp) => { - tracing::error!("Authentication service returned no success {:?}", resp); - return Err((ErrorUnauthorized("401 Unauthorized"), req)); - } - Err(err) => { - tracing::error!("error from reqwest {:?}", err); - return Err((ErrorInternalServerError(err.to_string()), req)); - } - }; - - let user_form: UserForm = match resp.json().await { - Ok(user) => { - tracing::info!("unpacked user {user:?}"); - user - } - Err(err) => { - tracing::error!("can't parse the response body {:?}", err); - return Err((ErrorUnauthorized(""), req)); - } - }; - - let user: User = match user_form.try_into() { - Ok(user) => user, - Err(err) => { - tracing::error!("Could not create User from form data: {:?}", err); - return Err((ErrorUnauthorized("Unauthorized"), req)); - } - }; - let existent_user = req.extensions_mut().insert(user); - if existent_user.is_some() { - tracing::error!("already logged {existent_user:?}"); - return Err((ErrorInternalServerError(""), req)); - } - - Ok(req) -} diff --git a/src/models/client.rs b/src/models/client.rs index afef0c1..d881597 100644 --- a/src/models/client.rs +++ b/src/models/client.rs @@ -9,10 +9,15 @@ pub struct Client { impl std::fmt::Debug for Client { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let secret: String = match self.secret.as_ref() { + Some(val) => val.chars().take(4).collect::() + "****", + None => "".to_string(), + }; + write!( f, - "Client {{id: {:?}, user_id: {:?}}}", - self.id, self.user_id + "Client {{id: {:?}, user_id: {:?}, secret: {}}}", + self.id, self.user_id, secret ) } } diff --git a/src/models/cloud.rs b/src/models/cloud.rs new file mode 100644 index 0000000..b7c8f63 --- /dev/null +++ b/src/models/cloud.rs @@ -0,0 +1,75 @@ +use chrono::{DateTime, Utc}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Cloud { + pub id: i32, + pub user_id: String, + pub provider: String, + pub cloud_token: Option, + pub cloud_key: Option, + pub cloud_secret: Option, + pub save_token: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +fn mask_string(s: Option<&String>) -> String { + match s { + Some(val) => val.chars().take(4).collect::() + "****", + None => "".to_string(), + } +} + +impl std::fmt::Display for Cloud { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let cloud_key = mask_string(self.cloud_key.as_ref()); + let cloud_token = mask_string(self.cloud_token.as_ref()); + let cloud_secret = mask_string(self.cloud_secret.as_ref()); + + write!(f, "{} cloud creds: cloud_key : {} cloud_token: {} cloud_secret: {}", + self.provider, + cloud_key, + cloud_token, + cloud_secret, + ) + } +} + +impl Cloud { + pub fn new(user_id: String, + provider: String, + cloud_token: Option, + cloud_key: Option, + cloud_secret: Option, + save_token: Option + ) -> Self { + Self { + id: 0, + user_id, + provider, + cloud_token, + cloud_key, + cloud_secret, + save_token, + created_at: Utc::now(), + updated_at: Utc::now(), + } + } +} + +impl Default for Cloud { + fn default() -> Self { + Cloud { + id: 0, + provider: "".to_string(), + user_id: "".to_string(), + cloud_key: Default::default(), + cloud_token: Default::default(), + cloud_secret: Default::default(), + save_token: Some(false), + created_at: Default::default(), + updated_at: Default::default(), + } + } +} diff --git a/src/models/deployment.rs b/src/models/deployment.rs new file mode 100644 index 0000000..b819ef2 --- /dev/null +++ b/src/models/deployment.rs @@ -0,0 +1,43 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +// Store user deployment attempts for a specific project +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Deployment { + pub id: i32, // id - is a unique identifier for the app project + pub project_id: i32, // external project ID + pub deleted: Option, + pub status: String, + pub body: Value, //json type + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl Deployment { + pub fn new(project_id: i32, status: String, body: Value) -> Self { + Self { + id: 0, + project_id, + deleted: Some(false), + status, + body, + created_at: Utc::now(), + updated_at: Utc::now(), + } + } +} + +impl Default for Deployment { + fn default() -> Self { + Deployment { + id: 0, + project_id: 0, + deleted: None, + status: "pending".to_string(), + body: Default::default(), + created_at: Default::default(), + updated_at: Default::default(), + } + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 32ecf55..c1c375b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,8 +1,21 @@ mod client; +mod product; +mod ratecategory; +mod rules; pub mod rating; -pub mod stack; +pub mod project; pub mod user; +pub(crate) mod deployment; +mod cloud; +mod server; pub use client::*; pub use rating::*; -pub use stack::*; +pub use project::*; +pub use user::*; +pub use product::*; +pub use ratecategory::*; +pub use rules::*; +pub use deployment::*; +pub use cloud::*; +pub use server::*; diff --git a/src/models/product.rs b/src/models/product.rs new file mode 100644 index 0000000..8fde4f3 --- /dev/null +++ b/src/models/product.rs @@ -0,0 +1,16 @@ +use chrono::{DateTime, Utc}; + +pub struct Product { + // Product - is an external object that we want to store in the database, + // that can be a project or an app in the project. feature, service, web app etc. + // id - is a unique identifier for the product + // user_id - is a unique identifier for the user + // rating - is a rating of the product + // product type project & app, + // id is generated based on the product type and external obj_id + pub id: i32, //primary key, for better data management + pub obj_id: i32, // external product ID db, no autoincrement, example: 100 + pub obj_type: String, // project | app, unique index + pub created_at: DateTime, + pub updated_at: DateTime, +} diff --git a/src/models/project.rs b/src/models/project.rs new file mode 100644 index 0000000..29b260b --- /dev/null +++ b/src/models/project.rs @@ -0,0 +1,47 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Project { + pub id: i32, // id - is a unique identifier for the app project + pub stack_id: Uuid, // external project ID + pub user_id: String, // external unique identifier for the user + pub name: String, + // pub body: sqlx::types::Json, + pub body: Value, //json type + pub request_json: Value, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl Project { + pub fn new(user_id: String, name: String, body: Value, request_json: Value) -> Self { + Self { + id: 0, + stack_id: Uuid::new_v4(), + user_id, + name, + body, + request_json, + created_at: Utc::now(), + updated_at: Utc::now(), + } + } +} + +impl Default for Project { + fn default() -> Self { + Project { + id: 0, + stack_id: Default::default(), + user_id: "".to_string(), + name: "".to_string(), + body: Default::default(), + request_json: Default::default(), + created_at: Default::default(), + updated_at: Default::default(), + } + } +} diff --git a/src/models/ratecategory.rs b/src/models/ratecategory.rs new file mode 100644 index 0000000..352bedb --- /dev/null +++ b/src/models/ratecategory.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +#[derive(sqlx::Type, Serialize, Deserialize, Debug, Clone, Copy)] +#[sqlx(rename_all = "lowercase", type_name = "rate_category")] +pub enum RateCategory { + Application, // app, feature, extension + Cloud, // is user satisfied working with this cloud + Project, // app project + DeploymentSpeed, + Documentation, + Design, + TechSupport, + Price, + MemoryUsage, +} + +impl Into for RateCategory { + fn into(self) -> String { + format!("{:?}", self) + } +} + +impl Default for RateCategory { + fn default() -> Self { + RateCategory::Application + } +} diff --git a/src/models/rating.rs b/src/models/rating.rs index c9ba705..d6f1eda 100644 --- a/src/models/rating.rs +++ b/src/models/rating.rs @@ -1,58 +1,16 @@ use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; +use serde::{Serialize}; +use crate::models; -pub struct Product { - // Product - is an external object that we want to store in the database, - // that can be a stack or an app in the stack. feature, service, web app etc. - // id - is a unique identifier for the product - // user_id - is a unique identifier for the user - // rating - is a rating of the product - // product type stack & app, - // id is generated based on the product type and external obj_id - pub id: i32, //primary key, for better data management - pub obj_id: i32, // external product ID db, no autoincrement, example: 100 - pub obj_type: String, // stack | app, unique index - pub created_at: DateTime, - pub updated_at: DateTime, -} - -#[derive(Debug, Serialize, Default)] +#[derive(Debug, Default)] pub struct Rating { pub id: i32, pub user_id: String, // external user_id, 100, taken using token (middleware?) pub obj_id: i32, // id of the external object - pub category: String, // rating of product | rating of service etc + pub category: models::RateCategory, // rating of product | rating of service etc pub comment: Option, // always linked to a product pub hidden: Option, // rating can be hidden for non-adequate user behaviour pub rate: Option, pub created_at: DateTime, pub updated_at: DateTime, } - - - -#[derive(sqlx::Type, Serialize, Deserialize, Debug, Clone, Copy)] -#[sqlx(rename_all = "lowercase", type_name = "varchar")] -pub enum RateCategory { - Application, // app, feature, extension - Cloud, // is user satisfied working with this cloud - Stack, // app stack - DeploymentSpeed, - Documentation, - Design, - TechSupport, - Price, - MemoryUsage, -} - -impl Into for RateCategory { - fn into(self) -> String { - format!("{:?}", self) - } -} - -pub struct Rules { - //-> Product.id - // example: allow to add only a single comment - comments_per_user: i32, // default = 1 -} diff --git a/src/models/rules.rs b/src/models/rules.rs new file mode 100644 index 0000000..58afbd9 --- /dev/null +++ b/src/models/rules.rs @@ -0,0 +1,5 @@ +pub struct Rules { + //-> Product.id + // example: allow to add only a single comment + comments_per_user: i32, // default = 1 +} diff --git a/src/models/server.rs b/src/models/server.rs new file mode 100644 index 0000000..3e575a1 --- /dev/null +++ b/src/models/server.rs @@ -0,0 +1,27 @@ +use chrono::{DateTime, Utc}; +use serde_derive::{Deserialize, Serialize}; +use serde_valid::Validate; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)] +pub struct Server { + pub id: i32, + pub user_id: String, + pub project_id: i32, + #[validate(min_length = 2)] + #[validate(max_length = 50)] + pub region: String, + #[validate(min_length = 2)] + #[validate(max_length = 50)] + pub zone: Option, + #[validate(min_length = 2)] + #[validate(max_length = 50)] + pub server: String, + #[validate(min_length = 2)] + #[validate(max_length = 50)] + pub os: String, + #[validate(min_length = 3)] + #[validate(max_length = 50)] + pub disk_type: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} \ No newline at end of file diff --git a/src/models/stack.rs b/src/models/stack.rs deleted file mode 100644 index f9ce272..0000000 --- a/src/models/stack.rs +++ /dev/null @@ -1,26 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde_json::Value; -use uuid::Uuid; -use serde::{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 - pub user_id: String, // external unique identifier for the user - pub name: String, - // pub body: sqlx::types::Json, - pub body: Value, //json type - 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/models/user.rs b/src/models/user.rs index 4e77598..0f6b1ef 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -6,19 +6,6 @@ pub struct User { pub first_name: String, pub last_name: String, pub email: String, + pub role: String, pub email_confirmed: bool, - // pub phone: Option, - // pub website: Option, } - -impl Clone for User { - fn clone(&self) -> Self { - User { - id: self.id.clone(), - first_name: self.first_name.clone(), - last_name: self.last_name.clone(), - email: self.email.clone(), - email_confirmed: self.email_confirmed.clone(), - } - } -} \ No newline at end of file diff --git a/src/routes/client/add.rs b/src/routes/client/add.rs index eee2ce0..bddbb74 100644 --- a/src/routes/client/add.rs +++ b/src/routes/client/add.rs @@ -1,90 +1,45 @@ use crate::configuration::Settings; +use crate::db; use crate::helpers::client; use crate::helpers::JsonResponse; -use crate::models::user::User; -use crate::models::Client; -use actix_web::error::ErrorInternalServerError; +use crate::models; use actix_web::{post, web, Responder, Result}; use sqlx::PgPool; use std::sync::Arc; -use tracing::Instrument; - #[tracing::instrument(name = "Add client.")] #[post("")] pub async fn add_handler( - user: web::ReqData, - settings: web::Data>, - pool: web::Data, + user: web::ReqData>, + settings: web::Data, + pg_pool: web::Data, ) -> Result { - let query_span = tracing::info_span!("Counting the user's clients"); + add_handler_inner(&user.id, settings, pg_pool) + .await + .map(|client| JsonResponse::build().set_item(client).ok("Ok")) + .map_err(|err| JsonResponse::::build().bad_request(err)) +} - match sqlx::query!( - r#" - SELECT - count(*) as client_count - FROM client c - WHERE c.user_id = $1 - "#, - user.id.clone(), - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(result) => { - let client_count = result.client_count.unwrap(); - if client_count >= settings.max_clients_number { - tracing::error!( - "Too many clients. The user {} has {} clients", - user.id, - client_count - ); +pub async fn add_handler_inner( + user_id: &String, + settings: web::Data, + pg_pool: web::Data, +) -> Result { + let client_count = db::client::count_by_user(pg_pool.get_ref(), user_id).await?; + if client_count >= settings.max_clients_number { + return Err("Too many clients created".to_string()); + } - return JsonResponse::build().err("Too many clients created".to_owned()); - } - } - Err(e) => { - tracing::error!("Failed to execute query: {:?}", e); - return JsonResponse::build().internal_error("Internal Server Error".to_owned()); - } - }; + let client = create_client(pg_pool.get_ref(), user_id).await?; + db::client::insert(pg_pool.get_ref(), client).await +} - let mut client = Client::default(); - client.id = 1; - client.user_id = user.id.clone(); - client.secret = client::generate_secret(pool.get_ref(), 255) +async fn create_client(pg_pool: &PgPool, user_id: &String) -> Result { + let mut client = models::Client::default(); + client.user_id = user_id.clone(); + client.secret = client::generate_secret(pg_pool, 255) .await - .map(|s| Some(s)) - .map_err(|s| ErrorInternalServerError(s))?; //todo move to helpers::JsonResponse - - let query_span = tracing::info_span!("Saving new client into the database"); - match sqlx::query!( - r#" - INSERT INTO client (user_id, secret, created_at, updated_at) - VALUES ($1, $2, NOW() at time zone 'utc', NOW() at time zone 'utc') - RETURNING id - "#, - client.user_id.clone(), - client.secret, - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(result) => { - tracing::info!("New client {} have been saved to database", result.id); - client.id = result.id; + .map(|s| Some(s))?; - return JsonResponse::build() - .set_id(client.id) - .set_item(Some(client)) - .ok("OK".to_owned()); - } - Err(e) => { - tracing::error!("Failed to execute query: {:?}", e); - let err = format!("Failed to insert. {}", e); - return JsonResponse::build().err(err); - } - } + Ok(client) } diff --git a/src/routes/client/disable.rs b/src/routes/client/disable.rs index 383130b..1c8b9d1 100644 --- a/src/routes/client/disable.rs +++ b/src/routes/client/disable.rs @@ -1,68 +1,59 @@ use crate::configuration::Settings; +use crate::db; use crate::helpers::JsonResponse; -use crate::models::user::User; -use crate::models::Client; -use actix_web::{error::ErrorInternalServerError, put, web, Responder, Result}; +use crate::models; +use actix_web::{put, web, Responder, Result}; use sqlx::PgPool; use std::sync::Arc; -use tracing::Instrument; -#[tracing::instrument(name = "Disable client.")] +#[tracing::instrument(name = "User disable client.")] #[put("/{id}/disable")] pub async fn disable_handler( - user: web::ReqData, - settings: web::Data>, - pool: web::Data, + user: web::ReqData>, + settings: web::Data, + pg_pool: web::Data, path: web::Path<(i32,)>, ) -> Result { let client_id = path.0; - let query_span = tracing::info_span!("Fetching the client by ID"); - let mut client: Client = match sqlx::query_as!( - Client, - r#" - SELECT - id, user_id, secret - FROM client c - WHERE c.id = $1 - "#, - client_id, - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(client) if client.secret.is_some() => Ok(client), - Ok(_client) => Err("client is not active"), - Err(sqlx::Error::RowNotFound) => Err("client not found"), - Err(e) => { - tracing::error!("Failed to execute fetch query: {:?}", e); - Err("") - } + let client = db::client::fetch(pg_pool.get_ref(), client_id) + .await + .map_err(|msg| JsonResponse::::build().internal_server_error(msg)) + .and_then( |client| { + match client { + Some(client) if client.user_id != user.id => Err(JsonResponse::::build().bad_request("client is not the owner")), + Some(client) => Ok(client), + None => Err(JsonResponse::::build().not_found("not found")) + } + })?; + + disable_client(pg_pool.get_ref(), client).await +} + +#[tracing::instrument(name = "Admin disable client.")] +#[put("/{id}/disable")] +pub async fn admin_disable_handler( + user: web::ReqData>, + settings: web::Data, + pg_pool: web::Data, + path: web::Path<(i32,)>, +) -> Result { + let client_id = path.0; + let client = db::client::fetch(pg_pool.get_ref(), client_id) + .await + .map_err(|msg| JsonResponse::::build().internal_server_error(msg))? + .ok_or_else(|| JsonResponse::::build().not_found("not found"))?; + + disable_client(pg_pool.get_ref(), client).await +} + +async fn disable_client(pg_pool: &PgPool, mut client: models::Client) -> Result { + if client.secret.is_none() { + return Err(JsonResponse::::build().bad_request("client is not active")); } - .map_err(|s| ErrorInternalServerError(s))?; //todo client.secret = None; - let query_span = tracing::info_span!("Updating client into the database"); - match sqlx::query!( - r#" - UPDATE client SET - secret=null, - updated_at=NOW() at time zone 'utc' - WHERE id = $1 - "#, - client.id - ) - .execute(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(_) => { - tracing::info!("Client {} have been saved to database", client.id); - JsonResponse::build().set_item(client).ok("success") - } - Err(e) => { - tracing::error!("Failed to execute query: {:?}", e); - JsonResponse::build().err("") - } - } + db::client::update(pg_pool, client) + .await + .map(|client| JsonResponse::build().set_item(client).ok("success")) + .map_err(|msg| JsonResponse::::build().bad_request(msg)) } diff --git a/src/routes/client/enable.rs b/src/routes/client/enable.rs index a870c65..e3955a6 100644 --- a/src/routes/client/enable.rs +++ b/src/routes/client/enable.rs @@ -1,75 +1,62 @@ use crate::configuration::Settings; -use crate::helpers::client; +use crate::db; +use crate::helpers; use crate::helpers::JsonResponse; -use crate::models::user::User; -use crate::models::Client; -use actix_web::{error::ErrorBadRequest, put, web, Responder, Result}; +use crate::models; +use actix_web::{put, web, Responder, Result}; use sqlx::PgPool; use std::sync::Arc; -use tracing::Instrument; -#[tracing::instrument(name = "Enable client.")] +#[tracing::instrument(name = "User enable client.")] #[put("/{id}/enable")] pub async fn enable_handler( - user: web::ReqData, - settings: web::Data>, - pool: web::Data, + user: web::ReqData>, + settings: web::Data, + pg_pool: web::Data, path: web::Path<(i32,)>, ) -> Result { let client_id = path.0; - let query_span = tracing::info_span!("Fetching the client by ID"); - let mut client: Client = match sqlx::query_as!( - Client, - r#" - SELECT - id, user_id, secret - FROM client c - WHERE c.id = $1 - "#, - client_id, - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(client) if client.secret.is_none() => Ok(client), - Ok(_client) => Err("client is already enabled"), - Err(sqlx::Error::RowNotFound) => Err("the client is not found"), - Err(e) => { - tracing::error!("Failed to execute fetch query: {:?}", e); + let client = db::client::fetch(pg_pool.get_ref(), client_id) + .await + .map_err(|msg| JsonResponse::::build().internal_server_error(msg))? + .ok_or_else(|| JsonResponse::::build().not_found("not found"))?; - Err("") - } + if client.user_id != user.id { + return Err(JsonResponse::::build().bad_request("client is not the owner")); } - .map_err(|s| ErrorBadRequest(s))?; //todo - client.secret = client::generate_secret(pool.get_ref(), 255) + enable_client(pg_pool.get_ref(), client).await +} + +#[tracing::instrument(name = "Admin enable client.")] +#[put("/{id}/enable")] +pub async fn admin_enable_handler( + user: web::ReqData>, + settings: web::Data, + pg_pool: web::Data, + path: web::Path<(i32,)>, +) -> Result { + let client_id = path.0; + let client = db::client::fetch(pg_pool.get_ref(), client_id) .await - .map(|s| Some(s)) - .map_err(|s| ErrorBadRequest(s))?; + .map_err(|msg| JsonResponse::::build().internal_server_error(msg))? + .ok_or_else(|| JsonResponse::::build().not_found("not found"))?; - let query_span = tracing::info_span!("Updating client into the database"); - match sqlx::query!( - r#" - UPDATE client SET - secret=$1, - updated_at=NOW() at time zone 'utc' - WHERE id = $2 - "#, - client.secret, - client.id - ) - .execute(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(_) => { - tracing::info!("Client {} have been saved to database", client.id); - JsonResponse::build().set_item(client).ok("success") - } - Err(e) => { - tracing::error!("Failed to execute query: {:?}", e); - JsonResponse::build().err_internal_server_error("") - } + enable_client(pg_pool.get_ref(), client).await +} + +async fn enable_client(pg_pool: &PgPool, mut client: models::Client) -> Result { + if client.secret.is_some() { + return Err(JsonResponse::::build().bad_request("client is already active")); } + + client.secret = helpers::client::generate_secret(pg_pool, 255) + .await + .map(|secret| Some(secret)) + .map_err(|err| JsonResponse::::build().bad_request(err))?; + + db::client::update(pg_pool, client) + .await + .map(|client| JsonResponse::build().set_item(client).ok("success")) + .map_err(|err| JsonResponse::::build().bad_request(err)) } diff --git a/src/routes/client/update.rs b/src/routes/client/update.rs index f50b4ed..de09581 100644 --- a/src/routes/client/update.rs +++ b/src/routes/client/update.rs @@ -1,74 +1,68 @@ +use crate::db; use crate::helpers::client; -use crate::models::user::User; -use crate::models::Client; +use crate::models; use crate::{configuration::Settings, helpers::JsonResponse}; -use actix_web::{error::ErrorBadRequest, put, web, Responder, Result}; +use actix_web::{put, web, Responder, Result}; use sqlx::PgPool; use std::sync::Arc; -use tracing::Instrument; -#[tracing::instrument(name = "Update client.")] +#[tracing::instrument(name = "User update client.")] #[put("/{id}")] pub async fn update_handler( - user: web::ReqData, - settings: web::Data>, - pool: web::Data, + user: web::ReqData>, + settings: web::Data, + pg_pool: web::Data, path: web::Path<(i32,)>, ) -> Result { let client_id = path.0; - let query_span = tracing::info_span!("Fetching the client by ID"); - let mut client: Client = match sqlx::query_as!( - Client, - r#" - SELECT - id, user_id, secret - FROM client c - WHERE c.id = $1 - "#, - client_id, - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(client) if client.secret.is_some() => Ok(client), - Ok(_client) => Err("client is not active"), - Err(sqlx::Error::RowNotFound) => Err("the client is not found"), - Err(e) => { - tracing::error!("Failed to execute fetch query: {:?}", e); + let client = db::client::fetch(pg_pool.get_ref(), client_id) + .await + .map_err(|msg| JsonResponse::::build().internal_server_error(msg))? + .ok_or_else(|| JsonResponse::::build().not_found("not found"))?; - Err("") - } + if client.user_id != user.id { + return Err(JsonResponse::::build().bad_request("client is not the owner")); } - .map_err(|s| ErrorBadRequest(s))?; //todo - client.secret = client::generate_secret(pool.get_ref(), 255) + update_client(pg_pool.get_ref(), client).await +} + +#[tracing::instrument(name = "Admin update client.")] +#[put("/{id}")] +pub async fn admin_update_handler( + user: web::ReqData>, + settings: web::Data, + pg_pool: web::Data, + path: web::Path<(i32,)>, +) -> Result { + let client_id = path.0; + let client = db::client::fetch(pg_pool.get_ref(), client_id) .await - .map(|s| Some(s)) - .map_err(|s| ErrorBadRequest(s))?; //todo + .map_err(|msg| JsonResponse::::build().internal_server_error(msg))? + .ok_or_else(|| JsonResponse::::build().not_found("not found"))?; - let query_span = tracing::info_span!("Updating client into the database"); - match sqlx::query!( - r#" - UPDATE client SET - secret=$1, - updated_at=NOW() at time zone 'utc' - WHERE id = $2 - "#, - client.secret, - client.id - ) - .execute(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(_) => { - tracing::info!("Client {} have been saved to database", client.id); - JsonResponse::build().set_item(client).ok("success") - } - Err(e) => { - tracing::error!("Failed to execute query: {:?}", e); - JsonResponse::build().err_internal_server_error("") - } + update_client(pg_pool.get_ref(), client).await +} + +async fn update_client(pg_pool: &PgPool, mut client: models::Client) -> Result { + if client.secret.is_none() { + return Err(JsonResponse::::build().bad_request("client is not active")); } + + client.secret = client::generate_secret(pg_pool, 255) + .await + .map(|s| Some(s)) + .map_err(|msg| JsonResponse::::build().bad_request(msg))?; + + db::client::update(pg_pool, client) + .await + .map(|client| { + JsonResponse::::build() + .set_item(client) + .ok("success") + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + JsonResponse::::build().internal_server_error("") + }) } diff --git a/src/routes/cloud/add.rs b/src/routes/cloud/add.rs new file mode 100644 index 0000000..ebd261e --- /dev/null +++ b/src/routes/cloud/add.rs @@ -0,0 +1,40 @@ +use std::ops::Deref; +use crate::forms; +use crate::helpers::JsonResponse; +use crate::models; +use crate::db; +use actix_web::{post, web, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use chrono::Utc; +use serde_valid::Validate; +use tracing::Instrument; + + +#[tracing::instrument(name = "Add cloud.")] +#[post("")] +pub async fn add( + user: web::ReqData>, + mut form: web::Json, + pg_pool: web::Data, +) -> Result { + + if !form.validate().is_ok() { + let errors = form.validate().unwrap_err().to_string(); + let err_msg = format!("Invalid data received {:?}", &errors); + tracing::debug!(err_msg); + + return Err(JsonResponse::::build().form_error(errors)); + } + + form.user_id = Some(user.id.clone()); + let cloud: models::Cloud = form.deref().into(); + + db::cloud::insert(pg_pool.get_ref(), cloud) + .await + .map(|cloud| JsonResponse::build() + .set_item(cloud) + .ok("success")) + .map_err(|_err| JsonResponse::::build() + .internal_server_error("Failed to insert")) +} diff --git a/src/routes/cloud/delete.rs b/src/routes/cloud/delete.rs new file mode 100644 index 0000000..2654bde --- /dev/null +++ b/src/routes/cloud/delete.rs @@ -0,0 +1,49 @@ +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{delete, web, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use crate::db; +use crate::models::Cloud; + +#[tracing::instrument(name = "Delete cloud record of a user.")] +#[delete("/{id}")] +pub async fn item( + user: web::ReqData>, + path: web::Path<(i32,)>, + pg_pool: web::Data, +) -> Result { + // Get cloud apps of logged user only + let (id,) = path.into_inner(); + + let cloud = db::cloud::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|cloud| { + match cloud { + Some(cloud) if cloud.user_id != user.id => { + Err(JsonResponse::::build().bad_request("Delete is forbidden")) + } + Some(cloud) => { + Ok(cloud) + }, + None => Err(JsonResponse::::build().not_found("not found")) + } + })?; + + db::cloud::delete(pg_pool.get_ref(), cloud.id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|result| { + match result + { + true => { + Ok(JsonResponse::::build().ok("Deleted")) + } + _ => { + Err(JsonResponse::::build().bad_request("Could not delete")) + } + } + }) + +} diff --git a/src/routes/cloud/get.rs b/src/routes/cloud/get.rs new file mode 100644 index 0000000..43ac801 --- /dev/null +++ b/src/routes/cloud/get.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; +use crate::db; +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{get, web, Responder, Result}; +use sqlx::PgPool; +use crate::forms::CloudForm; +use tracing::Instrument; + +#[tracing::instrument(name = "Get cloud credentials.")] +#[get("/{id}")] +pub async fn item( + path: web::Path<(i32,)>, + user: web::ReqData>, + pg_pool: web::Data, +) -> Result { + let id = path.0; + db::cloud::fetch(pg_pool.get_ref(), id) + .await + .map_err(|_err| JsonResponse::::build() + .internal_server_error("")) + .and_then(|cloud| { + match cloud { + Some(cloud) if cloud.user_id != user.id => { + Err(JsonResponse::not_found("record not found")) + }, + Some(cloud) => { + let cloud = CloudForm::decode_model(cloud, false); + Ok(JsonResponse::build().set_item(Some(cloud)).ok("OK")) + }, + None => Err(JsonResponse::not_found("record not found")), + } + }) + +} + +#[tracing::instrument(name = "Get all clouds.")] +#[get("")] +pub async fn list( + path: web::Path<()>, + user: web::ReqData>, + pg_pool: web::Data, +) -> Result { + db::cloud::fetch_by_user(pg_pool.get_ref(), user.id.as_ref()) + .await + .map(|clouds| { + + let clouds = clouds + .into_iter() + .map(|cloud| CloudForm::decode_model(cloud, false) ) + // .map_err(|e| tracing::error!("Failed to decode cloud, {:?}", e)) + .collect(); + + JsonResponse::build().set_list(clouds).ok("OK") + + }) + .map_err(|_err| JsonResponse::::build().internal_server_error("")) +} diff --git a/src/routes/cloud/mod.rs b/src/routes/cloud/mod.rs new file mode 100644 index 0000000..e4ea6c1 --- /dev/null +++ b/src/routes/cloud/mod.rs @@ -0,0 +1,9 @@ +pub mod add; +pub mod get; +pub mod update; +pub(crate) mod delete; + +// pub use add::*; +// pub use get::*; +// pub use update::*; +// pub use delete::*; diff --git a/src/routes/cloud/update.rs b/src/routes/cloud/update.rs new file mode 100644 index 0000000..5b4f4a1 --- /dev/null +++ b/src/routes/cloud/update.rs @@ -0,0 +1,54 @@ +use crate::forms; +use crate::helpers::JsonResponse; +use crate::models; +use crate::db; +use actix_web::{web, web::Data, Responder, Result, put}; +use serde_valid::Validate; +use sqlx::PgPool; +use std::sync::Arc; +use tracing::Instrument; +use std::ops::Deref; + +#[tracing::instrument(name = "Update cloud.")] +#[put("/{id}")] +pub async fn item( + path: web::Path<(i32,)>, + form: web::Json, + user: web::ReqData>, + pg_pool: Data, +) -> Result { + + let id = path.0; + let cloud_row = db::cloud::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|cloud| match cloud { + Some(cloud) if cloud.user_id != user.id => { + Err(JsonResponse::::build().bad_request("Cloud not found")) + } + Some(cloud) => Ok(cloud), + None => Err(JsonResponse::::build().not_found("Cloud not found")), + })?; + + if let Err(errors) = form.validate() { + return Err(JsonResponse::::build().form_error(errors.to_string())); + } + + let mut cloud:models::Cloud = form.deref().into(); + cloud.id = cloud_row.id; + cloud.user_id = user.id.clone(); + + tracing::debug!("Updating cloud {:?}", cloud); + + db::cloud::update(pg_pool.get_ref(), cloud) + .await + .map(|cloud| { + JsonResponse::::build() + .set_item(cloud) + .ok("success") + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + JsonResponse::::build().internal_server_error("Could not update") + }) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index cab3e46..647742a 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,8 +1,11 @@ -pub(crate) mod client; +pub mod client; pub mod health_checks; pub(crate) mod rating; pub(crate) mod test; pub use health_checks::*; -pub(crate) mod stack; -pub use stack::*; +pub(crate) mod project; +pub(crate) mod cloud; +pub(crate) mod server; + +pub use project::*; diff --git a/src/routes/project/add.rs b/src/routes/project/add.rs new file mode 100644 index 0000000..683e1d3 --- /dev/null +++ b/src/routes/project/add.rs @@ -0,0 +1,48 @@ +use crate::db; +use crate::forms::project::ProjectForm; +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{ + post, web, + web::{Data}, + Responder, Result, +}; +use serde_json::Value; +use sqlx::PgPool; +use std::sync::Arc; +use serde_valid::Validate; + +#[tracing::instrument(name = "Add project.")] +#[post("")] +pub async fn item( + web::Json(request_json): web::Json, + user: web::ReqData>, + pg_pool: Data, +) -> Result { + // @todo ACL + let form: ProjectForm= serde_json::from_value(request_json.clone()) + .map_err(|err| JsonResponse::bad_request(err.to_string()))?; + if !form.validate().is_ok() { + let errors = form.validate().unwrap_err(); + return Err(JsonResponse::bad_request(errors.to_string())); + } + + let project_name = form.custom.custom_stack_code.clone(); + let body: Value = serde_json::to_value::(form) + .or(serde_json::to_value::(ProjectForm::default())) + .unwrap(); + + let project = models::Project::new( + user.id.clone(), + project_name, + body, + request_json + ); + + db::project::insert(pg_pool.get_ref(), project) + .await + .map(|project| JsonResponse::build().set_item(project).ok("Ok")) + .map_err(|_| { + JsonResponse::internal_server_error("Internal Server Error") + }) +} diff --git a/src/routes/project/compose.rs b/src/routes/project/compose.rs new file mode 100644 index 0000000..ca2e414 --- /dev/null +++ b/src/routes/project/compose.rs @@ -0,0 +1,59 @@ +use crate::db; +use crate::helpers::project::builder::DcBuilder; +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{get, web, web::Data, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; + +#[tracing::instrument(name = "User's generate docker-compose.")] +#[get("/{id}/compose")] +pub async fn add( + user: web::ReqData>, + path: web::Path<(i32,)>, + pg_pool: Data, +) -> Result { + let id = path.0; + let project = db::project::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|project| match project { + Some(project) if project.user_id != user.id => { + Err(JsonResponse::::build().not_found("not found")) + } + Some(project) => Ok(project), + None => Err(JsonResponse::::build().not_found("not found")), + })?; + + DcBuilder::new(project) + .build() + .map_err(|err| { + JsonResponse::::build().internal_server_error(err) + }) + .map(|fc| JsonResponse::build().set_id(id).set_item(fc).ok("Success")) +} + +#[tracing::instrument(name = "Generate docker-compose. Admin")] +#[get("/{id}/compose")] +pub async fn admin( + user: web::ReqData>, + path: web::Path<(i32,)>, + pg_pool: Data, +) -> Result { + // Admin function for generating compose file for specified user + let id = path.0; + let project = db::project::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|project| match project { + Some(project) => Ok(project), + None => Err(JsonResponse::::build().not_found("not found")), + })?; + + DcBuilder::new(project) + .build() + .map_err(|err| { + JsonResponse::::build().internal_server_error(err) + }) + .map(|fc| JsonResponse::build().set_id(id).set_item(fc).ok("Success")) +} diff --git a/src/routes/project/delete.rs b/src/routes/project/delete.rs new file mode 100644 index 0000000..92c6d98 --- /dev/null +++ b/src/routes/project/delete.rs @@ -0,0 +1,49 @@ +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{delete, web, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use crate::db; +use crate::models::Project; + +#[tracing::instrument(name = "Delete project of a user.")] +#[delete("/{id}")] +pub async fn item( + user: web::ReqData>, + path: web::Path<(i32,)>, + pg_pool: web::Data, +) -> Result { + // Get project apps of logged user only + let (id,) = path.into_inner(); + + let project = db::project::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|project| { + match project { + Some(project) if project.user_id != user.id => { + Err(JsonResponse::::build().bad_request("Delete is forbidden")) + } + Some(project) => { + Ok(project) + }, + None => Err(JsonResponse::::build().not_found("")) + } + })?; + + db::project::delete(pg_pool.get_ref(), project.id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|result| { + match result + { + true => { + Ok(JsonResponse::::build().ok("Deleted")) + } + _ => { + Err(JsonResponse::::build().bad_request("Could not delete")) + } + } + }) + +} diff --git a/src/routes/project/deploy.rs b/src/routes/project/deploy.rs new file mode 100644 index 0000000..b0d767e --- /dev/null +++ b/src/routes/project/deploy.rs @@ -0,0 +1,278 @@ +use crate::configuration::Settings; +use crate::db; +use crate::forms; +use crate::helpers::project::builder::DcBuilder; +use crate::helpers::{JsonResponse, MqManager}; +use crate::models; +use actix_web::{post, web, web::Data, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use serde_valid::Validate; +use crate::helpers::compressor::compress; +use chrono::{Utc}; + + + +#[tracing::instrument(name = "Deploy for every user")] +#[post("/{id}/deploy")] +pub async fn item( + user: web::ReqData>, + path: web::Path<(i32,)>, + mut form: web::Json, + pg_pool: Data, + mq_manager: Data, + sets: Data, +) -> Result { + let id = path.0; + tracing::debug!("User {:?} is deploying project: {}", user, id); + + if !form.validate().is_ok() { + let errors = form.validate().unwrap_err().to_string(); + let err_msg = format!("Invalid form data received {:?}", &errors); + tracing::debug!(err_msg); + + return Err(JsonResponse::::build().form_error(errors)); + } + + // Validate project + let project = db::project::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|project| match project { + Some(project) => Ok(project), + None => Err(JsonResponse::::build().not_found("not found")), + })?; + + // Build compose + let id = project.id; + let dc = DcBuilder::new(project); + let fc = dc.build().map_err(|err| { + JsonResponse::::build().internal_server_error(err) + })?; + + form.cloud.user_id = Some(user.id.clone()); + form.cloud.project_id = Some(id); + // Save cloud credentials if requested + let cloud_creds: models::Cloud = (&form.cloud).into(); + + // let cloud_creds = forms::Cloud::decode_model(cloud_creds, false); + + if Some(true) == cloud_creds.save_token { + db::cloud::insert(pg_pool.get_ref(), cloud_creds.clone()) + .await + .map(|cloud| cloud) + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + })?; + } + + // Save server type and region + let mut server: models::Server = (&form.server).into(); + server.user_id = user.id.clone(); + server.project_id = id; + let server = db::server::insert(pg_pool.get_ref(), server) + .await + .map(|server| server) + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + })?; + + // Build Payload for the 3-d party service through RabbitMQ + let mut payload = forms::project::Payload::try_from(&dc.project) + .map_err(|err| JsonResponse::::build().bad_request(err))?; + + payload.server = Some(server.into()); + payload.cloud = Some(cloud_creds.into()); + payload.stack = form.stack.clone().into(); + payload.user_token = Some(user.id.clone()); + payload.user_email = Some(user.email.clone()); + payload.docker_compose = Some(compress(fc.as_str())); + + // Store deployment attempts into deployment table in db + let json_request = dc.project.body.clone(); + let deployment = models::Deployment::new( + dc.project.id, + String::from("pending"), + json_request + ); + + let result = db::deployment::insert(pg_pool.get_ref(), deployment) + .await + .map(|deployment| { + payload.id = Some(deployment.id); + deployment + } + ) + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + }); + + tracing::debug!("Save deployment result: {:?}", result); + tracing::debug!("Send project data <<<>>>{:?}", payload); + + // Send Payload + mq_manager + .publish( + "install".to_string(), + "install.start.tfa.all.all".to_string(), + &payload, + ) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .map(|_| { + JsonResponse::::build() + .set_id(id) + .ok("Success") + }) + +} +#[tracing::instrument(name = "Deploy, when cloud token is saved")] +#[post("/{id}/deploy/{cloud_id}")] +pub async fn saved_item( + user: web::ReqData>, + form: web::Json, + path: web::Path<(i32, i32)>, + pg_pool: Data, + mq_manager: Data, + sets: Data, +) -> Result { + let id = path.0; + let cloud_id = path.1; + + tracing::debug!("User {:?} is deploying project: {} to cloud: {} ", user, id, cloud_id); + + if !form.validate().is_ok() { + let errors = form.validate().unwrap_err().to_string(); + let err_msg = format!("Invalid form data received {:?}", &errors); + tracing::debug!(err_msg); + + return Err(JsonResponse::::build().form_error(errors)); + } + + // Validate project + let project = db::project::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|project| match project { + Some(project) => Ok(project), + None => Err(JsonResponse::::build().not_found("Project not found")), + })?; + + // Build compose + let id = project.id; + let dc = DcBuilder::new(project); + let fc = dc.build().map_err(|err| { + JsonResponse::::build().internal_server_error(err) + })?; + + let cloud = match db::cloud::fetch(pg_pool.get_ref(), cloud_id).await { + Ok(cloud) => { + match cloud { + Some(cloud) => { + cloud + }, + None => { + return Err(JsonResponse::::build().not_found("No cloud configured")); + } + } + } + Err(_e) => { + return Err(JsonResponse::::build().not_found("No cloud configured")); + } + }; + + let server = match db::server::fetch_by_project(pg_pool.get_ref(), dc.project.id.clone()).await { + Ok(server) => { + // currently we support only one type of servers + //@todo multiple server types support + match server.into_iter().nth(0) { + Some(mut server) => { + // new updates + server.disk_type = form.server.disk_type.clone(); + server.region = form.server.region.clone(); + server.server = form.server.server.clone(); + server.zone = form.server.zone.clone(); + server.os = form.server.os.clone(); + server.user_id = user.id.clone(); + server.project_id = id; + server + }, + None => { + // Create new server + // form.update_with(server.into()); + let mut server: models::Server = (&form.server).into(); + server.user_id = user.id.clone(); + server.project_id = id; + db::server::insert(pg_pool.get_ref(), server) + .await + .map(|server| server) + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + })? + } + } + } + Err(_e) => { + return Err(JsonResponse::::build().not_found("No servers configured")); + } + }; + + let server = db::server::update(pg_pool.get_ref(), server) + .await + .map(|server| server) + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + })?; + + // Building Payload for the 3-d party service through RabbitMQ + // let mut payload = forms::project::Payload::default(); + let mut payload = forms::project::Payload::try_from(&dc.project) + .map_err(|err| JsonResponse::::build().bad_request(err))?; + + payload.server = Some(server.into()); + payload.cloud = Some(cloud.into()); + payload.stack = form.stack.clone().into(); + payload.user_token = Some(user.id.clone()); + payload.user_email = Some(user.email.clone()); + payload.docker_compose = Some(compress(fc.as_str())); + + // Store deployment attempts into deployment table in db + let json_request = dc.project.body.clone(); + let deployment = models::Deployment::new( + dc.project.id, + String::from("pending"), + json_request + ); + + let result = db::deployment::insert(pg_pool.get_ref(), deployment) + .await + .map(|deployment| { + payload.id = Some(deployment.id); + deployment + }) + .map_err(|_| { + JsonResponse::::build().internal_server_error("Internal Server Error") + }); + + tracing::debug!("Save deployment result: {:?}", result); + tracing::debug!("Send project data <<<>>>{:?}", payload); + + // Send Payload + mq_manager + .publish( + "install".to_string(), + "install.start.tfa.all.all".to_string(), + &payload, + ) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .map(|_| { + JsonResponse::::build() + .set_id(id) + .ok("Success") + }) + +} + + + diff --git a/src/routes/project/get.rs b/src/routes/project/get.rs new file mode 100644 index 0000000..cc9da9c --- /dev/null +++ b/src/routes/project/get.rs @@ -0,0 +1,61 @@ +use crate::db; +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{get, web, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; + +#[tracing::instrument(name = "Get logged user project.")] +#[get("/{id}")] +pub async fn item( + user: web::ReqData>, + path: web::Path<(i32,)>, + pg_pool: web::Data, +) -> Result { + // Get project apps of logged user only + let id = path.0; + + db::project::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::internal_server_error(err.to_string())) + .and_then(|project| match project { + Some(project) if project.user_id != user.id => { + Err(JsonResponse::not_found("not found")) + } + Some(project) => Ok(JsonResponse::build().set_item(Some(project)).ok("OK")), + None => Err(JsonResponse::not_found("not found")), + }) +} + + +#[tracing::instrument(name = "Get project list.")] +#[get("")] +pub async fn list( + user: web::ReqData>, + pg_pool: web::Data, +) -> Result { + db::project::fetch_by_user(pg_pool.get_ref(), &user.id) + .await + .map_err(|err| JsonResponse::internal_server_error(err)) + .map(|projects| JsonResponse::build().set_list(projects).ok("OK")) +} + + +//admin's endpoint +#[tracing::instrument(name = "Get user's project list.")] +#[get("/user/{id}")] +pub async fn admin_list( + user: web::ReqData>, + path: web::Path<(String,)>, + pg_pool: web::Data, +) -> Result { + // This is admin endpoint, used by a client app, client app is confidential + // it should return projects by user id + // in order to pass validation at external deployment service + let user_id = path.into_inner().0; + + db::project::fetch_by_user(pg_pool.get_ref(), &user_id) + .await + .map_err(|err| JsonResponse::internal_server_error(err)) + .map(|projects| JsonResponse::build().set_list(projects).ok("OK")) +} diff --git a/src/routes/project/mod.rs b/src/routes/project/mod.rs new file mode 100644 index 0000000..05f7de8 --- /dev/null +++ b/src/routes/project/mod.rs @@ -0,0 +1,11 @@ +pub mod add; +pub mod deploy; +pub mod get; +pub mod update; +pub(crate) mod compose; +pub(crate) mod delete; + +pub use add::item; +// pub use update::*; +// pub use deploy::*; +// pub use get::*; diff --git a/src/routes/project/service.rs b/src/routes/project/service.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/project/update.rs b/src/routes/project/update.rs new file mode 100644 index 0000000..38d08ea --- /dev/null +++ b/src/routes/project/update.rs @@ -0,0 +1,80 @@ +use std::str::FromStr; +use crate::forms::project::{ProjectForm, DockerImageReadResult}; +use crate::helpers::JsonResponse; +use crate::models; +use crate::db; +use actix_web::{web, Responder, Result, put}; +use serde_json::Value; +use serde_valid::Validate; +use sqlx::PgPool; +use std::sync::Arc; +use tracing::Instrument; +use std::str; + +#[tracing::instrument(name = "Update project.")] +#[put("/{id}")] +pub async fn item( + path: web::Path<(i32,)>, + web::Json(request_json): web::Json, + user: web::ReqData>, + pg_pool: web::Data, +) -> Result { + let id = path.0; + let mut project = db::project::fetch(pg_pool.get_ref(), id) + .await + .map_err(JsonResponse::internal_server_error) + .and_then(|project| match project { + Some(project) if project.user_id != user.id => { + Err(JsonResponse::bad_request("Project not found")) + } + Some(project) => Ok(project), + None => Err(JsonResponse::not_found("Project not found")), + })?; + + // @todo ACL + let form: ProjectForm= serde_json::from_value(request_json.clone()) + .map_err(|err| JsonResponse::bad_request(err.to_string()))?; + + if !form.validate().is_ok() { + let errors = form.validate().unwrap_err(); + return Err(JsonResponse::bad_request(errors.to_string())); + } + + let project_name = form.custom.custom_stack_code.clone(); + + match form.is_readable_docker_image().await { + Ok(result) => { + if false == result.readable { + return Err(JsonResponse::::build() + .set_item(result) + .bad_request("Can not access docker image")); + } + } + Err(e) => { + return Err(JsonResponse::::build() + .bad_request(e)); + } + } + + + let body: Value = serde_json::to_value::(form) + .or(serde_json::to_value::(ProjectForm::default())) + .unwrap(); + + + project.name = project_name; + project.body = body; + project.request_json = request_json; + + db::project::update(pg_pool.get_ref(), project) + .await + .map(|project| { + JsonResponse::::build() + .set_item(project) + .ok("success") + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + JsonResponse::internal_server_error("") + }) +} diff --git a/src/routes/rating/add.rs b/src/routes/rating/add.rs index dd13eee..a97b8d9 100644 --- a/src/routes/rating/add.rs +++ b/src/routes/rating/add.rs @@ -1,108 +1,50 @@ use crate::forms; +use crate::views; use crate::helpers::JsonResponse; use crate::models; -use crate::models::user::User; -use crate::models::RateCategory; +use crate::db; +use actix_web::{post, web, Responder, Result}; use sqlx::PgPool; -use tracing::Instrument; -use actix_web::{ - web, - post, - Responder, Result, -}; +use std::sync::Arc; +use serde_valid::Validate; // workflow // add, update, list, get(user_id), ACL, // ACL - access to func for a user // ACL - access to objects for a user - #[tracing::instrument(name = "Add rating.")] #[post("")] -pub async fn add_handler( - user: web::ReqData, - form: web::Json, - pool: web::Data, +pub async fn user_add_handler( + user: web::ReqData>, + form: web::Json, + pg_pool: web::Data, ) -> Result { - let query_span = tracing::info_span!("Check product existence by id."); - match sqlx::query_as!( - models::Product, - r"SELECT * FROM product WHERE obj_id = $1", - form.obj_id - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(product) => { - tracing::info!("Found product: {:?}", product.obj_id); - } - Err(e) => { - tracing::error!("Failed to fetch product: {:?}, error: {:?}", form.obj_id, e); - return JsonResponse::::build() - .err(format!("Object not found {}", form.obj_id)); - } - }; - - let query_span = tracing::info_span!("Search for existing vote."); - match sqlx::query!( - r"SELECT id FROM rating where user_id=$1 AND obj_id=$2 AND category=$3 LIMIT 1", - user.id, - form.obj_id, - form.category as RateCategory - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(record) => { - tracing::info!( - "rating exists: {:?}, user: {}, product: {}, category: {:?}", - record.id, - user.id, - form.obj_id, - form.category - ); - return JsonResponse::build().conflict("Already rated".to_owned()); - } - Err(sqlx::Error::RowNotFound) => {} - Err(e) => { - tracing::error!("Failed to fetch rating, error: {:?}", e); - return JsonResponse::build().err(format!("Internal Server Error")); - } + if let Err(errors) = form.validate() { + return Err(JsonResponse::::build().form_error(errors.to_string())); } - let query_span = tracing::info_span!("Saving new rating details into the database"); - // Insert rating - match sqlx::query!( - r#" - INSERT INTO rating (user_id, obj_id, category, comment, hidden,rate, - created_at, - updated_at) - VALUES ($1, $2, $3, $4, $5, $6, NOW() at time zone 'utc', NOW() at time zone 'utc') - RETURNING id - "#, - user.id, - form.obj_id, - form.category as models::RateCategory, - form.comment, - false, - form.rate - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(result) => { - tracing::info!("New rating {} have been saved to database", result.id); + let _product = db::product::fetch_by_obj(pg_pool.get_ref(), form.obj_id) + .await + .map_err(|_msg| JsonResponse::::build().internal_server_error(_msg))? + .ok_or_else(|| JsonResponse::::build().not_found("not found"))? + ; + + let rating = db::rating::fetch_by_obj_and_user_and_category( + pg_pool.get_ref(), form.obj_id, user.id.clone(), form.category) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err))?; - JsonResponse::build() - .set_id(result.id) - .ok("Saved".to_owned()) - } - Err(e) => { - tracing::error!("Failed to execute query: {:?}", e); - JsonResponse::build().internal_error("Failed to insert".to_owned()) - } + if rating.is_some() { + return Err(JsonResponse::::build().bad_request("already rated")); } + + let mut rating: models::Rating = form.into_inner().into(); + rating.user_id = user.id.clone(); + + db::rating::insert(pg_pool.get_ref(), rating) + .await + .map(|rating| JsonResponse::build().set_item(Into::::into(rating)).ok("success")) + .map_err(|_err| JsonResponse::::build() + .internal_server_error("Failed to insert")) } diff --git a/src/routes/rating/delete.rs b/src/routes/rating/delete.rs new file mode 100644 index 0000000..2dde9c7 --- /dev/null +++ b/src/routes/rating/delete.rs @@ -0,0 +1,67 @@ +use crate::db; +use crate::helpers::JsonResponse; +use crate::models; +use crate::views; +use actix_web::{delete, web, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; + +#[tracing::instrument(name = "User delete rating.")] +#[delete("/{id}")] +pub async fn user_delete_handler( + user: web::ReqData>, + path: web::Path<(i32,)>, + pg_pool: web::Data, +) -> Result { + let rate_id = path.0; + let mut rating = db::rating::fetch(pg_pool.get_ref(), rate_id) + .await + .map_err(|_err| JsonResponse::::build().internal_server_error("")) + .and_then(|rating| { + match rating { + Some(rating) if rating.user_id == user.id && rating.hidden == Some(false) => Ok(rating), + _ => Err(JsonResponse::::build().not_found("not found")) + } + })?; + + rating.hidden.insert(true); + + db::rating::update(pg_pool.get_ref(), rating) + .await + .map(|rating| { + JsonResponse::::build().ok("success") + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + JsonResponse::::build().internal_server_error("Rating not update") + }) +} + +#[tracing::instrument(name = "Admin delete rating.")] +#[delete("/{id}")] +pub async fn admin_delete_handler( + user: web::ReqData>, + path: web::Path<(i32,)>, + pg_pool: web::Data, +) -> Result { + let rate_id = path.0; + let mut rating = db::rating::fetch(pg_pool.get_ref(), rate_id) + .await + .map_err(|_err| JsonResponse::::build().internal_server_error("")) + .and_then(|rating| { + match rating { + Some(rating) => Ok(rating), + _ => Err(JsonResponse::::build().not_found("not found")) + } + })?; + + db::rating::delete(pg_pool.get_ref(), rating) + .await + .map(|_| { + JsonResponse::::build().ok("success") + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + JsonResponse::::build().internal_server_error("Rating not deleted") + }) +} diff --git a/src/routes/rating/edit.rs b/src/routes/rating/edit.rs new file mode 100644 index 0000000..88cd7a3 --- /dev/null +++ b/src/routes/rating/edit.rs @@ -0,0 +1,89 @@ +use crate::forms; +use crate::helpers::JsonResponse; +use crate::models; +use crate::views; +use crate::db; +use actix_web::{put, web, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use serde_valid::Validate; + +// workflow +// add, update, list, get(user_id), ACL, +// ACL - access to func for a user +// ACL - access to objects for a user + +#[tracing::instrument(name = "User edit rating.")] +#[put("/{id}")] +pub async fn user_edit_handler( + path: web::Path<(i32,)>, + user: web::ReqData>, + form: web::Json, + pg_pool: web::Data, +) -> Result { + if let Err(errors) = form.validate() { + return Err(JsonResponse::::build().form_error(errors.to_string())); + } + + let rate_id = path.0; + let mut rating = db::rating::fetch(pg_pool.get_ref(), rate_id) + .await + .map_err(|_err| JsonResponse::::build().internal_server_error("")) + .and_then(|rating| { + match rating { + Some(rating) if rating.user_id == user.id && rating.hidden == Some(false) => Ok(rating), + _ => Err(JsonResponse::::build().not_found("not found")) + } + })?; + + form.into_inner().update(&mut rating); + + db::rating::update(pg_pool.get_ref(), rating) + .await + .map(|rating| { + JsonResponse::build() + .set_item(Into::::into(rating)) + .ok("success") + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + JsonResponse::::build().internal_server_error("Rating not update") + }) +} + +#[tracing::instrument(name = "Admin edit rating.")] +#[put("/{id}")] +pub async fn admin_edit_handler( + path: web::Path<(i32,)>, + form: web::Json, + pg_pool: web::Data, +) -> Result { + if let Err(errors) = form.validate() { + return Err(JsonResponse::::build().form_error(errors.to_string())); + } + + let rate_id = path.0; + let mut rating = db::rating::fetch(pg_pool.get_ref(), rate_id) + .await + .map_err(|_err| JsonResponse::::build().internal_server_error("")) + .and_then(|rating| { + match rating { + Some(rating) => Ok(rating), + _ => Err(JsonResponse::::build().not_found("not found")) + } + })?; + + form.into_inner().update(&mut rating); + + db::rating::update(pg_pool.get_ref(), rating) + .await + .map(|rating| { + JsonResponse::::build() + .set_item(Into::::into(rating)) + .ok("success") + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + JsonResponse::::build().internal_server_error("Rating not update") + }) +} diff --git a/src/routes/rating/get.rs b/src/routes/rating/get.rs index 75281c0..08f7d9c 100644 --- a/src/routes/rating/get.rs +++ b/src/routes/rating/get.rs @@ -1,71 +1,87 @@ +use crate::db; use crate::helpers::JsonResponse; use crate::models; +use crate::views; use actix_web::{get, web, Responder, Result}; use sqlx::PgPool; -use tracing::Instrument; +use std::convert::Into; -// workflow -// add, update, list, get(user_id), ACL, -// ACL - access to func for a user -// ACL - access to objects for a user - -#[tracing::instrument(name = "Get rating.")] +#[tracing::instrument(name = "Anonymouse get rating.")] #[get("/{id}")] -pub async fn get_handler( +pub async fn anonymous_get_handler( path: web::Path<(i32,)>, - pool: web::Data, + pg_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!( - models::Rating, - r"SELECT * FROM rating WHERE id=$1 LIMIT 1", - rate_id - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(rating) => { - tracing::info!("rating found: {:?}", rating.id); - return JsonResponse::build().set_item(Some(rating)).ok("OK"); - } - Err(sqlx::Error::RowNotFound) => { - return JsonResponse::build().err("Not Found"); - } - Err(e) => { - tracing::error!("Failed to fetch rating, error: {:?}", e); - return JsonResponse::build().err("Internal Server Error"); - } - } + let rating = db::rating::fetch(pg_pool.get_ref(), rate_id) + .await + .map_err(|_err| JsonResponse::::build().internal_server_error("")) + .and_then(|rating| { + match rating { + Some(rating) if rating.hidden == Some(false) => { Ok(rating) }, + _ => Err(JsonResponse::::build().not_found("not found")) + } + })?; + + Ok(JsonResponse::build().set_item(Into::::into(rating)).ok("OK")) } -#[tracing::instrument(name = "Get all ratings.")] +#[tracing::instrument(name = "Anonymous get all ratings.")] #[get("")] -pub async fn list_handler( +pub async fn anonymous_list_handler( path: web::Path<()>, - pool: web::Data, + pg_pool: web::Data, +) -> Result { + db::rating::fetch_all_visible(pg_pool.get_ref()) + .await + .map(|ratings| { + let ratings = ratings + .into_iter() + .map(Into::into) + .collect::>() + ; + + JsonResponse::build().set_list(ratings).ok("OK") + }) + .map_err(|_err| JsonResponse::::build().internal_server_error("")) +} + +#[tracing::instrument(name = "Admin get rating.")] +#[get("/{id}")] +pub async fn admin_get_handler( + path: web::Path<(i32,)>, + pg_pool: web::Data, ) -> Result { - /// Get ratings of all users + let rate_id = path.0; + let rating = db::rating::fetch(pg_pool.get_ref(), rate_id) + .await + .map_err(|_err| JsonResponse::::build().internal_server_error("")) + .and_then(|rating| { + match rating { + Some(rating) => { Ok(rating) }, + _ => Err(JsonResponse::::build().not_found("not found")) + } + })?; + + Ok(JsonResponse::build().set_item(Into::::into(rating)).ok("OK")) +} - let query_span = tracing::info_span!("Get all rates."); - // let category = path.0; - match sqlx::query_as!(models::Rating, r"SELECT * FROM rating") - .fetch_all(pool.get_ref()) - .instrument(query_span) +#[tracing::instrument(name = "Admin get the list of ratings.")] +#[get("")] +pub async fn admin_list_handler( + path: web::Path<()>, + pg_pool: web::Data, +) -> Result { + db::rating::fetch_all(pg_pool.get_ref()) .await - { - Ok(rating) => { - tracing::info!("Ratings found: {:?}", rating.len()); - return JsonResponse::build().set_list(rating).ok("OK".to_owned()); - } - Err(sqlx::Error::RowNotFound) => { - return JsonResponse::build().not_found("Not Found".to_owned()); - } - Err(e) => { - tracing::error!("Failed to fetch rating, error: {:?}", e); - return JsonResponse::build().internal_error("Internal Server Error".to_owned()); - } - } + .map(|ratings| { + let ratings = ratings + .into_iter() + .map(Into::into) + .collect::>() + ; + + JsonResponse::build().set_list(ratings).ok("OK") + }) + .map_err(|_err| JsonResponse::::build().internal_server_error("")) } diff --git a/src/routes/rating/mod.rs b/src/routes/rating/mod.rs index b13668a..2bd48db 100644 --- a/src/routes/rating/mod.rs +++ b/src/routes/rating/mod.rs @@ -1,5 +1,9 @@ pub mod add; pub mod get; +mod edit; +mod delete; pub use add::*; pub use get::*; +pub use edit::*; +pub use delete::*; diff --git a/src/routes/server/add.rs b/src/routes/server/add.rs new file mode 100644 index 0000000..5a8970c --- /dev/null +++ b/src/routes/server/add.rs @@ -0,0 +1,76 @@ +// use crate::forms; +// use crate::helpers::JsonResponse; +// use crate::models; +// use crate::db; +// use actix_web::{post, web, Responder, Result}; +// use sqlx::PgPool; +// use tracing::Instrument; +// use std::sync::Arc; +// use serde_valid::Validate; + +// workflow +// add, update, list, get(user_id), ACL, +// ACL - access to func for a user +// ACL - access to objects for a user + +// #[tracing::instrument(name = "Add server.")] +// #[post("")] +// pub async fn add( +// user: web::ReqData>, +// form: web::Json, +// pg_pool: web::Data, +// ) -> Result { +// // +// // if !form.validate().is_ok() { +// // let errors = form.validate().unwrap_err().to_string(); +// // let err_msg = format!("Invalid data received {:?}", &errors); +// // tracing::debug!(err_msg); +// // +// // return Err(JsonResponse::::build().form_error(errors)); +// // } +// // +// // +// // db::cloud::fetch(pg_pool.get_ref(), form.cloud_id) +// // .await +// // .map_err(|err| JsonResponse::::build().internal_server_error(err)) +// // .and_then(|cloud| { +// // match cloud { +// // Some(cloud) if cloud.user_id != user.id => { +// // Err(JsonResponse::::build().bad_request("Cloud not found")) +// // } +// // Some(cloud) => { +// // Ok(cloud) +// // }, +// // None => Err(JsonResponse::::build().not_found("Cloud not found")) +// // } +// // })?; +// // +// // db::project::fetch(pg_pool.get_ref(), form.project_id) +// // .await +// // .map_err(|_err| JsonResponse::::build() +// // .bad_request("Invalid project")) +// // .and_then(|project| { +// // match project { +// // Some(project) if project.user_id != user.id => { +// // Err(JsonResponse::::build().bad_request("Project not found")) +// // } +// // Some(project) => { Ok(project) }, +// // None => Err(JsonResponse::::build().not_found("Project not found")) +// // } +// // })?; +// // +// // let mut server: models::Server = form.into_inner().into(); +// // server.user_id = user.id.clone(); +// // +// // db::server::insert(pg_pool.get_ref(), server) +// // .await +// // .map(|server| JsonResponse::build() +// // .set_item(server) +// // .ok("success")) +// // .map_err(|err| +// // match err { +// // _ => { +// // return JsonResponse::::build().internal_server_error("Failed to insert"); +// // } +// // }) +// } diff --git a/src/routes/server/delete.rs b/src/routes/server/delete.rs new file mode 100644 index 0000000..35440ec --- /dev/null +++ b/src/routes/server/delete.rs @@ -0,0 +1,49 @@ +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{delete, web, Responder, Result}; +use sqlx::PgPool; +use std::sync::Arc; +use crate::db; +use crate::models::Server; + +#[tracing::instrument(name = "Delete user's server.")] +#[delete("/{id}")] +pub async fn item( + user: web::ReqData>, + path: web::Path<(i32,)>, + pg_pool: web::Data, +) -> Result { + // Get server apps of logged user only + let (id,) = path.into_inner(); + + let server = db::server::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|server| { + match server { + Some(server) if server.user_id != user.id => { + Err(JsonResponse::::build().bad_request("Delete is forbidden")) + } + Some(server) => { + Ok(server) + }, + None => Err(JsonResponse::::build().not_found("")) + } + })?; + + db::server::delete(pg_pool.get_ref(), server.id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|result| { + match result + { + true => { + Ok(JsonResponse::::build().ok("Item deleted")) + } + _ => { + Err(JsonResponse::::build().bad_request("Could not delete")) + } + } + }) + +} diff --git a/src/routes/server/get.rs b/src/routes/server/get.rs new file mode 100644 index 0000000..3bd5a6f --- /dev/null +++ b/src/routes/server/get.rs @@ -0,0 +1,49 @@ +use std::sync::Arc; +use crate::db; +use crate::helpers::JsonResponse; +use crate::models; +use actix_web::{get, web, Responder, Result}; +use sqlx::PgPool; +// use tracing::Instrument; + +// workflow +// add, update, list, get(user_id), ACL, +// ACL - access to func for a user +// ACL - access to objects for a user + +#[tracing::instrument(name = "Get server.")] +#[get("/{id}")] +pub async fn item( + path: web::Path<(i32,)>, + user: web::ReqData>, + pg_pool: web::Data, +) -> Result { + let id = path.0; + db::server::fetch(pg_pool.get_ref(), id) + .await + .map_err(|_err| JsonResponse::::build() + .internal_server_error("")) + .and_then(|server| { + match server { + Some(project) if project.user_id != user.id => { + Err(JsonResponse::not_found("not found")) + }, + Some(server) => Ok(JsonResponse::build().set_item(Some(server)).ok("OK")), + None => Err(JsonResponse::not_found("not found")), + } + }) + +} + +#[tracing::instrument(name = "Get all servers.")] +#[get("")] +pub async fn list( + path: web::Path<()>, + user: web::ReqData>, + pg_pool: web::Data, +) -> Result { + db::server::fetch_by_user(pg_pool.get_ref(), user.id.as_ref()) + .await + .map(|server| JsonResponse::build().set_list(server).ok("OK")) + .map_err(|_err| JsonResponse::::build().internal_server_error("")) +} diff --git a/src/routes/server/mod.rs b/src/routes/server/mod.rs new file mode 100644 index 0000000..8ef07d3 --- /dev/null +++ b/src/routes/server/mod.rs @@ -0,0 +1,9 @@ +pub mod add; +pub(crate) mod get; +pub(crate) mod delete; +pub(crate) mod update; + +// pub use get::*; +// pub use add::*; +// pub use update::*; +// pub use delete::*; diff --git a/src/routes/server/update.rs b/src/routes/server/update.rs new file mode 100644 index 0000000..52f0327 --- /dev/null +++ b/src/routes/server/update.rs @@ -0,0 +1,55 @@ +use crate::forms; +use crate::helpers::JsonResponse; +use crate::models; +use crate::db; +use actix_web::{web, web::Data, Responder, Result, put}; +use serde_valid::Validate; +use sqlx::PgPool; +use std::sync::Arc; +use tracing::Instrument; +use std::ops::Deref; + +#[tracing::instrument(name = "Update server.")] +#[put("/{id}")] +pub async fn item( + path: web::Path<(i32,)>, + form: web::Json, + user: web::ReqData>, + pg_pool: Data, +) -> Result { + + let id = path.0; + let server_row = db::server::fetch(pg_pool.get_ref(), id) + .await + .map_err(|err| JsonResponse::::build().internal_server_error(err)) + .and_then(|server| match server { + Some(server) if server.user_id != user.id => { + Err(JsonResponse::::build().bad_request("Server not found")) + } + Some(server) => Ok(server), + None => Err(JsonResponse::::build().not_found("Server not found")), + })?; + + if let Err(errors) = form.validate() { + return Err(JsonResponse::::build().form_error(errors.to_string())); + } + + let mut server:models::Server = form.deref().into(); + server.id = server_row.id; + server.project_id = server_row.project_id; + server.user_id = user.id.clone(); + + tracing::debug!("Updating server {:?}", server); + + db::server::update(pg_pool.get_ref(), server) + .await + .map(|server| { + JsonResponse::::build() + .set_item(server) + .ok("success") + }) + .map_err(|err| { + tracing::error!("Failed to execute query: {:?}", err); + JsonResponse::::build().internal_server_error("Could not update server") + }) +} diff --git a/src/routes/stack/add.rs b/src/routes/stack/add.rs deleted file mode 100644 index 27260e0..0000000 --- a/src/routes/stack/add.rs +++ /dev/null @@ -1,98 +0,0 @@ -use actix_web::{ - web, - web::{Bytes, Data}, - Responder, Result, -}; -use crate::forms::stack::StackForm; -use crate::helpers::JsonResponse; -use crate::models::user::User; -use actix_web::post; -use chrono::Utc; -use serde_json::Value; -use sqlx::PgPool; -use std::str; -use tracing::Instrument; -use uuid::Uuid; -use crate::models; - - -#[tracing::instrument(name = "Add stack.")] -#[post("")] -pub async fn add( - body: Bytes, - user: web::ReqData, - pool: Data, -) -> Result { - - let body_bytes = actix_web::body::to_bytes(body).await.unwrap(); - let body_str = str::from_utf8(&body_bytes).unwrap(); - let form = match serde_json::from_str::(body_str) { - Ok(f) => { - f - } - Err(_err) => { - let msg = format!("Invalid data. {:?}", _err); - return JsonResponse::::build().err("Invalid data".to_owned()); - } - }; - - 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.custom.project_name, - region=?&form.region, - domainList=?&form.domain_list - ); - // using `enter` is an async function - let _request_span_guard = request_span.enter(); // ->exit - - tracing::info!( - "request_id {} Adding '{}' '{}' as a new stack", - request_id, - form.custom.project_name, - form.region - ); - - let query_span = tracing::info_span!("Saving new stack details into the database"); - - let stack_name = form.custom.custom_stack_code.clone(); - let body: Value = match serde_json::to_value::(form) { - Ok(body) => body, - Err(err) => { - tracing::error!("request_id {} unwrap body {:?}", request_id, err); - serde_json::to_value::(StackForm::default()).unwrap() - } - }; - - return match sqlx::query!( - r#" - INSERT INTO user_stack (stack_id, user_id, name, body, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING id; - "#, - Uuid::new_v4(), - user_id, - stack_name, - body, - Utc::now(), - Utc::now(), - ) - .fetch_one(pool.get_ref()) - .instrument(query_span) - .await - { - Ok(record) => { - tracing::info!( - "req_id: {} New stack details have been saved to database", - request_id - ); - 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_owned()); - } - }; -} - diff --git a/src/routes/stack/compose.rs b/src/routes/stack/compose.rs deleted file mode 100644 index 4d19a3f..0000000 --- a/src/routes/stack/compose.rs +++ /dev/null @@ -1,119 +0,0 @@ -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 deleted file mode 100644 index 45c1b38..0000000 --- a/src/routes/stack/deploy.rs +++ /dev/null @@ -1,105 +0,0 @@ -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; - - -#[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 deleted file mode 100644 index 271d2dc..0000000 --- a/src/routes/stack/get.rs +++ /dev/null @@ -1,88 +0,0 @@ -use actix_web::{web, get, Responder, Result}; -use sqlx::PgPool; -use crate::helpers::{JsonResponse, JsonResponseBuilder}; -use crate::models; -use crate::models::user::User; -use std::convert::From; -use tracing::Instrument; - - -#[tracing::instrument(name = "Get logged user stack.")] -#[get("/{id}")] -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 {:?} gets stack by id {:?}", user.id, id); - match sqlx::query_as!( - models::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,); - return JsonResponse::build().set_item(Some(stack)).ok("OK".to_owned()); - } - Err(sqlx::Error::RowNotFound) => { - JsonResponse::build().not_found("Record not found".to_owned()) - } - Err(e) => { - tracing::error!("Failed to fetch stack, error: {:?}", e); - 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 deleted file mode 100644 index 27c8061..0000000 --- a/src/routes/stack/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -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::*; -pub use get::*; diff --git a/src/routes/stack/update.rs b/src/routes/stack/update.rs deleted file mode 100644 index 5a4fa0c..0000000 --- a/src/routes/stack/update.rs +++ /dev/null @@ -1,4 +0,0 @@ -use actix_web::HttpResponse; -pub async fn update() -> HttpResponse { - unimplemented!() -} diff --git a/src/routes/test/deploy.rs b/src/routes/test/deploy.rs index 945e293..4f36a3a 100644 --- a/src/routes/test/deploy.rs +++ b/src/routes/test/deploy.rs @@ -2,6 +2,7 @@ use crate::models::Client; use actix_web::{post, web, Responder, Result}; use serde::Serialize; use std::sync::Arc; +use crate::helpers::JsonResponse; #[derive(Serialize)] struct DeployResponse { @@ -9,12 +10,8 @@ struct DeployResponse { client: Arc, } -//todo inject client through enpoint's inputs #[tracing::instrument(name = "Test deploy.")] #[post("/deploy")] pub async fn handler(client: web::ReqData>) -> Result { - Ok(web::Json(DeployResponse { - status: "success".to_string(), - client: client.into_inner(), - })) + Ok(JsonResponse::build().set_item(client.into_inner()).ok("success")) } diff --git a/src/services/mod.rs b/src/services/mod.rs index 33c56f4..94b4efc 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,2 +1,2 @@ -mod stack; +pub mod project; mod rating; \ No newline at end of file diff --git a/src/services/project.rs b/src/services/project.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/services/rating.rs b/src/services/rating.rs index 22f4202..837be7b 100644 --- a/src/services/rating.rs +++ b/src/services/rating.rs @@ -1,8 +1,6 @@ -use crate::models::rating::Rating; -use sqlx::PgPool; -use reqwest::Url; -use tracing::Instrument; -use tracing_subscriber::fmt::format; +// use crate::models::rating::Rating; +// use tracing::Instrument; +// use tracing_subscriber::fmt::format; // impl Rating { // pub async fn filter_by(query_string: &str, pool: PgPool) -> Result<()> { diff --git a/src/startup.rs b/src/startup.rs index 3306fcd..22233db 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -1,74 +1,119 @@ use crate::configuration::Settings; +use crate::helpers; +use crate::routes; use actix_cors::Cors; -use actix_web::dev::Server; use actix_web::{ - web::{self}, - App, HttpServer, + dev::Server, + http, + error, + web, + App, + HttpServer, }; -use actix_web_httpauth::middleware::HttpAuthentication; +use crate::middleware; use sqlx::{Pool, Postgres}; use std::net::TcpListener; -use std::sync::Arc; use tracing_actix_web::TracingLogger; pub async fn run( listener: TcpListener, - db_pool: Pool, + pg_pool: Pool, settings: Settings, ) -> Result { - let settings = web::Data::new(Arc::new(settings)); - let db_pool = web::Data::new(db_pool); + let settings = web::Data::new(settings); + let pg_pool = web::Data::new(pg_pool); - // let address = format!("{}:{}", settings.app_host, settings.app_port); - // tracing::info!("Start server at {:?}", &address); - // let listener = std::net::TcpListener::bind(address) - // .expect(&format!("failed to bind to {}", settings.app_port)); + let mq_manager = helpers::MqManager::try_new(settings.amqp.connection_string())?; + let mq_manager = web::Data::new(mq_manager); + let authorization = middleware::authorization::try_new(settings.database.connection_string()).await?; + let json_config = web::JsonConfig::default() + .error_handler(|err, _req| { //todo + let msg: String = match err { + error::JsonPayloadError::Deserialize(err) => format!("{{\"kind\":\"deserialize\",\"line\":{}, \"column\":{}, \"msg\":\"{}\"}}", err.line(), err.column(), err), + _ => format!("{{\"kind\":\"other\",\"msg\":\"{}\"}}", err) + }; + error::InternalError::new(msg, http::StatusCode::BAD_REQUEST).into() + }); let server = HttpServer::new(move || { App::new() .wrap(TracingLogger::default()) - .service(web::scope("/health_check").service(crate::routes::health_check)) + .wrap(authorization.clone()) + .wrap(middleware::authentication::Manager::new()) + .wrap(Cors::permissive()) + .service( + web::scope("/health_check").service(routes::health_check) + ) .service( web::scope("/client") - .wrap(HttpAuthentication::bearer( - crate::middleware::trydirect::bearer_guard, - )) - .wrap(Cors::permissive()) - .service(crate::routes::client::add_handler) - .service(crate::routes::client::update_handler) - .service(crate::routes::client::enable_handler) - .service(crate::routes::client::disable_handler), + .service(routes::client::add_handler) + .service(routes::client::update_handler) + .service(routes::client::enable_handler) + .service(routes::client::disable_handler), ) .service( web::scope("/test") - .wrap(crate::middleware::client::Guard::new()) - .wrap(Cors::permissive()) - .service(crate::routes::test::deploy::handler), + .service(routes::test::deploy::handler) ) .service( web::scope("/rating") - .wrap(HttpAuthentication::bearer( - crate::middleware::trydirect::bearer_guard, - )) - .wrap(Cors::permissive()) - .service(crate::routes::rating::add_handler) - .service(crate::routes::rating::get_handler) - .service(crate::routes::rating::list_handler), + .service(routes::rating::anonymous_get_handler) + .service(routes::rating::anonymous_list_handler) + .service(routes::rating::user_add_handler) + .service(routes::rating::user_delete_handler) + .service(routes::rating::user_edit_handler), + ) + .service( + web::scope("/project") + .service(crate::routes::project::deploy::item) + .service(crate::routes::project::deploy::saved_item) + .service(crate::routes::project::compose::add) + .service(crate::routes::project::get::list) + .service(crate::routes::project::get::item) + .service(crate::routes::project::add::item) + .service(crate::routes::project::update::item) + .service(crate::routes::project::delete::item), + ) + .service( + web::scope("/admin") + .service( + web::scope("/rating") + .service(routes::rating::admin_get_handler) + .service(routes::rating::admin_list_handler) + .service(routes::rating::admin_edit_handler) + .service(routes::rating::admin_delete_handler) + ) + .service( + web::scope("/project") + .service(crate::routes::project::get::admin_list) + .service(crate::routes::project::compose::admin) + ) + .service( + web::scope("/client") + .service(routes::client::admin_enable_handler) + .service(routes::client::admin_update_handler) + .service(routes::client::admin_disable_handler), + ) + ) + .service( + web::scope("/cloud") + .service(crate::routes::cloud::get::item) + .service(crate::routes::cloud::get::list) + .service(crate::routes::cloud::add::add) + .service(crate::routes::cloud::update::item) + .service(crate::routes::cloud::delete::item), ) .service( - web::scope("/stack") - .wrap(HttpAuthentication::bearer( - crate::middleware::trydirect::bearer_guard, - )) - .wrap(Cors::permissive()) - .service(crate::routes::stack::deploy::add) - .service(crate::routes::stack::compose::add) - .service(crate::routes::stack::compose::admin) - .service(crate::routes::stack::get::item) - .service(crate::routes::stack::get::list) - .service(crate::routes::stack::add::add), + web::scope("/server") + .service(crate::routes::server::get::item) + .service(crate::routes::server::get::list) + // .service(crate::routes::server::add::add) + .service(crate::routes::server::update::item) + .service(crate::routes::server::delete::item), ) - .app_data(db_pool.clone()) + .app_data(json_config.clone()) + .app_data(pg_pool.clone()) + .app_data(mq_manager.clone()) .app_data(settings.clone()) }) .listen(listener)? diff --git a/src/telemetry.rs b/src/telemetry.rs index 724381a..fb57df1 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -1,4 +1,4 @@ -use tracing::subscriber::{self, set_global_default}; +use tracing::subscriber::set_global_default; use tracing::Subscriber; use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; use tracing_log::LogTracer; diff --git a/src/views/mod.rs b/src/views/mod.rs new file mode 100644 index 0000000..1795238 --- /dev/null +++ b/src/views/mod.rs @@ -0,0 +1 @@ +pub mod rating; diff --git a/src/views/rating/admin.rs b/src/views/rating/admin.rs new file mode 100644 index 0000000..0991dc6 --- /dev/null +++ b/src/views/rating/admin.rs @@ -0,0 +1,33 @@ +use crate::models; +use std::convert::From; +use chrono::{DateTime, Utc}; +use serde::{Serialize}; + +#[derive(Debug, Serialize, Default)] +pub struct Admin { + pub id: i32, + pub user_id: String, // external user_id, 100, taken using token (middleware?) + pub obj_id: i32, // id of the external object + pub category: models::RateCategory, // rating of product | rating of service etc + pub comment: Option, // always linked to a product + pub hidden: Option, // rating can be hidden for non-adequate user behaviour + pub rate: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl From for Admin { + fn from(rating: models::Rating) -> Self { + Self { + id: rating.id, + user_id: rating.user_id, + obj_id: rating.obj_id, + category: rating.category, + comment: rating.comment, + hidden: rating.hidden, + rate: rating.rate, + created_at: rating.created_at, + updated_at: rating.updated_at + } + } +} diff --git a/src/views/rating/anonymous.rs b/src/views/rating/anonymous.rs new file mode 100644 index 0000000..2871ab4 --- /dev/null +++ b/src/views/rating/anonymous.rs @@ -0,0 +1,27 @@ +use crate::models; +use std::convert::From; +use chrono::{DateTime, Utc}; +use serde::{Serialize}; + +#[derive(Debug, Serialize, Default)] +pub struct Anonymous { + pub id: i32, + pub user_id: String, // external user_id, 100, taken using token (middleware?) + pub obj_id: i32, // id of the external object + pub category: models::RateCategory, // rating of product | rating of service etc + pub comment: Option, // always linked to a product + pub rate: Option, +} + +impl From for Anonymous { + fn from(rating: models::Rating) -> Self { + Self { + id: rating.id, + user_id: rating.user_id, + obj_id: rating.obj_id, + category: rating.category, + comment: rating.comment, + rate: rating.rate, + } + } +} diff --git a/src/views/rating/mod.rs b/src/views/rating/mod.rs new file mode 100644 index 0000000..6474d91 --- /dev/null +++ b/src/views/rating/mod.rs @@ -0,0 +1,7 @@ +mod anonymous; +mod user; +mod admin; + +pub use anonymous::Anonymous as Anonymous; +pub use user::User as User; +pub use admin::Admin as Admin; diff --git a/src/views/rating/user.rs b/src/views/rating/user.rs new file mode 100644 index 0000000..901f6e5 --- /dev/null +++ b/src/views/rating/user.rs @@ -0,0 +1,31 @@ +use crate::models; +use std::convert::From; +use chrono::{DateTime, Utc}; +use serde::{Serialize}; + +#[derive(Debug, Serialize, Default)] +pub struct User { + pub id: i32, + pub user_id: String, // external user_id, 100, taken using token (middleware?) + pub obj_id: i32, // id of the external object + pub category: models::RateCategory, // rating of product | rating of service etc + pub comment: Option, // always linked to a product + pub rate: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl From for User { + fn from(rating: models::Rating) -> Self { + Self { + id: rating.id, + user_id: rating.user_id, + obj_id: rating.obj_id, + category: rating.category, + comment: rating.comment, + rate: rating.rate, + created_at: rating.created_at, + updated_at: rating.updated_at + } + } +} diff --git a/tests/cloud.rs b/tests/cloud.rs new file mode 100644 index 0000000..c3fd2d3 --- /dev/null +++ b/tests/cloud.rs @@ -0,0 +1,48 @@ +mod common; + +// test me: cargo t --test cloud -- --nocapture --show-output +#[tokio::test] +async fn list() { + + let app = common::spawn_app().await; // server + let client = reqwest::Client::new(); // client + + let response = client + .get(&format!("{}/cloud", &app.address)) + .send() + .await + .expect("Failed to execute request."); + + assert!(response.status().is_success()); + assert_eq!(Some(0), response.content_length()); +} + +// test me: cargo t --test cloud add_cloud -- --nocapture --show-output +#[tokio::test] +async fn add_cloud() { + + let app = common::spawn_app().await; // server + let client = reqwest::Client::new(); // client + + let data = r#" + { + "user_id": "fake_user_id", + "provider": "htz", + "cloud_token": "", + "cloud_key": "", + "cloud_secret": "", + "save_token": true + } + "#; + + let response = client + .post(&format!("{}/cloud", &app.address)) + .json(data) + .send() + .await + .expect("Failed to execute request."); + + println!("response: {}", response.status()); + assert!(response.status().is_success()); + assert_eq!(Some(0), response.content_length()); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..e5804ab --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,87 @@ +use actix_web::{get, web, App, HttpServer, Responder}; +use sqlx::{Connection, Executor, PgConnection, PgPool}; +use stacker::configuration::{get_configuration, DatabaseSettings, Settings}; +use stacker::forms; +use std::net::TcpListener; + +pub async fn spawn_app_with_configuration(mut configuration: Settings) -> TestApp { + let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port"); + + let port = listener.local_addr().unwrap().port(); + let address = format!("http://127.0.0.1:{}", port); + configuration.database.database_name = uuid::Uuid::new_v4().to_string(); + + let connection_pool = configure_database(&configuration.database).await; + + let server = stacker::startup::run(listener, connection_pool.clone(), configuration) + .await + .expect("Failed to bind address."); + + let _ = tokio::spawn(server); + println!("Used Port: {}", port); + + TestApp { + address, + db_pool: connection_pool, + } +} + +pub async fn spawn_app() -> TestApp { + let mut configuration = get_configuration().expect("Failed to get configuration"); + + let listener = std::net::TcpListener::bind("127.0.0.1:0") + .expect("Failed to bind port for testing auth server"); + + configuration.auth_url = format!( + "http://127.0.0.1:{}/me", + listener.local_addr().unwrap().port() + ); + println!("Auth Server is running on: {}", configuration.auth_url); + + let handle = tokio::spawn(mock_auth_server(listener)); + handle.await.expect("Auth Server can not be started"); + + spawn_app_with_configuration(configuration).await +} + +pub async fn configure_database(config: &DatabaseSettings) -> PgPool { + let mut connection = PgConnection::connect(&config.connection_string_without_db()) + .await + .expect("Failed to connect to postgres"); + + connection + .execute(format!(r#"CREATE DATABASE "{}""#, config.database_name).as_str()) + .await + .expect("Failed to create database"); + + let connection_pool = PgPool::connect(&config.connection_string()) + .await + .expect("Failed to connect to database pool"); + + sqlx::migrate!("./migrations") + .run(&connection_pool) + .await + .expect("Failed to migrate database"); + + connection_pool +} + +pub struct TestApp { + pub address: String, + pub db_pool: PgPool, +} + +#[get("")] +async fn mock_auth() -> actix_web::Result { + println!("Starting auth server in test mode ..."); + // 1. set user id + // 2. add token to header / hardcoded + Ok(web::Json(forms::user::UserForm::default())) +} + +async fn mock_auth_server(listener: TcpListener) -> actix_web::dev::Server { + HttpServer::new(|| App::new().service(web::scope("/me").service(mock_auth))) + .listen(listener) + .unwrap() + .run() +} diff --git a/tests/custom-stack-payload-2.json b/tests/custom-stack-payload-2.json deleted file mode 100644 index e64ec97..0000000 --- a/tests/custom-stack-payload-2.json +++ /dev/null @@ -1 +0,0 @@ -{"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":"mysampleproject","custom_stack_code":"smarty-bot","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-3.json b/tests/custom-stack-payload-3.json deleted file mode 100644 index 4008848..0000000 --- a/tests/custom-stack-payload-3.json +++ /dev/null @@ -1 +0,0 @@ -{"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":"mysampleproject","custom_stack_code":"another-bot","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-4.json b/tests/custom-stack-payload-4.json deleted file mode 100644 index 6f4bc7c..0000000 --- a/tests/custom-stack-payload-4.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 70bda71..0000000 --- a/tests/custom-stack-payload-5.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 61dedb9..0000000 --- a/tests/custom-stack-payload-6.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 1fbfbbd..0000000 --- a/tests/custom-stack-payload-7.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 7a55fc9..0000000 --- a/tests/custom-stack-payload-8.json +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 3bf473d..0000000 --- a/tests/custom-stack-payload-9.json +++ /dev/null @@ -1 +0,0 @@ -{"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/tests/custom-stack-payload-singleapp.json b/tests/custom-stack-payload-singleapp.json deleted file mode 100644 index e1b3998..0000000 --- a/tests/custom-stack-payload-singleapp.json +++ /dev/null @@ -1 +0,0 @@ -{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cx11","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":[],"save_token":false,"cloud_token":"nUDKdUk0b6fUOcW6I4zhmdMfhH8kR4nJrxWjRPxrfqTJ9smOSoKB4qZpsYjS8As6","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-free-periodically","custom":{"web":[{"name":"Smarty Bot","code":"smarty-bot","domain":"smartybot.com","sharedPorts":["8000"],"versions":[],"custom":true,"type":"web","main":true,"_id":"lmg1mg6c1acxn9bs7","dockerhub_user":"vsilent","dockerhub_name":"smarty"}],"feature":[],"service":[],"servers_count":3,"project_name":"sample1","custom_stack_code":"sample1"}} diff --git a/tests/custom-stack-payload.json b/tests/custom-stack-payload.json deleted file mode 100644 index a9ca754..0000000 --- a/tests/custom-stack-payload.json +++ /dev/null @@ -1,4 +0,0 @@ -{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cx21","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":["stack_migration","stack_health_monitoring","stack_security_monitoring"],"save_token":true,"cloud_token":"r6LAjqrynVt7pUwctVkzBlJmKjLOCxJIWjZFMLTkPYCCB4rsgphhEVhiL4DuO757","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"web":[{"name":"smarty database","code":"smarty-database","domain":"smarty-db.example.com","sharedPorts":["6532"],"versions":[],"custom":true,"type":"feature","main":true,"_id":"lm0gdh732y2qrojfl","dockerhub_user":"trydirect","dockerhub_name":"smarty-db","ram_size":"1Gb","cpu":1,"disk_size":"1Gb"}],"feature":[{"_etag":null,"_id":235,"_created":"2023-08-11T07:07:12.123355","_updated":"2023-08-15T13:07:30.597485","name":"Nginx Proxy Manager","code":"nginx_proxy_manager","role":["nginx_proxy_manager"],"type":"feature","default":null,"popularity":null,"descr":null,"ports":{"public":["80","81","443"]},"commercial":null,"subscription":null,"autodeploy":null,"suggested":null,"dependency":null,"avoid_render":null,"price":null,"icon":{"light":{"width":192,"height":192,"image":"205128e6-0303-4b62-b946-9810b61f3d04.png"},"dark":{}},"category_id":2,"parent_app_id":null,"full_description":null,"description":"

Nginx Proxy Manager is a user-friendly software application designed to effortlessly route traffic to your websites, whether they're hosted at home or elsewhere. It comes equipped with free SSL capabilities, eliminating the need for extensive Nginx or Letsencrypt knowledge. This tool proves especially handy for simplifying SSL generation and seamlessly proxying your docker containers.

","plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"1","ram_size":"1Gb","disk_size":"0.3Gb","dockerhub_image":"nginx-proxy-manager","versions":[{"_etag":"599","_id":599,"_created":"2023-08-11T10:23:33","_updated":"2023-08-11T10:23:34.420583","app_id":235,"name":"Nginx proxy manager","version":"2.10.4","update_status":"ready_for_testing","tag":"unstable"},{"_etag":"601","_id":601,"_created":null,"_updated":"2023-08-15T08:11:19.703882","app_id":235,"name":"Nginx proxy manager","version":"2.10.4","update_status":"published","tag":"stable"},{"_etag":null,"_id":600,"_created":null,"_updated":"2023-08-11T07:08:43.944998","app_id":235,"name":"Nginx proxy manager","version":"2.10.4","update_status":"ready_for_testing","tag":"latest"}],"domain":"","sharedPorts":["443"],"main":true}],"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":null,"ram_size":null,"disk_size":null,"dockerhub_image":"postgres","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":["5432"],"main":true}],"servers_count":3,"custom_stack_name":"SMBO","custom_stack_code":"sample-stack","custom_stack_git_url":"https://github.com/vsilent/smbo.git","custom_stack_category":["New","Marketing Automation"],"custom_stack_short_description":"Should be what is my project about shortly","custom_stack_description":"what is my project about more detailed","project_name":"sample stack","project_overview":"my short description, stack to marketplace, keep my token","project_description":"my full description, stack to marketplace, keep my token"}} - - - diff --git a/tests/dockerhub.rs b/tests/dockerhub.rs new file mode 100644 index 0000000..d0975bd --- /dev/null +++ b/tests/dockerhub.rs @@ -0,0 +1,143 @@ +// use std::fs; +// use std::collections::HashMap; +use std::env; +use docker_compose_types::{ComposeVolume, SingleValue}; + +mod common; +use stacker::forms::project::DockerImage; +// use stacker::helpers::project::dctypes::{ComposeVolume, SingleValue}; +use serde_yaml; +use stacker::forms::project::Volume; + +const DOCKER_USERNAME: &str = "trydirect"; +const DOCKER_PASSWORD: &str = "**********"; +// Unit Test + +// #[test] +// fn test_deserialize_project_web() { +// +// let body_str = fs::read_to_string("./tests/web-item.json").unwrap(); +// // let form:serde_json::Value = serde_json::from_str(&body_str).unwrap(); +// let form:App = serde_json::from_str(&body_str).unwrap(); +// println!("{:?}", form); +// // { +// // Ok(f) => { +// // f +// // } +// // Err(_err) => { +// // let msg = format!("Invalid data. {:?}", _err); +// // return JsonResponse::::build().bad_request(msg); +// // } +// // }; +// // +// // assert_eq!(result, 12); +// } +// #[test] +// fn test_deserialize_project() { +// +// let body_str = fs::read_to_string("./tests/custom-project-payload-11.json").unwrap(); +// let form = serde_json::from_str::(&body_str).unwrap(); +// println!("{:?}", form); +// // @todo assert required data +// +// // { +// // Ok(f) => { +// // f +// // } +// // Err(_err) => { +// // let msg = format!("Invalid data. {:?}", _err); +// // return JsonResponse::::build().bad_request(msg); +// // } +// // }; +// // +// // assert_eq!(result, 12); +// +// // let form:Environment = serde_json::from_str(&body_str).unwrap(); +// // let form:Vec> = serde_json::from_str(&body_str).unwrap(); +// // println!("{:?}", form); +// } + +#[tokio::test] +async fn test_docker_hub_successful_login() { + + common::spawn_app().await; // server + // let username = env::var("TEST_DOCKER_USERNAME") + // .expect("username environment variable is not set"); + // + // let password= env::var("TEST_DOCKER_PASSWORD") + // .expect("password environment variable is not set"); + let di = DockerImage { + dockerhub_user: Some(String::from("trydirect")), + dockerhub_name: Some(String::from("nginx-waf")), + dockerhub_image: None, + dockerhub_password: Some(String::from(DOCKER_PASSWORD)) + }; + assert_eq!(di.is_active().await.unwrap(), true); +} + +#[tokio::test] +async fn test_docker_private_exists() { + + common::spawn_app().await; // server + let di = DockerImage { + dockerhub_user: Some(String::from("trydirect")), + dockerhub_name: Some(String::from("nginx-waf")), + dockerhub_image: None, + dockerhub_password: Some(String::from(DOCKER_PASSWORD)) + }; + assert_eq!(di.is_active().await.unwrap(), true); +} + +#[tokio::test] +async fn test_public_repo_is_accessible() { + + common::spawn_app().await; // server + let di = DockerImage { + dockerhub_user: Some(String::from("")), + dockerhub_name: Some(String::from("nginx")), + dockerhub_image: None, + dockerhub_password: Some(String::from("")) + }; + assert_eq!(di.is_active().await.unwrap(), true); +} +#[tokio::test] +async fn test_docker_non_existent_repo() { + + common::spawn_app().await; // server + let di = DockerImage { + dockerhub_user: Some(String::from("trydirect")), //namespace + dockerhub_name: Some(String::from("nonexistent")), //repo + dockerhub_image: None, // namesps/reponame:tag full docker image string + dockerhub_password: Some(String::from("")) + }; + println!("{}", di.is_active().await.unwrap()); + assert_eq!(di.is_active().await.unwrap(), false); +} + +#[tokio::test] +async fn test_docker_non_existent_repo_empty_namespace() { + + common::spawn_app().await; // server + let di = DockerImage { + dockerhub_user: Some(String::from("")), //namespace + dockerhub_name: Some(String::from("nonexistent")), //repo + dockerhub_image: None, // namesps/reponame:tag full docker image string + dockerhub_password: Some(String::from("")) + }; + assert_eq!(di.is_active().await.unwrap(), true); +} + +#[tokio::test] +async fn test_docker_named_volume() { + let volume = Volume { + host_path: Some("flask-data".to_owned()), + container_path: Some("/var/www/flaskdata".to_owned()), + }; + + let cv:ComposeVolume = (&volume).into(); + println!("ComposeVolume: {:?}", cv); + println!("{:?}", cv.driver_opts); + assert_eq!(Some("flask-data".to_string()), cv.name); + assert_eq!(&Some(SingleValue::String("/root/project/flask-data".to_string())), cv.driver_opts.get("device").unwrap()); + assert_eq!(&Some(SingleValue::String("none".to_string())), cv.driver_opts.get("type").unwrap()); +} diff --git a/tests/health_check.rs b/tests/health_check.rs index 0b858bf..1496735 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -1,31 +1,4 @@ -//#[actix_rt::test] - -use std::net::TcpListener; -use actix_web::{App, HttpServer, web, Responder, get}; -use sqlx::{Connection, Executor, PgConnection, PgPool}; -use stacker::configuration::{get_configuration, DatabaseSettings}; -use stacker::forms; - - -#[get("")] -async fn mock_auth() -> actix_web::Result { - println!("Starting auth server in test mode ..."); - // 1. set user id - // 2. add token to header / hardcoded - Ok(web::Json(forms::user::UserForm::default())) -} - -async fn mock_auth_server(listener:TcpListener) -> actix_web::dev::Server { - - HttpServer::new(|| { - App::new() - .service(web::scope("/me") - .service(mock_auth)) - }) - .listen(listener) - .unwrap() - .run() -} +mod common; #[tokio::test] async fn health_check_works() { @@ -34,7 +7,7 @@ async fn health_check_works() { // 3. Assert println!("Before spawn_app"); - let app = spawn_app().await; // server + let app = common::spawn_app().await; // server println!("After spawn_app"); let client = reqwest::Client::new(); // client @@ -46,106 +19,4 @@ async fn health_check_works() { assert!(response.status().is_success()); assert_eq!(Some(0), response.content_length()); - // let app = App::new().service(web::resource("/health_check").route(web::get().to(health_check))); - // let mut app = test::init_service(app).await; - // let req = test::TestRequest::get().uri("/health_check").to_request(); - // let resp = test::call_service(&mut app, req).await; - // assert_eq!(resp.status(), StatusCode::OK); -} - -// test that locks main thread -// async fn spawn_app() -> std::io::Result<()> { -// stacker::run().await -// } - -pub struct TestApp { - pub address: String, - pub db_pool: PgPool, -} - -pub async fn configure_database(config: &DatabaseSettings) -> PgPool { - let mut connection = PgConnection::connect(&config.connection_string_without_db()) - .await - .expect("Failed to connect to postgres"); - - connection - .execute(format!(r#"CREATE DATABASE "{}""#, config.database_name).as_str()) - .await - .expect("Failed to create database"); - - let connection_pool = PgPool::connect(&config.connection_string()) - .await - .expect("Failed to connect to database pool"); - - sqlx::migrate!("./migrations") - .run(&connection_pool) - .await - .expect("Failed to migrate database"); - - connection_pool -} - - -// we have to run server in another thread -async fn spawn_app() -> TestApp { - // Future - - // let mut rt = tokio::runtime::Runtime::new().unwrap(); - // rt.spawn(mock_auth_server(listener)).expect("Could not spawn auth server"); - let mut configuration = get_configuration().expect("Failed to get configuration"); - - let listener = std::net::TcpListener::bind("127.0.0.1:0") - .expect("Failed to bind port for testing auth server"); - - configuration.auth_url = format!("http://127.0.0.1:{}/me", listener.local_addr().unwrap().port()); - println!("Auth Server is running on: {}", configuration.auth_url); - - let handle = tokio::spawn(mock_auth_server(listener)); - handle.await.expect("Auth Server can not be started"); - - let listener = std::net::TcpListener::bind("127.0.0.1:0") - .expect("Failed to bind random port"); - - let port = listener.local_addr().unwrap().port(); - let address = format!("http://127.0.0.1:{}", port); - configuration.database.database_name = uuid::Uuid::new_v4().to_string(); - - let connection_pool = configure_database(&configuration.database).await; - - let server = stacker::startup::run(listener, connection_pool.clone(), configuration) - .await.expect("Failed to bind address."); - - let _ = tokio::spawn(server); - println!("Used Port: {}", port); - - TestApp { - address, - db_pool: connection_pool, - } } - -#[tokio::test] -async fn add_rating_returns_a_200_for_valid_form_data() { - // Arrange - let app = spawn_app().await; - let client = reqwest::Client::new(); - - // let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; // %20 - space, %40 - @ - // let response = client - // .post(&format!("{}/subscriptions", &app.address)) - // .header("Content-Type", "application/x-www-form-urlencoded") - // .body(body) - // .send() - // .await - // .expect("Failed to execute request."); - // - // assert_eq!(200, response.status().as_u16()); - // - // let saved = sqlx::query!("SELECT email, name FROM subscriptions",) - // .fetch_one(&app.db_pool) - // .await - // .expect("Failed to fetch saved subscription."); - // - // assert_eq!(saved.email, "ursula_le_guin@gmail.com"); - // assert_eq!(saved.name, "le guin"); -} \ No newline at end of file diff --git a/tests/middleware_client.rs b/tests/middleware_client.rs new file mode 100644 index 0000000..8f2a9f5 --- /dev/null +++ b/tests/middleware_client.rs @@ -0,0 +1,26 @@ +mod common; + +#[tokio::test] +async fn middleware_client_works() { + // 1. Arrange + // 2. Act + // 3. Assert + + println!("Before spawn_app"); + let app = common::spawn_app().await; // server + println!("After spawn_app"); + let client = reqwest::Client::new(); // client + + let response = client + .get(&format!("{}/health_check", &app.address)) + .send() + .await + .expect("Failed to execute request."); + + assert!(response.status().is_success()); + assert_eq!(Some(0), response.content_length()); + + + //todo header stacker-id not found + // +} diff --git a/tests/middleware_trydirect.rs b/tests/middleware_trydirect.rs new file mode 100644 index 0000000..4937781 --- /dev/null +++ b/tests/middleware_trydirect.rs @@ -0,0 +1,25 @@ +mod common; +use wiremock::MockServer; + +#[tokio::test] +async fn middleware_trydirect_works() { + // 1. Arrange + let trydirect_auth_server = MockServer::start().await; + + // 2. Act + // 3. Assert + + println!("Before spawn_app"); + let app = common::spawn_app().await; // server + println!("After spawn_app"); + let client = reqwest::Client::new(); // client + + let response = client + .get(&format!("{}/health_check", &app.address)) + .send() + .await + .expect("Failed to execute request."); + + assert!(response.status().is_success()); + assert_eq!(Some(0), response.content_length()); +} diff --git a/tests/mock_data/README.md b/tests/mock_data/README.md new file mode 100644 index 0000000..4321e92 --- /dev/null +++ b/tests/mock_data/README.md @@ -0,0 +1,10 @@ +This directory contains mock data for different endpoints + +Here is some examples of making requests using CURL + +Add new cloud credentials +curl -X POST -v http://localhost:8000/cloud -d @cloud.json --header 'Content-Type: application/json' -H "Authorization: Bearer $TD_BEARER" + +Get cloud credentials +curl -X GET http://localhost:8000/cloud/31 --header 'Content-Type: application/json' -H "Authorization: Bearer $TD_BEARER" + diff --git a/tests/mock_data/app.json b/tests/mock_data/app.json new file mode 100644 index 0000000..0a37717 --- /dev/null +++ b/tests/mock_data/app.json @@ -0,0 +1,99 @@ +{ + "_etag": null, + "_id": 180, + "_created": "2021-12-17T08:11:40.875486", + "_updated": "2023-11-21T17:37:24.594545", + "name": "Airflow", + "code": "airflow", + "role": [ + "airflow" + ], + "type": "web", + "default": true, + "popularity": null, + "descr": null, + "ports": { + "private": [ + "31" + ] + }, + "commercial": null, + "subscription": null, + "autodeploy": null, + "suggested": null, + "dependency": null, + "avoid_render": null, + "price": null, + "icon": { + "light": { + "width": 150, + "height": 150, + "image": "7f41d873-2de5-4c6e-a037-42eeff572db6.svg" + }, + "dark": { + } + }, + "category_id": null, + "parent_app_id": null, + "full_description": null, + "description": "

Airflow description

", + "plan_type": null, + "ansible_var": null, + "repo_dir": null, + "cpu": "0.0", + "ram_size": null, + "disk_size": null, + "dockerhub_image": "airflow", + "form": null, + "category": [ + null + ], + "group": [ + ], + "versions": [ + { + "_id": 425, + "name": "Airflow", + "version": "2.7.3", + "update_status": "published", + "tag": "latest" + }, + { + "_id": 426, + "name": "Airflow", + "version": "2.7.1-unstable", + "update_status": "published", + "tag": "stable" + } + ], + "links": [ + ], + "domain": "latestports.com", + "shared_ports": [ + { + "host_port": "5000", + "container_port": "5000" + } + ], + "main": true, + "version": { + "_id": 426, + "name": "Airflow", + "version": "2.7.1-unstable", + "update_status": "published", + "tag": "stable" + }, + "environment": [ + { + "ENV_VAR1": "ENV_VAR1_VALUE" + } + ], + "network": "testnetwork", + "restart": "always", + "volumes": [ + { + "host_path": "/airflow/host/path/", + "container_path": "/airflow/container/path" + } + ] +} \ No newline at end of file diff --git a/tests/mock_data/cloud-update.json b/tests/mock_data/cloud-update.json new file mode 100644 index 0000000..72967ad --- /dev/null +++ b/tests/mock_data/cloud-update.json @@ -0,0 +1,9 @@ +{ + "user_id": "hy181TZa4DaabUZWklsrxw", + "project_id": 1, + "provider": "htz", + "cloud_token": "cloud_token_updates", + "cloud_key": "cloud_token_updates", + "cloud_secret": "cloud_secret_updates", + "save_token": false +} diff --git a/tests/mock_data/cloud.json b/tests/mock_data/cloud.json new file mode 100644 index 0000000..3865cd7 --- /dev/null +++ b/tests/mock_data/cloud.json @@ -0,0 +1,8 @@ +{ + "project_id": 1, + "provider": "htz", + "cloud_token": "cloud_token_here", + "cloud_key": "cloud_key_here", + "cloud_secret": "cloud_secret_here", + "save_token": true +} diff --git a/tests/mock_data/custom-stack-payload-no-networks.json b/tests/mock_data/custom-stack-payload-no-networks.json new file mode 100644 index 0000000..2f81246 --- /dev/null +++ b/tests/mock_data/custom-stack-payload-no-networks.json @@ -0,0 +1 @@ +{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cx11","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":[{"name":"MyApp","code":"myapp","domain":"myapp.com","shared_ports":[{"host_port":"","container_port":"8080"}],"versions":[],"custom":true,"type":"web","network":null,"restart":"no","timestamp":"2024-01-15T09:47:46.580Z","_id":"lreqrvv81ifrmng39","dockerhub_user":"","dockerhub_name":"nginx", "dockerhub_password":"", "dockerhub_image": "None", "environment":[{"key":"SOMEENVVAR1","value":"SOMEENVVAR_VALUE1"},{"key":"SOMEENVVAR2","value":"SOMEENVVAR_VALUE2"}],"volumes":[]}],"feature":[],"service":[],"servers_count":3,"project_git_url":"info@optimum-web.com","project_name":"Sample2024-1","custom_stack_code":"sample2024-1"}} \ No newline at end of file diff --git a/tests/mock_data/custom-stack-payload.json b/tests/mock_data/custom-stack-payload.json new file mode 100644 index 0000000..f602dbb --- /dev/null +++ b/tests/mock_data/custom-stack-payload.json @@ -0,0 +1 @@ +{"commonDomain":"","domainList":{},"region":"","zone":null,"server":"cx11","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":[],"form_app":[],"save_token":true,"disk_type":"pd-standart","cloud_token":"*******************","cloud_key":"*********","cloud_secret":"****************","disk_size":40,"provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-annually","custom":{"networks":[{"name":"default_network","id":"lsnd2ttg3ivhw3db5"}],"web":[{"_etag":null,"_id":"lsq2tdff3c0acf2lj","_created":"2023-04-28T09:46:19.470502","_updated":"2024-02-09T13:44:36.854036","name":"PostgREST","code":"postgrest","role":["postgrest"],"type":"web","default":false,"popularity":null,"descr":null,"ports":{"public":["3000","8080"]},"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":null,"plan_type":null,"ansible_var":null,"repo_dir":null,"cpu":"0.0","ram_size":null,"disk_size":null,"dockerhub_image":"","form":null,"requirements":null,"docker_image_is_internal":true,"custom_preset":{"volumes":[],"environment":[{"key":"PGRST_OPENAPI_SERVER_PROXY_URI","value":"http://DOMAIN_NAME:3000"},{"key":"PGRST_DB_ANON_ROLE","value":"anon"},{"key":"PGRST_DB_SCHEMA","value":"public"},{"key":"PGHOST","value":"postgresql_host"},{"key":"PGPORT","value":"5432"},{"key":"PGUSER","value":"user"},{"key":"PGDATABASE","value":"database"},{"key":"PGPASSWORD ","value":"password"}],"shared_ports":[{"host_port":"3000","container_port":"3000"}],"restart":"always"},"category":[null],"group":[],"versions":[{"_id":556,"name":"Postgrest","version":"latest","update_status":"published","tag":"latest"}],"links":[{"url":"https://postgrest.org","title":"Vendor","type":"vendor","follow":false},{"repo_owner":"PostgREST","repo_name":"postgrest","type":"github","follow":false}],"version":{"_id":556,"name":"Postgrest","version":"latest","update_status":"published","tag":"latest"},"network":["lsnd2ttg3ivhw3db5"],"restart":"always","timestamp":"2024-02-17T12:50:01.659Z","volumes":[],"environment":[{"key":"PGRST_OPENAPI_SERVER_PROXY_URI","value":"http://DOMAIN_NAME:3000"},{"key":"PGRST_DB_ANON_ROLE","value":"anon"},{"key":"PGRST_DB_SCHEMA","value":"public"},{"key":"PGHOST","value":"postgresql_host"},{"key":"PGPORT","value":"5432"},{"key":"PGUSER","value":"user"},{"key":"PGDATABASE","value":"database"},{"key":"PGPASSWORD ","value":"password"}],"shared_ports":[{"host_port":"3000","container_port":"3000"}],"domain":"","dockerhub_name":"mariadb:11.3.2-latest"}],"feature":[],"service":[],"servers_count":3,"project_name":"Databases","custom_stack_code":"databases"}} \ No newline at end of file diff --git a/tests/mock_data/custom.json b/tests/mock_data/custom.json new file mode 100644 index 0000000..952b1f9 --- /dev/null +++ b/tests/mock_data/custom.json @@ -0,0 +1 @@ +{"commonDomain":"","domainList":{},"region":"fsn1","zone":null,"server":"cx11","os":"ubuntu-20.04","ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":[],"form_app":[],"save_token":true,"disk_type":"pd-standart","cloud_token":"*********","provider":"htz","stack_code":"custom-stack","selected_plan":"plan-individual-monthly","custom":{"networks":[{"name":"default_network","id":"ls005v9r2xn6l3d2s"},{"name":"lkhlkjhlkjh","id":"ls025c5f1ld1u2tnn"},{"name":"samples","id":"ls007dio3aq1uh6ad"}],"web":[{"name":"samplecom","code":"samplecom","domain":"sample.com","shared_ports":[{"host_port":"80","container_port":"8080"}],"versions":[],"custom":true,"type":"web","network":["ls005v9r2xn6l3d2s","ls025c5f1ld1u2tnn","ls007dio3aq1uh6ad"],"restart":"always","timestamp":"2024-01-30T06:53:55.713Z","_id":"ls0063cx3h9cks1ue","dockerhub_name":"nginx"}],"feature":[],"service":[{"_etag":null,"_id":"ls086eg8szwxxcph","_created":"2020-06-19T13:07:24.228389","_updated":"2024-01-23T11:43:30.452364","name":"PostgreSQL","code":"postgres","role":["postgres"],"type":"service","default":null,"popularity":null,"descr":null,"ports":{"private":["5432"]},"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.0","ram_size":null,"disk_size":null,"dockerhub_image":"postgres","form":null,"requirements":null,"docker_image_is_internal":true,"category":[null],"group":[{"_etag":null,"_id":12,"_created":null,"_updated":null,"name":"Database","code":"database"}],"versions":[{"_id":458,"name":"15","version":"15","update_status":"published","tag":"15"},{"_id":288,"name":"10.22","version":"10.22","update_status":"published","tag":"10.22"},{"_id":303,"name":"13.8","version":"13.8","update_status":"published","tag":"13.8"},{"_id":266,"name":"11","version":"11","update_status":"published","tag":"11"},{"_id":267,"name":"12.12","version":"12.12","update_status":"published","tag":"12.12"},{"_id":38,"name":"14.5","version":"14.5","update_status":"published","tag":"14.5"},{"_id":596,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"}],"links":[],"shared_ports":[{"host_port":"","container_port":"5432"}],"version":{"_id":596,"name":"Postgres","version":"15.1","update_status":"published","tag":"15.1"},"network":["ls025c5f1ld1u2tnn","ls007dio3aq1uh6ad"],"restart":"always","timestamp":"2024-01-30T10:38:07.016Z","domain":""}],"servers_count":3,"project_name":"sampletest","custom_stack_code":"sampletest"}} \ No newline at end of file diff --git a/tests/mock_data/deploy.json b/tests/mock_data/deploy.json new file mode 100644 index 0000000..6917d31 --- /dev/null +++ b/tests/mock_data/deploy.json @@ -0,0 +1 @@ +{"project":{"networks":[{"id":"ltoi393j2i4iit2pz","name":"default_network"}],"web":[{"_created":"2023-05-10T09:57:23.773552","_etag":null,"_id":"ltoi3u3515z6nwsk1","_updated":"2024-02-27T15:10:40.107999","ansible_var":null,"autodeploy":null,"avoid_render":null,"category":[null],"category_id":null,"code":"openresty","commercial":null,"cpu":"0.0","custom_preset":{"dockerhub_name":"openresty","dockerhub_user":"openresty","environment":[],"restart":"always","shared_ports":[],"volumes":[]},"default":true,"dependency":null,"descr":null,"description":"

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

","disk_size":null,"docker_image_is_internal":true,"dockerhub_image":"openresty","dockerhub_name":"openresty","dockerhub_user":"openresty","domain":"","environment":[],"form":null,"full_description":null,"group":[],"icon":{"dark":{},"light":{"height":150,"image":"12140d93-350c-4fb8-b3d2-e350f3943b0b.svg","width":147}},"links":[{"follow":false,"title":"Openresty","type":"vendor","url":"https://openresty.org/"},{"follow":false,"repo_name":"openresty","repo_owner":"openresty","type":"github"}],"name":"OpenResty","network":["ltoi393j2i4iit2pz"],"parent_app_id":null,"plan_type":null,"popularity":null,"ports":{"public":["80","443"]},"price":null,"ram_size":null,"repo_dir":null,"requirements":null,"restart":"always","role":null,"shared_ports":[{"host_port":"80","container_port":"80"}],"subscription":null,"suggested":null,"timestamp":"2024-03-12T15:02:14.033Z","type":"web","version":{"_id":586,"name":"1.15.8.3","tag":"1.15.8.3","update_status":"published","version":"1.15.8.3"},"versions":[{"_id":586,"name":"1.15.8.3","tag":"1.15.8.3","update_status":"published","version":"1.15.8.3"}],"volumes":[]}],"feature":[],"service":[],"custom_stack_category":null,"custom_stack_code":"project-1","custom_stack_description":null,"custom_stack_short_description":null,"project_description":null,"project_git_url":null,"project_name":"Project 1","project_overview":null},"cloud":{"provider": "htz","save_token":true,"cloud_token":"*****"},"server":{"region":"fsn1","zone":null,"server":"cx11","os":"ubuntu-20.04","disk_type":"pd-standart","servers_count":3},"stack":{"commonDomain":"","domainList":{},"ssl":"letsencrypt","vars":[],"integrated_features":[],"extended_features":[],"subscriptions":[],"form_app":[]}} \ No newline at end of file diff --git a/tests/mock_data/deploy2.json b/tests/mock_data/deploy2.json new file mode 100644 index 0000000..4bf3857 --- /dev/null +++ b/tests/mock_data/deploy2.json @@ -0,0 +1 @@ +{"cloud":{"save_token":false,"cloud_token":"****","provider":"htz"},"server":{"region":"fsn1","zone":null,"server":"cx11","os":"ubuntu-20.04","disk_type":"pd-standart","servers_count":3},"stack":{"vars":[],"integrated_features":[],"extended_features":[],"subscriptions":[],"form_app":[]}} diff --git a/tests/mock_data/project-update.json b/tests/mock_data/project-update.json new file mode 100644 index 0000000..f13f68b --- /dev/null +++ b/tests/mock_data/project-update.json @@ -0,0 +1,7 @@ +{ + "id": 1, + "stack_id": "9239ea1d-8306-4493-aae1-fcc00de76241", + "user_id": "hy181TZa4DaabUZWklsrxw", + "name": "sample", + "body": "{\"key\": \"val\"}" +} \ No newline at end of file diff --git a/tests/mock_data/project.json b/tests/mock_data/project.json new file mode 100644 index 0000000..4c0e7c7 --- /dev/null +++ b/tests/mock_data/project.json @@ -0,0 +1,6 @@ +{ + "stack_id": "9239ea1d-8306-4493-aae1-fcc00de76241", + "user_id": "hy181TZa4DaabUZWklsrxw", + "name": "sample", + "body": "{}" +} \ No newline at end of file diff --git a/tests/mock_data/server-update-invalid.json b/tests/mock_data/server-update-invalid.json new file mode 100644 index 0000000..5110345 --- /dev/null +++ b/tests/mock_data/server-update-invalid.json @@ -0,0 +1,14 @@ +{ + "id": 1, + "user_id": "hy181TZa4DaabUZWklsrxw", + "cloud_id": 100000, + "region": "fra-1", + "zone": "a", + "server": "server-1", + "os": "3408230498203948234", + "disk_type": "samples", + "created_at": "", + "updated_at": "", + "project_id": 100000 +} + diff --git a/tests/mock_data/server-update.json b/tests/mock_data/server-update.json new file mode 100644 index 0000000..b85eb42 --- /dev/null +++ b/tests/mock_data/server-update.json @@ -0,0 +1,14 @@ +{ + "id": 1, + "user_id": "hy181TZa4DaabUZWklsrxw", + "project_id": 1, + "cloud_id": 1, + "region": "fra-1", + "zone": "a", + "server": "server-1", + "os": "3408230498203948234", + "disk_type": "samples", + "created_at": "", + "updated_at": "" +} + diff --git a/tests/mock_data/server.json b/tests/mock_data/server.json new file mode 100644 index 0000000..2d7d626 --- /dev/null +++ b/tests/mock_data/server.json @@ -0,0 +1,13 @@ +{ + "user_id": "hy181TZa4DaabUZWklsrxw", + "project_id":1, + "cloud_id": 1, + "region": "fra-1", + "zone": "a", + "server": "server-1", + "os": "3408230498203948234", + "disk_type": "samples", + "created_at": "", + "updated_at": "" +} + diff --git a/tests/mock_data/web-item.json b/tests/mock_data/web-item.json new file mode 100644 index 0000000..0a37717 --- /dev/null +++ b/tests/mock_data/web-item.json @@ -0,0 +1,99 @@ +{ + "_etag": null, + "_id": 180, + "_created": "2021-12-17T08:11:40.875486", + "_updated": "2023-11-21T17:37:24.594545", + "name": "Airflow", + "code": "airflow", + "role": [ + "airflow" + ], + "type": "web", + "default": true, + "popularity": null, + "descr": null, + "ports": { + "private": [ + "31" + ] + }, + "commercial": null, + "subscription": null, + "autodeploy": null, + "suggested": null, + "dependency": null, + "avoid_render": null, + "price": null, + "icon": { + "light": { + "width": 150, + "height": 150, + "image": "7f41d873-2de5-4c6e-a037-42eeff572db6.svg" + }, + "dark": { + } + }, + "category_id": null, + "parent_app_id": null, + "full_description": null, + "description": "

Airflow description

", + "plan_type": null, + "ansible_var": null, + "repo_dir": null, + "cpu": "0.0", + "ram_size": null, + "disk_size": null, + "dockerhub_image": "airflow", + "form": null, + "category": [ + null + ], + "group": [ + ], + "versions": [ + { + "_id": 425, + "name": "Airflow", + "version": "2.7.3", + "update_status": "published", + "tag": "latest" + }, + { + "_id": 426, + "name": "Airflow", + "version": "2.7.1-unstable", + "update_status": "published", + "tag": "stable" + } + ], + "links": [ + ], + "domain": "latestports.com", + "shared_ports": [ + { + "host_port": "5000", + "container_port": "5000" + } + ], + "main": true, + "version": { + "_id": 426, + "name": "Airflow", + "version": "2.7.1-unstable", + "update_status": "published", + "tag": "stable" + }, + "environment": [ + { + "ENV_VAR1": "ENV_VAR1_VALUE" + } + ], + "network": "testnetwork", + "restart": "always", + "volumes": [ + { + "host_path": "/airflow/host/path/", + "container_path": "/airflow/container/path" + } + ] +} \ No newline at end of file diff --git a/tests/model_project.rs b/tests/model_project.rs new file mode 100644 index 0000000..e5fd40d --- /dev/null +++ b/tests/model_project.rs @@ -0,0 +1,144 @@ +use stacker::forms::project::ProjectForm; +use stacker::forms::project::DockerImage; +use stacker::forms::project::App; +use std::fs; +use std::collections::HashMap; + +// Unit Test + +// #[test] +// fn test_deserialize_project_web() { +// +// let body_str = fs::read_to_string("./tests/web-item.json").unwrap(); +// // let form:serde_json::Value = serde_json::from_str(&body_str).unwrap(); +// let form:App = serde_json::from_str(&body_str).unwrap(); +// println!("{:?}", form); +// // { +// // Ok(f) => { +// // f +// // } +// // Err(_err) => { +// // let msg = format!("Invalid data. {:?}", _err); +// // return JsonResponse::::build().bad_request(msg); +// // } +// // }; +// // +// // assert_eq!(result, 12); +// } +#[test] +fn test_deserialize_project() { + + let body_str = fs::read_to_string("./tests/custom-project-payload-11.json").unwrap(); + let form = serde_json::from_str::(&body_str).unwrap(); + println!("{:?}", form); + // @todo assert required data + + // { + // Ok(f) => { + // f + // } + // Err(_err) => { + // let msg = format!("Invalid data. {:?}", _err); + // return JsonResponse::::build().bad_request(msg); + // } + // }; + // + // assert_eq!(result, 12); + + // let form:Environment = serde_json::from_str(&body_str).unwrap(); + + // let body_str = r#" + // [ + // { + // "ENV_VAR1": "ENV_VAR1_VALUE" + // }, + // { + // "ENV_VAR2": "ENV_VAR2_VALUE", + // "ENV_VAR3": "ENV_VAR3_VALUE" + // } + // ] + // "#; + // let form:Vec> = serde_json::from_str(&body_str).unwrap(); + // println!("{:?}", form); +} + +#[test] +fn test_docker_image_only_name_other_empty() { + let docker_image = DockerImage { + dockerhub_user: Some("".to_string()), + dockerhub_name: Some("mysql".to_string()), + dockerhub_image: Some("".to_string(),), + dockerhub_password: None, + }; + let output = docker_image.to_string(); + assert_eq!(String::from("mysql:latest"), output); +} + +#[test] +fn test_docker_image_only_name_other_none() { + let docker_image = DockerImage { + dockerhub_user: None, + dockerhub_name: Some("mysql".to_string()), + dockerhub_image: None, + dockerhub_password: None, + }; + let output = docker_image.to_string(); + assert_eq!(String::from("mysql:latest"), output); +} +#[test] +fn test_docker_image_namespace_and_repo() { + let docker_image = DockerImage { + dockerhub_user: Some("trydirect".to_string()), + dockerhub_name: Some("mysql".to_string()), + dockerhub_image: Some("".to_string(),), + dockerhub_password: None, + }; + let output = docker_image.to_string(); + assert_eq!(String::from("trydirect/mysql:latest"), output); +} + +#[test] +fn test_docker_image_namespace_and_repo_tag() { + let docker_image = DockerImage { + dockerhub_user: Some("trydirect".to_string()), + dockerhub_name: Some("mysql:8.1".to_string()), + dockerhub_image: Some("".to_string(),), + dockerhub_password: None, + }; + let output = docker_image.to_string(); + assert_eq!(String::from("trydirect/mysql:8.1"), output); +} +#[test] +fn test_docker_image_only_image() { + let docker_image = DockerImage { + dockerhub_user: None, + dockerhub_name: None, + dockerhub_image: Some("trydirect/mysql:stable".to_string(),), + dockerhub_password: None, + }; + let output = docker_image.to_string(); + assert_eq!(String::from("trydirect/mysql:stable"), output); +} + +#[test] +fn test_docker_image_only_image_other_empty() { + let docker_image = DockerImage { + dockerhub_user: Some("".to_string()), + dockerhub_name: Some("".to_string()), + dockerhub_image: Some("trydirect/mysql:stable".to_string()), + dockerhub_password: None, + }; + let output = docker_image.to_string(); + assert_eq!(String::from("trydirect/mysql:stable"), output); +} +#[test] +fn test_docker_repo_name_with_tag_other_none() { + let docker_image = DockerImage { + dockerhub_user: None, + dockerhub_name: Some("mysql:stable".to_string()), + dockerhub_image: None, + dockerhub_password: None, + }; + let output = docker_image.to_string(); + assert_eq!(String::from("mysql:stable"), output); +}