From 8ba90924d77f74cb1a16fc7094da1627c04e204c Mon Sep 17 00:00:00 2001 From: samuchila Date: Wed, 7 Feb 2024 15:01:10 +0100 Subject: [PATCH] Replace ShipLift crate with Bollard Maintenance of ShipLift crate has been stopped and the latest docker engine version 25 is incompatible with the crate. This will be replaced with Bollard that is also an official docker SDK for rust. Additional improvements to efficiency added. --- api/Cargo.lock | 406 ++++----- api/Cargo.toml | 9 +- api/src/infrastructure/docker.rs | 1315 ++++++++++++++++++------------ api/src/tickets.rs | 2 +- 4 files changed, 990 insertions(+), 742 deletions(-) diff --git a/api/Cargo.lock b/api/Cargo.lock index 4331ec3a..4b06f79e 100644 --- a/api/Cargo.lock +++ b/api/Cargo.lock @@ -142,7 +142,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -153,7 +153,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -246,7 +246,7 @@ dependencies = [ "bitflags 2.5.0", "boa_interner", "boa_macros", - "indexmap", + "indexmap 2.2.6", "num-bigint", "rustc-hash", ] @@ -269,9 +269,9 @@ dependencies = [ "cfg-if", "dashmap", "fast-float", - "hashbrown", + "hashbrown 0.14.3", "icu_normalizer", - "indexmap", + "indexmap 2.2.6", "intrusive-collections", "itertools", "num-bigint", @@ -304,7 +304,7 @@ checksum = "c055ef3cd87ea7db014779195bc90c6adfc35de4902e3b2fe587adecbd384578" dependencies = [ "boa_macros", "boa_profiler", - "hashbrown", + "hashbrown 0.14.3", "thin-vec", ] @@ -316,8 +316,8 @@ checksum = "0cacc9caf022d92195c827a3e5bf83f96089d4bfaff834b359ac7b6be46e9187" dependencies = [ "boa_gc", "boa_macros", - "hashbrown", - "indexmap", + "hashbrown 0.14.3", + "indexmap 2.2.6", "once_cell", "phf", "rustc-hash", @@ -332,7 +332,7 @@ checksum = "6be9c93793b60dac381af475b98634d4b451e28336e72218cad9a20176218dbc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "synstructure 0.13.1", ] @@ -361,6 +361,52 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0d8372f2d5cbac600a260de87877141b42da1e18d2c7a08ccb493a49cbd55c0" +[[package]] +name = "bollard" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aed08d3adb6ebe0eff737115056652670ae290f177759aac19c30456135f94c" +dependencies = [ + "base64 0.22.0", + "bollard-stubs", + "bytes", + "chrono", + "futures-core", + "futures-util", + "hex", + "http 1.1.0", + "http-body-util", + "hyper 1.3.1", + "hyper-named-pipe", + "hyper-util", + "hyperlocal-next", + "log", + "pin-project-lite", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.44.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709d9aa1c37abb89d40f19f5d0ad6f0d88cb1581264e571c9350fc5bb89cf1c5" +dependencies = [ + "chrono", + "serde", + "serde_repr", + "serde_with", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -384,21 +430,9 @@ checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" - [[package]] name = "bytes" version = "1.6.0" @@ -416,9 +450,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" [[package]] name = "cfg-if" @@ -472,7 +506,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -523,15 +557,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc32fast" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" -dependencies = [ - "cfg-if", -] - [[package]] name = "crypto-common" version = "0.1.6" @@ -563,7 +588,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -574,7 +599,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -584,7 +609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.3", "lock_api", "once_cell", "parking_lot_core", @@ -597,6 +622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -629,7 +655,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -651,7 +677,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -761,9 +787,9 @@ checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "figment" -version = "0.10.17" +version = "0.10.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "752eb150770d6f51eb24d60e3ff84a2c24ccc5e5b3b0f550917ce5ec77c13fe4" +checksum = "d032832d74006f99547004d49410a4b4218e4c33382d56ca3ff89df74f86b953" dependencies = [ "atomic 0.6.0", "parking_lot", @@ -787,16 +813,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "flate2" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "fnv" version = "1.0.7" @@ -889,7 +905,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -923,18 +939,6 @@ dependencies = [ "slab", ] -[[package]] -name = "futures_codec" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce54d63f8b0c75023ed920d46fd71d0cbbb830b0ee012726b5b4f506fb6dea5b" -dependencies = [ - "bytes 0.5.6", - "futures 0.3.30", - "memchr", - "pin-project 0.4.30", -] - [[package]] name = "generator" version = "0.7.5" @@ -987,13 +991,13 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ - "bytes 1.6.0", + "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1006,13 +1010,13 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" dependencies = [ - "bytes 1.6.0", + "bytes", "fnv", "futures-core", "futures-sink", "futures-util", "http 1.1.0", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1039,6 +1043,12 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98f494b2060b2a8f5e63379e1e487258e014cee1b1725a735816c0107a2e9d93" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.3" @@ -1091,7 +1101,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.6.0", + "bytes", "fnv", "itoa", ] @@ -1102,7 +1112,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ - "bytes 1.6.0", + "bytes", "fnv", "itoa", ] @@ -1133,7 +1143,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes 1.6.0", + "bytes", "http 0.2.12", "pin-project-lite", ] @@ -1144,7 +1154,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ - "bytes 1.6.0", + "bytes", "http 1.1.0", ] @@ -1154,7 +1164,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ - "bytes 1.6.0", + "bytes", "futures-core", "http 1.1.0", "http-body 1.0.0", @@ -1185,7 +1195,7 @@ version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ - "bytes 1.6.0", + "bytes", "futures-channel", "futures-core", "futures-util", @@ -1209,7 +1219,7 @@ version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ - "bytes 1.6.0", + "bytes", "futures-channel", "futures-util", "h2 0.4.4", @@ -1223,6 +1233,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper 1.3.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + [[package]] name = "hyper-rustls" version = "0.27.1" @@ -1261,7 +1286,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.6.0", + "bytes", "hyper 0.14.28", "native-tls", "tokio", @@ -1274,7 +1299,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ - "bytes 1.6.0", + "bytes", "http-body-util", "hyper 1.3.1", "hyper-util", @@ -1290,7 +1315,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ - "bytes 1.6.0", + "bytes", "futures-channel", "futures-util", "http 1.1.0", @@ -1305,16 +1330,18 @@ dependencies = [ ] [[package]] -name = "hyperlocal" -version = "0.8.0" +name = "hyperlocal-next" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fafdf7b2b2de7c9784f76e02c0935e65a8117ec3b768644379983ab333ac98c" +checksum = "acf569d43fa9848e510358c07b80f4adf34084ddc28c6a4a651ee8474c070dcc" dependencies = [ - "futures-util", "hex", - "hyper 0.14.28", - "pin-project 1.1.5", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "pin-project-lite", "tokio", + "tower-service", ] [[package]] @@ -1455,7 +1482,7 @@ checksum = "d2abdd3a62551e8337af119c5899e600ca0c88ec8f23a46c60ba216c803dcf1a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1474,6 +1501,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -1481,7 +1519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", "serde", ] @@ -1617,7 +1655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0708306b5c0085f249f5e3d2d56a9bbfe0cbbf4fd4eb9ed4bbba542ba7649a7" dependencies = [ "base64 0.22.0", - "bytes 1.6.0", + "bytes", "chrono", "either", "futures 0.3.30", @@ -1673,7 +1711,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1787,7 +1825,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" dependencies = [ - "bytes 1.6.0", + "bytes", "encoding_rs", "futures-util", "http 0.2.12", @@ -1902,7 +1940,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1929,7 +1967,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b95a2c51531af0cb93761f66094044ca6ea879320bccd35ab747ff3fcab3f422" dependencies = [ - "bytes 1.6.0", + "bytes", "chrono", "futures-util", "http 1.1.0", @@ -1938,7 +1976,7 @@ dependencies = [ "lazy_static", "olpc-cjson", "regex", - "reqwest 0.12.3", + "reqwest 0.12.4", "serde", "serde_json", "sha2", @@ -1988,7 +2026,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2073,7 +2111,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2123,7 +2161,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2167,7 +2205,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2179,33 +2217,13 @@ dependencies = [ "siphasher", ] -[[package]] -name = "pin-project" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef0f924a5ee7ea9cbcea77529dba45f8a9ba9f622419fe3386ca581a3ae9d5a" -dependencies = [ - "pin-project-internal 0.4.30", -] - [[package]] name = "pin-project" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ - "pin-project-internal 1.1.5", -] - -[[package]] -name = "pin-project-internal" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "851c8d0ce9bebe43790dedfc86614c23494ac9f423dd618d3a61fc693eafe61e" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "pin-project-internal", ] [[package]] @@ -2216,7 +2234,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2270,6 +2288,7 @@ dependencies = [ "async-trait", "base64 0.22.0", "boa_engine", + "bollard", "bytesize", "chrono", "clap", @@ -2291,7 +2310,7 @@ dependencies = [ "pest_derive", "regex", "regex-syntax 0.8.3", - "reqwest 0.12.3", + "reqwest 0.12.4", "rocket", "schemars", "secstr", @@ -2302,7 +2321,7 @@ dependencies = [ "serde_regex", "serde_yaml", "sha2", - "shiplift", + "tar", "tempfile", "tokio", "toml", @@ -2337,7 +2356,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "version_check", "yansi", ] @@ -2407,7 +2426,7 @@ checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2460,7 +2479,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eae2a1ebfecc58aff952ef8ccd364329abe627762f5bf09ff42eb9d98522479" dependencies = [ - "hashbrown", + "hashbrown 0.14.3", "memchr", ] @@ -2471,7 +2490,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.7", - "bytes 1.6.0", + "bytes", "encoding_rs", "futures-core", "futures-util", @@ -2506,12 +2525,12 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ "base64 0.22.0", - "bytes 1.6.0", + "bytes", "encoding_rs", "futures-core", "futures-util", @@ -2573,11 +2592,11 @@ dependencies = [ "async-trait", "atomic 0.5.3", "binascii", - "bytes 1.6.0", + "bytes", "either", "figment", "futures 0.3.30", - "indexmap", + "indexmap 2.2.6", "log", "memchr", "multer", @@ -2609,11 +2628,11 @@ checksum = "a2238066abf75f21be6cd7dc1a09d5414a671f4246e384e49fe3f8a4936bd04c" dependencies = [ "devise", "glob", - "indexmap", + "indexmap 2.2.6", "proc-macro2", "quote", "rocket_http", - "syn 2.0.59", + "syn 2.0.60", "unicode-xid", "version_check", ] @@ -2629,7 +2648,7 @@ dependencies = [ "futures 0.3.30", "http 0.2.12", "hyper 0.14.28", - "indexmap", + "indexmap 2.2.6", "log", "memchr", "pear", @@ -2659,9 +2678,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -2672,9 +2691,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.4" +version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c4d6d8ad9f2492485e13453acbb291dd08f64441b6609c491f1c2cd2c6b4fe1" +checksum = "afabcee0551bd1aa3e18e5adbf2c0544722014b899adb31bd186ec638d3da97e" dependencies = [ "log", "once_cell", @@ -2725,9 +2744,9 @@ checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring", "rustls-pki-types", @@ -2867,7 +2886,7 @@ checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2902,6 +2921,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "serde_spanned" version = "0.6.5" @@ -2923,13 +2953,30 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" +dependencies = [ + "base64 0.21.7", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "time", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -2956,36 +3003,11 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shiplift" -version = "0.7.0" -source = "git+https://github.com/softprops/shiplift.git?rev=3a7c1dc3ae388b6a9f0a8f724fabff30953bcc5b#3a7c1dc3ae388b6a9f0a8f724fabff30953bcc5b" -dependencies = [ - "base64 0.13.1", - "byteorder", - "bytes 1.6.0", - "chrono", - "flate2", - "futures-util", - "futures_codec", - "hyper 0.14.28", - "hyperlocal", - "log", - "mime", - "paste", - "pin-project 1.1.5", - "serde", - "serde_json", - "tar", - "tokio", - "url", -] - [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3094,9 +3116,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.59" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -3129,7 +3151,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -3190,22 +3212,22 @@ checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -3284,7 +3306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", - "bytes 1.6.0", + "bytes", "libc", "mio", "num_cpus", @@ -3303,7 +3325,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -3344,7 +3366,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ - "bytes 1.6.0", + "bytes", "futures-core", "futures-sink", "pin-project-lite", @@ -3361,7 +3383,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.9", + "toml_edit 0.22.12", ] [[package]] @@ -3379,18 +3401,18 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ - "indexmap", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -3405,7 +3427,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "pin-project 1.1.5", + "pin-project", "pin-project-lite", "tokio", "tokio-util", @@ -3422,7 +3444,7 @@ checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "base64 0.21.7", "bitflags 2.5.0", - "bytes 1.6.0", + "bytes", "http 1.1.0", "http-body 1.0.0", "http-body-util", @@ -3465,7 +3487,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -3686,7 +3708,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "wasm-bindgen-shared", ] @@ -3720,7 +3742,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4023,7 +4045,7 @@ checksum = "9e6936f0cce458098a201c245a11bef556c6a0181129c7034d10d76d1ec3a2b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "synstructure 0.13.1", ] @@ -4044,7 +4066,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -4064,7 +4086,7 @@ checksum = "e6a647510471d372f2e6c2e6b7219e44d8c574d24fdc11c610a61455782f18c3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "synstructure 0.13.1", ] @@ -4093,5 +4115,5 @@ checksum = "7b4e5997cbf58990550ef1f0e5124a05e47e1ebd33a84af25739be6031a62c20" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] diff --git a/api/Cargo.toml b/api/Cargo.toml index ef63e940..aede42e7 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -16,6 +16,7 @@ async-trait = "0.1" async-stream = "0.3" base64 = "0.22" boa_engine = "0.18" +bollard = { version = "0.16", features = ["chrono"] } bytesize = { version = "1.3", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.4", features = ["derive", "cargo", "help", "usage", "error-context"] } @@ -47,19 +48,13 @@ serde_derive = "1.0" serde_json = "1.0" serde_regex = "1.1" serde_yaml = "0.9" +tar = "0.4" tokio = { version = "1.29", features = ["macros", "rt", "rt-multi-thread", "sync", "time"] } toml = "0.8" url = { version = "2.4", features = ["serde"] } uuid = { version = "1.5", features = ["serde", "v4"] } yansi = "1.0" - -[dependencies.shiplift] -git = "https://github.com/softprops/shiplift.git" -rev = "3a7c1dc3ae388b6a9f0a8f724fabff30953bcc5b" -default-features = false -features = ["unix-socket", "chrono"] - [dev-dependencies] assert-json-diff = "2.0" figment = { version = "0.10", features = ["test"] } diff --git a/api/src/infrastructure/docker.rs b/api/src/infrastructure/docker.rs index 1fcbd3c4..4eeb8797 100644 --- a/api/src/infrastructure/docker.rs +++ b/api/src/infrastructure/docker.rs @@ -37,21 +37,31 @@ use crate::models::{ }; use async_stream::stream; use async_trait::async_trait; +use bollard::auth::DockerCredentials; +use bollard::container::{ + CreateContainerOptions, ListContainersOptions, LogOutput, StartContainerOptions, + UploadToContainerOptions, +}; +use bollard::errors::Error as BollardError; +use bollard::image::CreateImageOptions; +use bollard::network::{ + ConnectNetworkOptions, CreateNetworkOptions, DisconnectNetworkOptions, ListNetworksOptions, +}; +use bollard::service::{ + ContainerCreateResponse, ContainerInspectResponse, ContainerStateStatusEnum, ContainerSummary, + CreateImageInfo, EndpointSettings, HostConfig, RestartPolicy, RestartPolicyNameEnum, + VolumeListResponse, +}; +use bollard::volume::{CreateVolumeOptions, ListVolumesOptions}; +use bollard::Docker; use chrono::{DateTime, FixedOffset}; use failure::{format_err, Error}; -use futures::future::join_all; use futures::stream::BoxStream; +use futures::stream::FuturesUnordered; use futures::{StreamExt, TryStreamExt}; use multimap::MultiMap; use regex::Regex; -use shiplift::container::{ContainerCreateInfo, ContainerDetails, ContainerInfo}; -use shiplift::errors::Error as ShipLiftError; -use shiplift::tty::TtyChunk; -use shiplift::volume::VolumeInfo; -use shiplift::{ - ContainerConnectionOptions, ContainerFilter, ContainerListOptions, ContainerOptions, Docker, - LogsOptions, NetworkCreateOptions, PullOptions, RegistryAuth, VolumeCreateOptions, -}; +use rocket::form::validate::Contains; use std::collections::HashMap; use std::convert::{From, TryFrom}; use std::net::{AddrParseError, IpAddr}; @@ -85,6 +95,10 @@ pub enum DockerInfrastructureError { UnknownServiceType { unknown_label: String }, #[fail(display = "Unexpected container address: {}", internal_message)] InvalidContainerAddress { internal_message: String }, + #[fail(display = "Unexpected state for container: {}", container_id)] + InvalidContainerState { container_id: String }, + #[fail(display = "Unexpected image details for container: {}", container_id)] + InvalidContainerImage { container_id: String }, } impl DockerInfrastructure { @@ -95,7 +109,7 @@ impl DockerInfrastructure { async fn find_status_change_container( &self, status_id: &str, - ) -> Result, ShipLiftError> { + ) -> Result, BollardError> { self.get_status_change_containers(None, Some(status_id)) .await .map(|list| list.into_iter().next()) @@ -105,7 +119,7 @@ impl DockerInfrastructure { &self, status_id: &str, app_name: &AppName, - ) -> Result { + ) -> Result { let existing_task = self .get_status_change_containers(Some(app_name), None) .await? @@ -115,8 +129,7 @@ impl DockerInfrastructure { if let Some(existing_task) = existing_task { // TODO: what to if there is already a deployment return Err(format_err!( - "There is already an operation in progress: {:?}", - existing_task + "There is already an operation in progress: {existing_task:?}" )); } @@ -128,130 +141,144 @@ impl DockerInfrastructure { labels.insert(APP_NAME_LABEL, app_name); labels.insert(STATUS_ID, status_id); - let mut options = ContainerOptions::builder(&image.to_string()); - options.labels(&labels); - - let docker = Docker::new(); - let containers = docker.containers(); + let docker = Docker::connect_with_socket_defaults()?; - trace!( - "Create deployment task container {} for {}", - status_id, - app_name - ); - let container_info = containers.create(&options.build()).await; - let ci = container_info?; + trace!("Create deployment task container {status_id} for {app_name}"); - let container = containers.get(&ci.id); - Ok(container.inspect().await?) + let container_info = docker + .create_container( + None::>, + bollard::container::Config::<&str> { + image: Some("docker.io/library/busybox:stable"), + labels: Some(labels), + ..Default::default() + }, + ) + .await?; + Ok(docker.inspect_container(&container_info.id, None).await?) } - async fn create_or_get_network_id(&self, app_name: &String) -> Result { - trace!("Resolve network id for {}", app_name); + async fn create_or_get_network_id(&self, app_name: &AppName) -> Result { + trace!("Resolve network id for {app_name}"); - let network_name = format!("{}-net", app_name); + let network_name = format!("{app_name}-net"); - let docker = Docker::new(); + let docker = Docker::connect_with_socket_defaults()?; let network_id = docker - .networks() - .list(&Default::default()) + .list_networks(None::>) .await? - .iter() - .find(|n| n.name == network_name) - .map(|n| n.id.clone()); + .into_iter() + .find(|n| n.name.as_ref() == Some(&network_name)) + .and_then(|n| n.id); if let Some(n) = network_id { return Ok(n); } - debug!("Creating network for app {}.", app_name); + debug!("Creating network for app {app_name}."); let network_create_info = docker - .networks() - .create(&NetworkCreateOptions::builder(network_name.as_ref()).build()) + .create_network(CreateNetworkOptions::<&str> { + name: &network_name, + ..Default::default() + }) .await?; + let network_id = network_create_info + .id + .expect("id is mandatory for a Docker Network."); - debug!( - "Created network for app {} with id {}", - app_name, network_create_info.id - ); + debug!("Created network for app {app_name} with id {network_id}"); - Ok(network_create_info.id) + Ok(network_id) } - async fn connect_traefik(&self, network_id: &String) -> Result<(), ShipLiftError> { - let docker = Docker::new(); + async fn connect_traefik(&self, network_id: &str) -> Result<(), BollardError> { + let docker = Docker::connect_with_socket_defaults()?; let containers = docker - .containers() - .list(&ContainerListOptions::builder().build()) + .list_containers(None::>) .await?; + let traefik_container_id = containers .into_iter() - .find(|c| c.image.contains("traefik")) - .map(|c| c.id); + .find(|c| c.image.as_ref().map_or(false, |s| s.contains("traefik"))) + .and_then(|c| c.id); if let Some(id) = traefik_container_id { if let Err(e) = docker - .networks() - .get(network_id) - .connect(&ContainerConnectionOptions::builder(&id).build()) + .connect_network( + network_id, + ConnectNetworkOptions::<&str> { + container: &id, + ..Default::default() + }, + ) .await { - debug!("Cannot traefik: {}", e); + debug!("Cannot traefik: {e}"); } } Ok(()) } - async fn disconnect_traefik(&self, network_id: &String) -> Result<(), ShipLiftError> { - let docker = Docker::new(); - + async fn disconnect_traefik(&self, network_id: &str) -> Result<(), BollardError> { + let docker = Docker::connect_with_socket_defaults()?; let containers = docker - .containers() - .list(&ContainerListOptions::builder().build()) + .list_containers(None::>) .await?; let traefik_container_id = containers .into_iter() - .find(|c| c.image.contains("traefik")) - .map(|c| c.id); + .find(|c| c.image.as_ref().map_or(false, |s| s.contains("traefik"))) + .and_then(|c| c.id); if let Some(id) = traefik_container_id { docker - .networks() - .get(network_id) - .disconnect(&ContainerConnectionOptions::builder(&id).build()) - .await?; + .disconnect_network( + network_id, + DisconnectNetworkOptions::<&str> { + container: &id, + ..Default::default() + }, + ) + .await? } Ok(()) } - async fn delete_network(&self, app_name: &String) -> Result<(), ShipLiftError> { - let network_name = format!("{}-net", app_name); + async fn delete_network(&self, app_name: &AppName) -> Result<(), BollardError> { + let network_name = format!("{app_name}-net"); + + let docker = Docker::connect_with_socket_defaults()?; - let docker = Docker::new(); for n in docker - .networks() - .list(&Default::default()) + .list_networks(Some(ListNetworksOptions::<&str> { + filters: HashMap::from([("name", vec![network_name.as_str()])]), + })) .await? - .iter() - .filter(|n| n.name == network_name) { - self.disconnect_traefik(&n.id).await?; - docker.networks().get(&n.id).delete().await?; + let network_id = + n.id.as_ref() + .expect("id is mandatory for a Docker Network."); + self.disconnect_traefik(network_id).await?; + docker.remove_network(network_id).await?; } Ok(()) } - async fn delete_volume_mount(&self, app_name: &String) -> Result<(), ShipLiftError> { - let docker = Docker::new(); - let docker_volumes = docker.volumes(); - for volume in DockerInfrastructure::fetch_existing_volumes(app_name).await? { - docker_volumes.get(&volume.name).delete().await? + async fn delete_volume_mount(&self, app_name: &AppName) -> Result<(), BollardError> { + let docker = Docker::connect_with_socket_defaults()?; + for volume in Self::fetch_existing_volumes(app_name) + .await? + .volumes + .into_iter() + .flatten() + { + docker.remove_volume(&volume.name, None).await?; } + Ok(()) } @@ -265,8 +292,8 @@ impl DockerInfrastructure { let network_id = self.create_or_get_network_id(app_name).await?; self.connect_traefik(&network_id).await?; - let existing_volumes = DockerInfrastructure::fetch_existing_volumes(app_name).await?; - let futures = services + let existing_volumes = Self::fetch_existing_volumes(app_name).await?; + let mut futures = services .iter() .map(|service| { self.start_container( @@ -277,10 +304,11 @@ impl DockerInfrastructure { &existing_volumes, ) }) - .collect::>(); + .map(Box::pin) + .collect::>(); let mut services: Vec = Vec::new(); - for service in join_all(futures).await { + while let Some(service) = futures.next().await { services.push(service?); } @@ -297,23 +325,56 @@ impl DockerInfrastructure { Some(services) => services.clone(), }; - let futures = container_details - .iter() - .filter(|details| details.state.running) - .map(|details| stop(details.clone())); - for container in join_all(futures).await { - trace!("Stopped container {:?}", container?); + let docker = Docker::connect_with_socket_defaults()?; + + let mut futures = container_details + .clone() + .into_iter() + .filter(|p| { + p.state.as_ref().and_then(|state| state.status) + == Some(ContainerStateStatusEnum::RUNNING) + }) + .map(|details| async { + let id = details + .id + .as_ref() + .expect("id is mandatory for a docker container"); + + docker.stop_container(id, None).await?; + + Ok::(details) + }) + .map(Box::pin) + .collect::>(); + + while let Some(result) = futures.next().await { + let container = result?; + let id = container + .id + .expect("id is mandatory for a docker container"); + trace!("Stopped container {id} for {app_name}"); } - let mut services = Vec::with_capacity(container_details.len()); - let futures = container_details - .iter() - .map(|details| delete(details.clone())); - for container in join_all(futures).await { - let container = container?; - trace!("Deleted container {:?}", container); + let mut futures = container_details + .into_iter() + .map(|details| async { + let id = details + .id + .as_ref() + .expect("id is mandatory for a docker container"); - services.push(Service::try_from(&container)?); + docker.remove_container(id, None).await?; + trace!("Deleted container {id} for {app_name}"); + + Ok::(details) + }) + .map(Box::pin) + .collect::>(); + + let mut services = Vec::with_capacity(futures.len()); + while let Some(result) = futures.next().await { + let container = result?; + services.push(Service::try_from(container)?); } self.delete_network(app_name).await?; @@ -325,243 +386,285 @@ impl DockerInfrastructure { async fn start_container( &self, app_name: &AppName, - network_id: &String, + network_id: &str, service: &DeployableService, container_config: &ContainerConfig, - existing_volumes: &[VolumeInfo], + existing_volumes: &VolumeListResponse, ) -> Result { - let docker = Docker::new(); - let containers = docker.containers(); - let images = docker.images(); + let docker = Docker::connect_with_socket_defaults()?; + let service_name = service.service_name(); + let service_image = service.image(); - if let Image::Named { .. } = service.image() { + if let Image::Named { .. } = service_image { self.pull_image(app_name, service).await?; } let mut image_to_delete = None; - if let Some(ref container_info) = self - .get_app_container(app_name, service.service_name()) - .await? - { - let container = containers.get(&container_info.id); - let container_details = container.inspect().await?; + if let Some(ref container_info) = self.get_app_container(app_name, service_name).await? { + let container_details = docker + .inspect_container( + container_info + .id + .as_ref() + .expect("id is mandatory for a docker container"), + None, + ) + .await?; match service.strategy() { DeploymentStrategy::RedeployOnImageUpdate(image_id) - if &container_details.image == image_id => + if container_details.image.as_ref() == Some(image_id) => { - debug!("Container {:?} of review app {:?} is still running with the desired image id {}", container_info, app_name, image_id); - return Ok(Service::try_from(&container_details)?); + debug!("Container {container_info:?} of review app {app_name:?} is still running with the desired image id {image_id}"); + return Ok(Service::try_from(container_details)?); } DeploymentStrategy::RedeployNever => { debug!( - "Container {:?} of review app {:?} already deployed.", - container_info, app_name + "Container {container_info:?} of review app {app_name:?} already deployed." ); - return Ok(Service::try_from(&container_details)?); + return Ok(Service::try_from(container_details)?); } DeploymentStrategy::RedeployAlways | DeploymentStrategy::RedeployOnImageUpdate(_) => {} }; - info!( - "Removing container {:?} of review app {:?}", - container_info, app_name - ); + info!("Removing container {container_info:?} of review app {app_name:?}"); - if container_details.state.running { - container - .stop(Some(core::time::Duration::from_secs(10))) + if container_details + .state + .map(|state| state.running == Some(true)) + .is_some() + { + docker + .stop_container( + container_details + .id + .as_ref() + .expect("id is mandatory for a docker container"), + None, + ) .await?; } - container.delete().await?; - image_to_delete = Some(container_details.image); + docker + .remove_container( + container_details + .id + .as_ref() + .expect("id is mandatory for a docker container"), + None, + ) + .await?; + image_to_delete = container_details.image; } info!( - "Creating new review app container for {:?}: service={:?} with image={:?} ({:?})", - app_name, - service.service_name(), - service.image(), + "Creating new review app container for {app_name:?}: service={service_name:?} with image={service_image:?} ({:?})", service.container_type(), ); let host_config_binds = - DockerInfrastructure::create_host_config_binds(app_name, existing_volumes, service) - .await?; + Self::create_host_config_binds(app_name, existing_volumes, service).await?; - let options = DockerInfrastructure::create_container_options( - app_name, - service, - container_config, - &host_config_binds, - ); + let options = + Self::create_container_options(app_name, service, container_config, &host_config_binds); - let container_info = containers.create(&options).await?; - debug!("Created container: {:?}", container_info); + let container_info = docker + .create_container::<&str, String>(None, options) + .await?; + let container_id = container_info.id.as_ref(); + debug!("Created container: {container_info:?}"); self.copy_file_data(&container_info, service).await?; - containers.get(&container_info.id).start().await?; - debug!("Started container: {:?}", container_info); + docker + .start_container(container_id, None::>) + .await?; + debug!("Started container: {container_info:?}"); docker - .networks() - .get(network_id) - .connect( - &ContainerConnectionOptions::builder(&container_info.id) - .aliases(vec![service.service_name().as_str()]) - .build(), + .connect_network( + network_id, + ConnectNetworkOptions::<&str> { + container: container_id, + endpoint_config: EndpointSettings { + aliases: Some(vec![service_name.to_string()]), + ..Default::default() + }, + }, ) .await?; - debug!( - "Connected container {:?} to {:?}", - container_info.id, network_id - ); - let container_details = containers.get(&container_info.id).inspect().await?; + debug!("Connected container {container_id} to {network_id}"); + + let container_details = docker.inspect_container(container_id, None).await?; if let Some(image) = image_to_delete { - info!("Clean up image {:?} of app {:?}", image, app_name); - match images.get(&image).delete().await { + info!("Clean up image {image:?} of app {app_name:?}"); + match docker.remove_image(&image, None, None).await { Ok(output) => { for o in output { - debug!("{:?}", o); + debug!("{o:?}"); } } - Err(err) => debug!("Could not clean up image: {:?}", err), - } + Err(err) => debug!("Could not clean up image: {err:?}"), + }; } - Ok(Service::try_from(&container_details)?) + Ok(Service::try_from(container_details)?) } - fn create_container_options( - app_name: &str, - service_config: &ServiceConfig, - container_config: &ContainerConfig, - host_config_binds: &[String], - ) -> ContainerOptions { - let mut options = ContainerOptions::builder(&service_config.image().to_string()); - if let Some(env) = service_config.env() { - let variables = env - .iter() - .map(|e| format!("{}={}", e.key(), e.value().unsecure())) - .collect::>(); - - options.env(variables.iter().map(|s| s.as_str()).collect::>()); - } + fn create_container_options<'a>( + app_name: &'a str, + service_config: &'a ServiceConfig, + container_config: &'a ContainerConfig, + host_config_binds: &'a [String], + ) -> bollard::container::Config { + let env = service_config.env().map(|env| { + env.iter() + .map(|v| format!("{}={}", v.key(), v.value().unsecure())) + .collect::>() + }); - let mut labels: HashMap<&str, &str> = HashMap::new(); + let mut labels: HashMap = HashMap::new(); let traefik_frontend = format!( "PathPrefixStrip: /{app_name}/{service_name}/; PathPrefix:/{app_name}/{service_name}/;", app_name = app_name, service_name = service_config.service_name() ); - labels.insert("traefik.frontend.rule", &traefik_frontend); + labels.insert("traefik.frontend.rule".to_string(), traefik_frontend); if let Some(config_labels) = service_config.labels() { for (k, v) in config_labels { - labels.insert(k, v); + labels.insert(k.to_string(), v.to_string()); } } - labels.insert(APP_NAME_LABEL, app_name); - labels.insert(SERVICE_NAME_LABEL, service_config.service_name()); - let container_type_name = service_config.container_type().to_string(); - labels.insert(CONTAINER_TYPE_LABEL, &container_type_name); + labels.insert(APP_NAME_LABEL.to_string(), app_name.to_string()); + labels.insert( + SERVICE_NAME_LABEL.to_string(), + service_config.service_name().to_string(), + ); + let container_type = service_config.container_type().to_string(); + labels.insert(CONTAINER_TYPE_LABEL.to_string(), container_type); let image_name = service_config.image().to_string(); - labels.insert(IMAGE_LABEL, &image_name); + labels.insert(IMAGE_LABEL.to_string(), image_name); let replicated_env = service_config .env() - .and_then(|env| super::replicated_environment_variable_to_json(env)) + .and_then(super::replicated_environment_variable_to_json) .map(|value| value.to_string()); - if let Some(replicated_env) = &replicated_env { - labels.insert(REPLICATED_ENV_LABEL, replicated_env); + if let Some(replicated_env) = replicated_env { + labels.insert(REPLICATED_ENV_LABEL.to_string(), replicated_env); } - if !host_config_binds.is_empty() { - options.volumes(host_config_binds.iter().map(|bind| bind.as_str()).collect()); - } - options.labels(&labels); - options.restart_policy("always", 5); - - if let Some(memory_limit) = container_config.memory_limit() { - options.memory(memory_limit.as_u64()); - options.memory_swap(memory_limit.as_u64() as i64); + let memory = container_config + .memory_limit() + .map(|mem| mem.as_u64() as i64); + + bollard::container::Config { + image: Some(service_config.image().to_string()), + env, + labels: Some(labels), + host_config: Some(HostConfig { + restart_policy: Some(RestartPolicy { + name: Some(RestartPolicyNameEnum::ALWAYS), + ..Default::default() + }), + binds: Some(host_config_binds.to_vec()), + memory, + memory_swap: memory, + ..Default::default() + }), + ..Default::default() } - - options.build() } async fn copy_file_data( &self, - container_info: &ContainerCreateInfo, + container_info: &ContainerCreateResponse, service_config: &ServiceConfig, - ) -> Result<(), ShipLiftError> { + ) -> Result<(), BollardError> { let files = match service_config.files() { None => return Ok(()), Some(files) => files.clone(), }; debug!( - "Copy data to container: {:?} (service = {})", - container_info, + "Copy data to container: {container_info:?} (service = {})", service_config.service_name() ); - let docker = Docker::new(); - let containers = docker.containers(); + let docker = Docker::connect_with_socket_defaults()?; + + let mut tar_builder = tar::Builder::new(Vec::new()); for (path, data) in files.into_iter() { - containers - .get(&container_info.id) - .copy_file_into(path, data.into_unsecure().as_bytes()) - .await?; + let mut header = tar::Header::new_gnu(); + let file_contents = data.into_unsecure(); + header.set_size(file_contents.as_bytes().len() as u64); + header.set_mode(0o644); + tar_builder.append_data( + &mut header, + path.to_path_buf() + .iter() + .skip(1) + .collect::(), + file_contents.as_bytes(), + )?; } + docker + .upload_to_container( + &container_info.id, + Some(UploadToContainerOptions { + path: "/", + ..Default::default() + }), + tar_builder.into_inner()?.into(), + ) + .await?; + Ok(()) } - async fn fetch_existing_volumes(app_name: &String) -> Result, ShipLiftError> { - let docker = Docker::new(); - docker.volumes().list().await.map(|volume_infos| { - volume_infos - .into_iter() - .filter(|s| { - s.labels - .as_ref() - .map(|label| label.get(APP_NAME_LABEL) == Some(app_name)) - .unwrap_or(false) - }) - .collect::>() - }) + async fn fetch_existing_volumes( + app_name: &AppName, + ) -> Result { + let docker = Docker::connect_with_socket_defaults()?; + docker + .list_volumes(Some(ListVolumesOptions { + filters: HashMap::from([( + "label".to_string(), + vec![format!("{APP_NAME_LABEL}={app_name}")], + )]), + })) + .await } async fn create_docker_volume( - app_name: &str, + app_name: &AppName, service: &DeployableService, - ) -> Result { - let docker = Docker::new(); - let volumes = docker.volumes(); + ) -> Result { + let docker = Docker::connect_with_socket_defaults()?; let mut labels: HashMap<&str, &str> = HashMap::new(); labels.insert(APP_NAME_LABEL, app_name); labels.insert(SERVICE_NAME_LABEL, service.service_name()); - let volume_options = VolumeCreateOptions::builder().labels(&labels).build(); - volumes - .create(&volume_options) + docker + .create_volume(CreateVolumeOptions { + labels, + ..Default::default() + }) .await - .map(|volume_info| volume_info.name) + .map(|vol| vol.name) } async fn create_host_config_binds( - app_name: &str, - existing_volume: &[VolumeInfo], + app_name: &AppName, + existing_volume: &VolumeListResponse, service: &DeployableService, - ) -> Result, ShipLiftError> { + ) -> Result, BollardError> { let mut host_binds = Vec::new(); if service.declared_volumes().is_empty() { @@ -569,22 +672,22 @@ impl DockerInfrastructure { } let service_volume = existing_volume - .iter() - .find(|vol| { - vol.labels.as_ref().map_or_else( - || false, - |label| label.get(SERVICE_NAME_LABEL) == Some(service.service_name()), - ) + .volumes + .as_ref() + .and_then(|volume| { + volume + .iter() + .find(|vol| vol.labels.get(SERVICE_NAME_LABEL) == Some(service.service_name())) }) - .map(|info| &info.name); + .map(|info| info.name.clone()); let volume_name = match service_volume { - Some(name) => String::from(name), - None => DockerInfrastructure::create_docker_volume(app_name, service).await?, + Some(name) => name, + None => Self::create_docker_volume(app_name, service).await?, }; for declared_volume in service.declared_volumes() { - host_binds.push(format!("{}:{}", volume_name, declared_volume)); + host_binds.push(format!("{volume_name}:{declared_volume}")); } Ok(host_binds) @@ -592,22 +695,20 @@ impl DockerInfrastructure { async fn pull_image( &self, - app_name: &String, + app_name: &AppName, config: &ServiceConfig, - ) -> Result<(), ShipLiftError> { + ) -> Result<(), BollardError> { let image = config.image(); info!( - "Pulling {:?} for {:?} of app {:?}", - image, - config.service_name(), - app_name + "Pulling {image:?} for {:?} of app {app_name:?}", + config.service_name() ); let pull_results = pull(image, &self.config).await?; for pull_result in pull_results { - debug!("{:?}", pull_result); + debug!("{pull_result:?}"); } Ok(()) @@ -615,28 +716,41 @@ impl DockerInfrastructure { async fn get_containers( &self, - filters: Vec, - ) -> Result, ShipLiftError> { - let docker = Docker::new(); - let containers = docker.containers(); + filters: HashMap>, + ) -> Result, BollardError> { + let docker = Docker::connect_with_socket_defaults()?; - let list_options = ContainerListOptions::builder() - .all() - .filter(filters) - .build(); + let list_options = Some(ListContainersOptions { + all: true, + filters, + ..Default::default() + }); - containers.list(&list_options).await + docker.list_containers(list_options).await } async fn get_app_containers( &self, app_name: Option<&AppName>, service_name: Option<&str>, - ) -> Result, ShipLiftError> { - let filters = vec![ - label_filter(APP_NAME_LABEL, app_name.map(|app_name| app_name.as_str())), - label_filter(SERVICE_NAME_LABEL, service_name), - ]; + ) -> Result, BollardError> { + let mut filters = HashMap::new(); + if let Some(app_name_filter) = + label_filter(APP_NAME_LABEL, app_name.map(|app_name| app_name.as_str())) + { + filters + .entry("label".to_string()) + .or_insert_with(Vec::new) + .push(app_name_filter); + } + + if let Some(service_name_filter) = label_filter(SERVICE_NAME_LABEL, service_name) { + filters + .entry("label".to_string()) + .or_insert_with(Vec::new) + .push(service_name_filter); + } + self.get_containers(filters).await } @@ -644,11 +758,19 @@ impl DockerInfrastructure { &self, app_name: Option<&AppName>, status_id: Option<&str>, - ) -> Result, ShipLiftError> { - let filters = vec![ - label_filter(APP_NAME_LABEL, app_name.map(|app_name| app_name.as_str())), - label_filter(STATUS_ID, status_id), - ]; + ) -> Result, BollardError> { + let mut label_filters = vec![]; + if let Some(app_name_filter) = + label_filter(APP_NAME_LABEL, app_name.map(|app_name| app_name.as_str())) + { + label_filters.push(app_name_filter); + } + + if let Some(status_id_filter) = label_filter(STATUS_ID, status_id) { + label_filters.push(status_id_filter); + } + + let filters = HashMap::from([("label".to_string(), label_filters)]); self.get_containers(filters).await } @@ -656,7 +778,7 @@ impl DockerInfrastructure { &self, app_name: &AppName, service_name: &str, - ) -> Result, ShipLiftError> { + ) -> Result, BollardError> { self.get_app_containers(Some(app_name), Some(service_name)) .await .map(|list| list.into_iter().next()) @@ -666,8 +788,8 @@ impl DockerInfrastructure { &self, app_name: Option<&AppName>, service_name: Option<&str>, - ) -> Result, Error> { - debug!("Resolve container details for app {:?}", app_name); + ) -> Result, Error> { + debug!("Resolve container details for app {app_name:?}"); let container_list = self.get_app_containers(app_name, service_name).await?; @@ -678,11 +800,13 @@ impl DockerInfrastructure { Some(app_name) => app_name.clone(), None => details .config - .labels - .clone() - .unwrap() - .get(APP_NAME_LABEL) - .and_then(|app_name| AppName::from_str(app_name).ok()) + .as_ref() + .and_then(|con| { + con.labels.as_ref().and_then(|lab| { + lab.get(APP_NAME_LABEL) + .and_then(|app_name| AppName::from_str(app_name).ok()) + }) + }) .unwrap(), }; container_details.insert(app_name, details); @@ -699,12 +823,12 @@ impl Infrastructure for DockerInfrastructure { let mut apps = MultiMap::new(); let container_details = self.get_container_details(None, None).await?; - for (app_name, details_vec) in container_details.iter_all() { + for (app_name, details_vec) in container_details.into_iter() { for details in details_vec { let service = match Service::try_from(details) { Ok(service) => service, Err(e) => { - debug!("Container does not provide required data: {:?}", e); + debug!("Container does not provide required data: {e:?}"); continue; } }; @@ -741,7 +865,11 @@ impl Infrastructure for DockerInfrastructure { .find_status_change_container(status_id) .await? .as_ref() - .and_then(|c| c.labels.get(APP_NAME_LABEL)) + .and_then(|c| { + c.labels + .as_ref() + .and_then(|label| label.get(APP_NAME_LABEL)) + }) .and_then(|app_name| AppName::from_str(app_name).ok()) { Some(app_name) => { @@ -749,7 +877,7 @@ impl Infrastructure for DockerInfrastructure { if let Some(container_details) = self .get_container_details(Some(&app_name), None) .await? - .get_vec(&app_name) + .remove(&app_name) { for container in container_details { services.push(Service::try_from(container)?); @@ -795,38 +923,43 @@ impl Infrastructure for DockerInfrastructure { { Ok(None) => {} Ok(Some(container)) => { - let docker = Docker::new(); - - trace!( - "Acquiring logs of container {} since {:?}", - container.id, - from - ); - - let mut log_options = LogsOptions::builder(); - log_options.stdout(true).stderr(true).timestamps(true); - - if let Some(since) = from { - log_options.since(since); - } - - log_options.follow(follow); + let docker = Docker::connect_with_socket_defaults()?; + let container_id = container + .id + .as_ref() + .expect("id is mandatory for docker container"); + trace!("Acquiring logs of container {container_id} since {from:?}"); + + let log_options = match from { + Some(from) => bollard::container::LogsOptions::<&str> { + stdout: true, + stderr: true, + since: from.timestamp(), + timestamps: true, + follow, + ..Default::default() + }, + None => bollard::container::LogsOptions::<&str> { + stdout: true, + stderr: true, + timestamps: true, + follow, + ..Default::default() + }, + }; - let logs = docker - .containers() - .get(&container.id) - .logs(&log_options.build()); + let logs = docker.logs(container_id, Some(log_options)); let mut logs = match limit { Some(log_limit) => Box::pin(logs.take(*log_limit)) - as BoxStream>, - None => Box::pin(logs) as BoxStream>, + as BoxStream>, + None => Box::pin(logs) as BoxStream>, }; while let Some(result) = logs.next().await { match result { Ok(chunk) => { - let line = String::from_utf8_lossy(&chunk.to_vec()).to_string(); + let line = chunk.to_string(); let mut iter = line.splitn(2, ' '); let timestamp = iter.next() @@ -854,21 +987,32 @@ impl Infrastructure for DockerInfrastructure { ) -> Result, failure::Error> { match self.get_app_container(app_name, service_name).await? { Some(container) => { - let docker = Docker::new(); - let containers = docker.containers(); - let c = containers.get(&container.id); - - let details = c.inspect().await?; + let docker = Docker::connect_with_socket_defaults()?; + let details = docker + .inspect_container( + container + .id + .as_ref() + .expect("id is mandatory for a docker container"), + None, + ) + .await?; macro_rules! run_future_and_map_err { ( $future:expr, $log_format:expr ) => { if let Err(err) = $future.await { match err { - ShipLiftError::Fault { code, message } if code.as_u16() == 304 => { + BollardError::DockerResponseServerError { + status_code, + message, + .. + } if status_code == 304 => { trace!( - "Container {} already in desired state: {}", - details.id, - message + "Container {} already in desired state: {message}", + details + .id + .as_ref() + .expect("id is mandatory for a docker container") ); } err => { @@ -882,202 +1026,227 @@ impl Infrastructure for DockerInfrastructure { match status { ServiceStatus::Running => { - if !details.state.running { - run_future_and_map_err!(c.start(), "Could not start container: {}"); + if !details + .state + .as_ref() + .map(|state| state.running.unwrap_or_default()) + .unwrap() + { + run_future_and_map_err!( + docker.start_container( + container + .id + .as_ref() + .expect("id is mandatory for a docker container"), + None::>, + ), + "Could not start container: {}" + ); } } ServiceStatus::Paused => { - if details.state.running { - run_future_and_map_err!(c.stop(None), "Could not pause container: {}"); + if details + .state + .as_ref() + .map(|state| state.running.unwrap_or_default()) + .unwrap() + { + run_future_and_map_err!( + docker.stop_container( + container + .id + .as_ref() + .expect("id is mandatory for a docker container"), + None + ), + "Could not pause container: {}" + ); } } } - Ok(Some(Service::try_from(&details)?)) + Ok(Some(Service::try_from(details)?)) } None => Ok(None), } } } -/// Helper function to build ContainerFilters -fn label_filter(label_name: S, label_value: Option) -> ContainerFilter +/// Helper function to build Label Filters +fn label_filter(label_name: S, label_value: Option) -> Option where S: AsRef, { let label_name = label_name.as_ref().to_string(); - match label_value { - None => ContainerFilter::LabelName(label_name), - Some(value) => ContainerFilter::Label(label_name, value.as_ref().to_string()), + match label_value.as_ref().map(AsRef::as_ref) { + None => Some(label_name.to_string()), + Some(value) => Some(format!("{label_name}={value}")), } } /// Helper function to map ShipLift 404 errors to None -fn not_found_to_none(result: Result) -> Result, ShipLiftError> { +fn not_found_to_none(result: Result) -> Result, BollardError> { match result { Ok(value) => Ok(Some(value)), - Err(ShipLiftError::Fault { code, .. }) if code.as_u16() == 404u16 => Ok(None), + Err(BollardError::DockerResponseServerError { + status_code: 404u16, + .. + }) => Ok(None), Err(err) => Err(err), } } /// Helper function to pull images -async fn pull(image: &Image, config: &Config) -> Result, ShipLiftError> { - let mut pull_options_builder = PullOptions::builder(); - pull_options_builder.image(&image.to_string()); - - if let Some(registry) = image.registry() { +async fn pull(image: &Image, config: &Config) -> Result, BollardError> { + let pull_options = CreateImageOptions::<&str> { + from_image: &image.to_string(), + ..Default::default() + }; + let docker_auth = if let Some(registry) = image.registry() { if let Some((username, password)) = config.registry_credentials(®istry) { - pull_options_builder.auth( - RegistryAuth::builder() - .username(username) - .password(password.unsecure()) - .build(), - ); + Some(DockerCredentials { + username: Some(username.to_string()), + password: Some(password.unsecure().to_string()), + ..Default::default() + }) + } else { + None } - } - - let docker = Docker::new(); - let images = docker.images(); + } else { + None + }; - images - .pull(&pull_options_builder.build()) + let docker = Docker::connect_with_socket_defaults()?; + docker + .create_image(Some(pull_options), None, docker_auth) .try_collect() .await } -/// Helper function to stop containers with the aid of futures::future::join_all -async fn stop(details: ContainerDetails) -> Result { - let docker = Docker::new(); - let containers = docker.containers(); - containers.get(&details.id).stop(None).await?; - Ok(details) -} - /// Helper function to delete containers with the aid of futures::future::join_all -async fn delete(details: ContainerDetails) -> Result { - let docker = Docker::new(); - let containers = docker.containers(); - containers.get(&details.id).delete().await?; +async fn delete( + details: ContainerInspectResponse, +) -> Result { + let docker = Docker::connect_with_socket_defaults()?; + docker + .remove_container( + details + .id + .as_ref() + .expect("id is mandatory for a docker container"), + None, + ) + .await?; Ok(details) } /// Helper function to inspect containers with the aid of futures::future::join_all -async fn inspect(container: ContainerInfo) -> Result { - let docker = Docker::new(); - let containers = docker.containers(); - containers.get(&container.id).inspect().await +async fn inspect(container: ContainerSummary) -> Result { + let docker = Docker::connect_with_socket_defaults()?; + docker + .inspect_container( + &container + .id + .expect("id is mandatory for a docker container"), + None, + ) + .await } fn find_port( - container_details: &ContainerDetails, - labels: Option<&HashMap>, + ports: &[&str], + labels: &mut Option>, ) -> Result { - if let Some(port) = labels.and_then(|labels| labels.get(CONTAINER_PORT_LABEL)) { + if let Some(port) = labels + .as_mut() + .and_then(|labels| labels.remove(CONTAINER_PORT_LABEL)) + { match port.parse::() { Ok(port) => Ok(port), Err(err) => Err(DockerInfrastructureError::UnexpectedError { internal_message: format!( - "Cannot parse traefik port label into port number: {}", - err + "Cannot parse traefik port label into port number: {err}" ), }), } } else { - Ok(match &container_details.network_settings.ports { - None => 80u16, - Some(ports) => { - let ports_regex = Regex::new(r#"^(?P\d+).*"#).unwrap(); - ports - .keys() - .filter_map(|port| ports_regex.captures(port)) - .map(|captures| String::from(captures.name("port").unwrap().as_str())) - .filter_map(|port| port.parse::().ok()) - .min() - .unwrap_or(80u16) - } - }) + let ports_regex = Regex::new(r#"^(?P\d+).*"#).unwrap(); + Ok(ports + .iter() + .filter_map(|port| ports_regex.captures(port)) + .map(|captures| String::from(captures.name("port").unwrap().as_str())) + .filter_map(|port| port.parse::().ok()) + .min() + .unwrap_or(80u16)) } } -impl TryFrom<&ContainerDetails> for Service { +impl TryFrom for Service { type Error = DockerInfrastructureError; fn try_from( - container_details: &ContainerDetails, + container_details: ContainerInspectResponse, ) -> Result { - let labels = container_details.config.labels.as_ref(); - - let app_name = match labels.and_then(|labels| labels.get(APP_NAME_LABEL)) { + let mut labels = container_details.config.and_then(|config| config.labels); + let container_id = container_details + .id + .expect("id is mandatory for a docker container"); + let app_name = match labels + .as_mut() + .and_then(|labels| labels.remove(APP_NAME_LABEL)) + { Some(name) => name, None => { - return Err(DockerInfrastructureError::MissingAppNameLabel { - container_id: container_details.id.clone(), - }); + return Err(DockerInfrastructureError::MissingAppNameLabel { container_id }); } }; - let started_at = container_details.state.started_at; - let status = if container_details.state.running { - ServiceStatus::Running - } else { - ServiceStatus::Paused - }; - - let mut builder = ServiceBuilder::new() - .id(container_details.id.clone()) - .app_name(app_name.clone()) - .config(ServiceConfig::try_from(container_details)?) - .service_status(status) - .started_at(started_at); - - if !container_details.network_settings.ip_address.is_empty() { - let addr = IpAddr::from_str(&container_details.network_settings.ip_address)?; - let port = find_port(container_details, labels)?; - builder = builder.endpoint(addr, port); - } - - Ok(builder.build()?) - } -} - -impl TryFrom<&ContainerDetails> for ServiceConfig { - type Error = DockerInfrastructureError; - - fn try_from(container_details: &ContainerDetails) -> Result { - let labels = container_details.config.labels.as_ref(); - - let service_name = match labels.and_then(|labels| labels.get(SERVICE_NAME_LABEL)) { + let service_name = match labels + .as_mut() + .and_then(|labels| labels.remove(SERVICE_NAME_LABEL)) + { Some(name) => name, None => { - return Err(DockerInfrastructureError::MissingServiceNameLabel { - container_id: container_details.id.clone(), - }); + return Err(DockerInfrastructureError::MissingServiceNameLabel { container_id }); } }; - let image = match labels.and_then(|labels| labels.get(IMAGE_LABEL)) { - Some(image_label) => Image::from_str(image_label).map_err(|err| { - DockerInfrastructureError::UnexpectedImageFormat { - img: image_label.clone(), - err: err.to_string(), - } - }), - None => Image::from_str(&container_details.image).map_err(|err| { + let image = match labels + .as_mut() + .and_then(|labels| labels.remove(IMAGE_LABEL)) + { + Some(image_label) => Image::from_str(&image_label).map_err(|err| { DockerInfrastructureError::UnexpectedImageFormat { - img: container_details.image.clone(), + img: image_label, err: err.to_string(), } }), + None => { + let Some(image) = container_details.image else { + return Err(DockerInfrastructureError::InvalidContainerImage { container_id }); + }; + Image::from_str(&image).map_err(|err| { + DockerInfrastructureError::UnexpectedImageFormat { + img: image, + err: err.to_string(), + } + }) + } }?; let mut config = ServiceConfig::new(service_name.clone(), image); - if let Some(lb) = labels.and_then(|labels| labels.get(CONTAINER_TYPE_LABEL)) { + if let Some(lb) = labels + .as_mut() + .and_then(|labels| labels.remove(CONTAINER_TYPE_LABEL)) + { config.set_container_type(lb.parse::()?); } - if let Some(replicated_env) = labels.and_then(|labels| labels.get(REPLICATED_ENV_LABEL)) { - let env = serde_json::from_str::(replicated_env).map_err(|err| { + if let Some(replicated_env) = labels + .as_mut() + .and_then(|labels| labels.remove(REPLICATED_ENV_LABEL)) + { + let env = serde_json::from_str::(&replicated_env).map_err(|err| { DockerInfrastructureError::UnexpectedError { internal_message: err.to_string(), } @@ -1085,14 +1254,57 @@ impl TryFrom<&ContainerDetails> for ServiceConfig { config.set_env(Some(env)); } - Ok(config) + let Some(state) = container_details.state else { + return Err(DockerInfrastructureError::InvalidContainerState { container_id }); + }; + + let started_at = state + .started_at + .as_deref() + .and_then(|s| DateTime::parse_from_rfc3339(s).ok()) + .expect("started_at is mandatory for a docker container"); + + let status = match state.status.unwrap_or(ContainerStateStatusEnum::PAUSED) { + ContainerStateStatusEnum::RUNNING => ServiceStatus::Running, + _ => ServiceStatus::Paused, + }; + + let mut builder = ServiceBuilder::new() + .id(container_id.clone()) + .app_name(app_name.clone()) + .config(config) + .service_status(status) + .started_at(started_at.into()); + + if let Some(network_settings) = container_details.network_settings { + if let Some(ip_address) = network_settings.ip_address.as_ref() { + if !ip_address.is_empty() { + let port = find_port( + &network_settings + .ports + .unwrap_or_default() + .keys() + .map(AsRef::as_ref) + .collect::>(), + &mut labels, + )?; + let ip_address = IpAddr::from_str(ip_address)?; + builder = builder.endpoint(ip_address, port); + } + } + } + + Ok(builder.build()?) } } -impl From for DockerInfrastructureError { - fn from(err: ShipLiftError) -> Self { +impl From for DockerInfrastructureError { + fn from(err: BollardError) -> Self { match &err { - ShipLiftError::Fault { code, message } => match code.as_u16() { + BollardError::DockerResponseServerError { + status_code, + message, + } => match status_code { 404u16 => DockerInfrastructureError::ImageNotFound { internal_message: message.clone(), }, @@ -1143,6 +1355,9 @@ mod tests { use super::*; use crate::models::{Environment, EnvironmentVariable}; use crate::sc; + use bollard::models::ContainerState; + use bollard::models::ContainerStateStatusEnum; + use bollard::models::NetworkSettings; use secstr::SecUtf8; macro_rules! container_details { @@ -1164,77 +1379,84 @@ mod tests { $( labels.insert($l_key, $l_value); )* - ContainerDetails { - app_armor_profile: "".to_string(), - args: vec![], - config: shiplift::image::Config { - attach_stderr: false, - attach_stdin: false, - attach_stdout: false, + + + ContainerInspectResponse { + app_armor_profile: Some("".to_string()), + args: Some(vec![]), + config: Some(bollard::service::ContainerConfig { + attach_stderr: Some(false), + attach_stdin: Some(false), + attach_stdout: Some(false), cmd: None, - domainname: "".to_string(), + domainname: Some("".to_string()), entrypoint: None, env: None, exposed_ports: None, - hostname: "".to_string(), - image: "".to_string(), + hostname: Some("".to_string()), + image: Some("".to_string()), labels: Some(labels), on_build: None, - open_stdin: false, - stdin_once: false, - tty: false, - user: "".to_string(), - working_dir: "".to_string(), - }, - driver: "".to_string(), - host_config: shiplift::container::HostConfig { + open_stdin: Some(false), + stdin_once: Some(false), + tty: Some(false), + user: Some("".to_string()), + working_dir: Some("".to_string()), + ..Default::default() + }), + driver: Some("".to_string()), + host_config: Some(HostConfig { cgroup_parent: None, - container_id_file: "".to_string(), + container_id_file: Some("".to_string()), cpu_shares: None, cpuset_cpus: None, memory: None, memory_swap: None, - network_mode: "".to_string(), + network_mode: Some("".to_string()), pid_mode: None, port_bindings: None, - privileged: false, - publish_all_ports: false, + privileged: Some(false), + publish_all_ports: Some(false), readonly_rootfs: None, - }, - hostname_path: "".to_string(), - hosts_path: "".to_string(), - log_path: "".to_string(), - id: $id, - image: "sha256:9895c9b90b58c9490471b877f6bb6a90e6bdc154da7fbb526a0322ea242fc913".to_string(), - mount_label: "".to_string(), - name: "".to_string(), - network_settings: shiplift::network::NetworkSettings { - bridge: "".to_string(), - gateway: "".to_string(), - ip_address: "".to_string(), - ip_prefix_len: 0, - mac_address: "".to_string(), + ..Default::default() + }), + hostname_path: Some("".to_string()), + hosts_path: Some("".to_string()), + log_path: Some("".to_string()), + id: Some($id), + image: Some("sha256:9895c9b90b58c9490471b877f6bb6a90e6bdc154da7fbb526a0322ea242fc913".to_string()), + mount_label: Some("".to_string()), + name: Some("".to_string()), + network_settings: Some(NetworkSettings { + bridge: Some("".to_string()), + gateway: Some("".to_string()), + ip_address: Some("".to_string()), + ip_prefix_len: Some(0), + mac_address: Some("".to_string()), ports: None, - networks: Default::default(), - }, - path: "".to_string(), - process_label: "".to_string(), - resolv_conf_path: "".to_string(), - restart_count: 0, - state: shiplift::container::State { - status: "Running".to_string(), - error: "".to_string(), - exit_code: 0, - oom_killed: false, - paused: false, - pid: 0, - restarting: false, - running: false, - finished_at: chrono::Utc::now(), - started_at: chrono::Utc::now(), - }, - created: chrono::Utc::now(), - mounts: vec![], + networks: None, + ..Default::default() + }), + path: Some("".to_string()), + process_label: Some("".to_string()), + resolv_conf_path: Some("".to_string()), + restart_count: Some(0) , + state: Some(ContainerState { + status: Some(ContainerStateStatusEnum::RUNNING), + error: Some("".to_string()), + exit_code: Some(0), + oom_killed: Some(false), + paused: Some(false), + pid: Some(0), + restarting: Some(false), + running: Some(false), + finished_at: Some(chrono::Utc::now().to_rfc3339()), + started_at: Some(chrono::Utc::now().to_rfc3339()), + ..Default::default() + }), + created: Some(chrono::Utc::now().to_rfc3339()), + mounts: Some(vec![]), + ..Default::default() } }}; } @@ -1254,17 +1476,19 @@ mod tests { assert_json_diff::assert_json_eq!( json, serde_json::json!({ - "name": null, - "params": { - "HostConfig.RestartPolicy.Name": "always", - "Image": "docker.io/library/mariadb:10.3.17", - "Labels": { - "com.aixigo.preview.servant.app-name": "master", - "com.aixigo.preview.servant.container-type": "instance", - "com.aixigo.preview.servant.service-name": "db", - "com.aixigo.preview.servant.image": "docker.io/library/mariadb:10.3.17", - "traefik.frontend.rule": "PathPrefixStrip: /master/db/; PathPrefix:/master/db/;" + "HostConfig": { + "Binds": [], + "RestartPolicy": { + "Name": "always" } + }, + "Image": "docker.io/library/mariadb:10.3.17", + "Labels": { + "com.aixigo.preview.servant.app-name": "master", + "com.aixigo.preview.servant.container-type": "instance", + "com.aixigo.preview.servant.image": "docker.io/library/mariadb:10.3.17", + "com.aixigo.preview.servant.service-name": "db", + "traefik.frontend.rule": "PathPrefixStrip: /master/db/; PathPrefix:/master/db/;" } }) ); @@ -1289,20 +1513,22 @@ mod tests { assert_json_diff::assert_json_eq!( json, serde_json::json!({ - "name": null, - "params": { - "HostConfig.RestartPolicy.Name": "always", - "Image": "docker.io/library/mariadb:10.3.17", - "Labels": { - "com.aixigo.preview.servant.app-name": "master", - "com.aixigo.preview.servant.container-type": "instance", - "com.aixigo.preview.servant.service-name": "db", - "com.aixigo.preview.servant.image": "docker.io/library/mariadb:10.3.17", - "traefik.frontend.rule": "PathPrefixStrip: /master/db/; PathPrefix:/master/db/;" - }, - "Env": [ - "MYSQL_ROOT_PASSWORD=example" - ] + "Env": [ + "MYSQL_ROOT_PASSWORD=example" + ], + "HostConfig": { + "Binds": [], + "RestartPolicy": { + "Name": "always" + } + }, + "Image": "docker.io/library/mariadb:10.3.17", + "Labels": { + "com.aixigo.preview.servant.app-name": "master", + "com.aixigo.preview.servant.container-type": "instance", + "com.aixigo.preview.servant.image": "docker.io/library/mariadb:10.3.17", + "com.aixigo.preview.servant.service-name": "db", + "traefik.frontend.rule": "PathPrefixStrip: /master/db/; PathPrefix:/master/db/;" } }) ); @@ -1329,28 +1555,30 @@ mod tests { assert_json_diff::assert_json_eq!( json, serde_json::json!({ - "name": null, - "params": { - "HostConfig.RestartPolicy.Name": "always", - "Image": "docker.io/library/mariadb:10.3.17", - "Labels": { - "com.aixigo.preview.servant.app-name": "master", - "com.aixigo.preview.servant.container-type": "instance", - "com.aixigo.preview.servant.service-name": "db", - "com.aixigo.preview.servant.image": "docker.io/library/mariadb:10.3.17", - "com.aixigo.preview.servant.replicated-env": serde_json::json!({ - "MYSQL_ROOT_PASSWORD": { + "Env": [ + "MYSQL_ROOT_PASSWORD=example" + ], + "HostConfig": { + "Binds": [], + "RestartPolicy": { + "Name": "always" + } + }, + "Image": "docker.io/library/mariadb:10.3.17", + "Labels": { + "com.aixigo.preview.servant.app-name": "master", + "com.aixigo.preview.servant.container-type": "instance", + "com.aixigo.preview.servant.image": "docker.io/library/mariadb:10.3.17", + "com.aixigo.preview.servant.replicated-env": serde_json::json!({ + "MYSQL_ROOT_PASSWORD": { "value": "example", "templated": false, "replicate": true, - } + } }).to_string(), - "traefik.frontend.rule": "PathPrefixStrip: /master/db/; PathPrefix:/master/db/;" - }, - "Env": [ - "MYSQL_ROOT_PASSWORD=example" - ] - } + "com.aixigo.preview.servant.service-name": "db", + "traefik.frontend.rule": "PathPrefixStrip: /master/db/; PathPrefix:/master/db/;" + } }) ); } @@ -1365,7 +1593,7 @@ mod tests { None, ); - let service = Service::try_from(&details).unwrap(); + let service = Service::try_from(details).unwrap(); assert_eq!(service.id(), "some-random-id"); assert_eq!(service.app_name(), "master"); @@ -1386,7 +1614,7 @@ mod tests { None, ); - let error = Service::try_from(&details).unwrap_err(); + let error = Service::try_from(details).unwrap_err(); assert_eq!( error, DockerInfrastructureError::UnexpectedImageFormat { @@ -1406,7 +1634,7 @@ mod tests { None, ); - let service = Service::try_from(&details).unwrap(); + let service = Service::try_from(details).unwrap(); assert_eq!(service.id(), "some-random-id"); assert_eq!(service.app_name(), "master"); @@ -1428,7 +1656,7 @@ mod tests { String::from(REPLICATED_ENV_LABEL) => serde_json::json!({ "MYSQL_ROOT_PASSWORD": { "value": "example" } }).to_string() ); - let service = Service::try_from(&details).unwrap(); + let service = Service::try_from(details).unwrap(); assert_eq!( service.config().env().unwrap().get(0).unwrap(), @@ -1454,18 +1682,21 @@ mod tests { assert_json_diff::assert_json_eq!( json, serde_json::json!({ - "name": null, - "params": { - "HostConfig.RestartPolicy.Name": "always", - "Image": "docker.io/library/mariadb:10.3.17", - "HostConfig.Binds":["test-volume:/var/lib/mysql"], - "Labels": { - "com.aixigo.preview.servant.app-name": "master", - "com.aixigo.preview.servant.container-type": "instance", - "com.aixigo.preview.servant.service-name": "db", - "com.aixigo.preview.servant.image": "docker.io/library/mariadb:10.3.17", - "traefik.frontend.rule": "PathPrefixStrip: /master/db/; PathPrefix:/master/db/;" + "HostConfig": { + "Binds": [ + "test-volume:/var/lib/mysql" + ], + "RestartPolicy": { + "Name": "always" } + }, + "Image": "docker.io/library/mariadb:10.3.17", + "Labels": { + "com.aixigo.preview.servant.app-name": "master", + "com.aixigo.preview.servant.container-type": "instance", + "com.aixigo.preview.servant.image": "docker.io/library/mariadb:10.3.17", + "com.aixigo.preview.servant.service-name": "db", + "traefik.frontend.rule": "PathPrefixStrip: /master/db/; PathPrefix:/master/db/;" } }) ); diff --git a/api/src/tickets.rs b/api/src/tickets.rs index a2ef8fe3..c43de79c 100644 --- a/api/src/tickets.rs +++ b/api/src/tickets.rs @@ -69,7 +69,7 @@ pub async fn tickets( let issue_keys = services .keys() - .map(|s| format!("{:?}", s)) + .map(|s| format!("\"{}\"", s)) .collect::>() .join(", ");