diff --git a/Cargo.lock b/Cargo.lock index b91c358..7fa4fcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -52,6 +58,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -64,6 +76,50 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bollard" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aed08d3adb6ebe0eff737115056652670ae290f177759aac19c30456135f94c" +dependencies = [ + "base64", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "http", + "http-body-util", + "hyper", + "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 = [ + "serde", + "serde_repr", + "serde_with", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -80,6 +136,12 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "cc" version = "1.1.6" @@ -108,6 +170,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.6", ] @@ -179,6 +242,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", + "serde_core", +] + [[package]] name = "dirs" version = "3.0.2" @@ -199,11 +272,23 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "docuum" version = "0.25.1" dependencies = [ "atty", + "bollard", "byte-unit", "chrono", "clap", @@ -211,6 +296,7 @@ dependencies = [ "ctrlc", "dirs", "env_logger", + "futures-util", "humantime", "log", "regex", @@ -219,8 +305,15 @@ dependencies = [ "serde_yaml", "sysinfo", "tempfile", + "tokio", ] +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.13.0" @@ -238,6 +331,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.9" @@ -254,6 +353,73 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -271,6 +437,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -280,12 +452,130 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "humantime" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "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", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "hyperlocal-next" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf569d43fa9848e510358c07b80f4adf34084ddc28c6a4a651ee8474c070dcc" +dependencies = [ + "hex", + "http-body-util", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -309,6 +599,113 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -316,7 +713,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "serde_core", ] [[package]] @@ -342,9 +752,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libredox" @@ -368,6 +778,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "log" version = "0.4.22" @@ -380,6 +796,17 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.59.0", +] + [[package]] name = "nix" version = "0.29.0" @@ -401,6 +828,12 @@ 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.19" @@ -416,11 +849,44 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -465,6 +931,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.10.5" @@ -513,20 +999,54 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "serde" -version = "1.0.204" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -535,27 +1055,98 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", "itoa", "ryu", "serde", ] +[[package]] +name = "serde_with" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.11.4", + "schemars 0.9.0", + "schemars 1.0.4", + "serde_core", + "serde_json", + "time", +] + [[package]] name = "serde_yaml" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ - "indexmap", + "indexmap 1.9.3", "ryu", "serde", "yaml-rust", ] +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.8.0" @@ -564,15 +1155,26 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "syn" -version = "2.0.72" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sysinfo" version = "0.23.13" @@ -649,6 +1251,117 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -661,18 +1374,45 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "utf8-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -773,6 +1513,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.48.0" @@ -800,6 +1546,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -824,13 +1588,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -843,6 +1624,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -855,6 +1642,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -867,12 +1660,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -885,6 +1690,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -897,6 +1708,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -909,6 +1726,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -921,6 +1744,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "yaml-rust" version = "0.4.5" @@ -929,3 +1764,81 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index ff08a4a..183c86a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,9 @@ serde_json = "1.0" serde_yaml = "0.8" tempfile = "3" humantime = "2.2.0" +bollard = { version = "0.16" } +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +futures-util = "0.3" [target.'cfg(target_os = "linux")'.dependencies] sysinfo = "0.23.5" @@ -44,3 +47,6 @@ features = ["termination"] # [tag:ctrlc_term] [dependencies.serde] version = "1" features = ["derive"] + +[profile.release] +strip = true diff --git a/Dockerfile b/Dockerfile index 019f7a1..fcf510c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,47 @@ -# The base image for the build stage -FROM --platform=$BUILDPLATFORM alpine:3.20 AS build +FROM --platform=$BUILDPLATFORM ubuntu AS builder +ENV HOME="/root" +WORKDIR $HOME -# Choose the appropriate Docuum binary to install. +RUN apt update \ + && apt install -y --no-install-recommends \ + build-essential \ + curl \ + python3-venv \ + && apt clean \ + && rm -rf /var/lib/apt/lists/* + +# Setup zig as cross compiling linker +RUN python3 -m venv $HOME/.venv +RUN .venv/bin/pip install cargo-zigbuild +ENV PATH="$HOME/.venv/bin:$PATH" + +# Install rust ARG TARGETPLATFORM -COPY artifacts/docuum-x86_64-unknown-linux-musl /tmp/linux/amd64 -COPY artifacts/docuum-aarch64-unknown-linux-musl /tmp/linux/arm64 -RUN cp "/tmp/$TARGETPLATFORM" /usr/local/bin/docuum +RUN case "$TARGETPLATFORM" in \ + "linux/arm64") echo "aarch64-unknown-linux-musl" > rust_target.txt ;; \ + "linux/amd64") echo "x86_64-unknown-linux-musl" > rust_target.txt ;; \ + *) exit 1 ;; \ + esac + +# Update rustup whenever we bump the rust version +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --target $(cat rust_target.txt) --profile minimal --default-toolchain none +ENV PATH="$HOME/.cargo/bin:$PATH" +# Install the toolchain then the musl target +RUN rustup toolchain install stable +RUN rustup target add $(cat rust_target.txt) -# A minimal base image -FROM --platform=$TARGETPLATFORM alpine:3.20 +# Build +COPY . . +RUN cargo zigbuild --bin docuum --target $(cat rust_target.txt) --release +RUN cp target/$(cat rust_target.txt)/release/docuum /docuum -# Install the Docker CLI. -RUN apk add --no-cache docker-cli +# A distroless base image +FROM scratch +WORKDIR /app # Install Docuum. -COPY --from=build /usr/local/bin/docuum /usr/local/bin/docuum +COPY --from=builder /docuum . # Set the entrypoint to Docuum. Note that Docuum is not intended to be run as # an init process, so be sure to pass `--init` to `docker run`. -ENTRYPOINT ["/usr/local/bin/docuum"] +ENTRYPOINT ["/app/docuum"] diff --git a/src/main.rs b/src/main.rs index 10c48d0..1b02f8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,22 +10,18 @@ use { clap::{App, AppSettings, Arg}, env_logger::{Builder, fmt::Color}, humantime::parse_duration, - log::{Level, LevelFilter}, + log::{Level, LevelFilter, debug, error, info, warn}, regex::RegexSet, std::{ env, io::{self, Write}, process::exit, str::FromStr, - sync::{Arc, Mutex}, thread::sleep, time::Duration, }, }; -#[macro_use] -extern crate log; - // The program version const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -259,34 +255,9 @@ fn settings() -> io::Result { }) } -// This function consumes and runs all the registered destructors. We use this mechanism instead of -// RAII for things that need to be cleaned up even when the process is killed due to a signal. -#[allow(clippy::type_complexity)] -fn run_destructors(destructors: &Arc>>>) { - let mut mutex_guard = destructors.lock().unwrap(); - let destructor_fns = std::mem::take(&mut *mutex_guard); - for destructor in destructor_fns { - destructor(); - } -} - // Let the fun begin! -fn main() { - // If Docuum is in the foreground process group for some TTY, the process will receive a SIGINT - // when the user types CTRL+C at the terminal. The default behavior is to crash when this signal - // is received. However, we would rather clean up resources before terminating, so we trap the - // signal here. This code also traps SIGHUP and SIGTERM, since we compile the `ctrlc` crate with - // the `termination` feature [ref:ctrlc_term]. - let destructors = Arc::new(Mutex::new(Vec::>::new())); - let destructors_clone = destructors.clone(); - if let Err(error) = ctrlc::set_handler(move || { - run_destructors(&destructors_clone); - exit(1); - }) { - // Log the error and proceed anyway. - error!("{}", error); - } - +#[tokio::main] +async fn main() { // Determine whether to print colored output. colored::control::set_override(atty::is(Stream::Stderr)); @@ -320,13 +291,10 @@ fn main() { // Stream Docker events and vacuum when necessary. Restart if an error occurs. loop { // This will run until an error occurs (it never returns `Ok`). - if let Err(error) = run(&settings, &mut state, &mut first_run, &destructors) { + if let Err(error) = run(&settings, &mut state, &mut first_run).await { error!("{}", error); } - // Clean up any resources left over from that run. - run_destructors(&destructors); - // Wait a moment and then retry. info!("Retrying in 5 seconds\u{2026}"); sleep(Duration::from_secs(5)); diff --git a/src/run.rs b/src/run.rs index dad32af..4e09071 100644 --- a/src/run.rs +++ b/src/run.rs @@ -4,31 +4,31 @@ use { format::CodeStr, state::{self, State}, }, + bollard::{ + Docker, + container::ListContainersOptions, + image::{ListImagesOptions, RemoveImageOptions}, + models::{EventMessage, EventMessageTypeEnum}, + }, byte_unit::Byte, - chrono::DateTime, + futures_util::stream::StreamExt, + log::{debug, error, info, trace}, regex::RegexSet, - serde::{Deserialize, Serialize}, std::{ cmp::max, collections::{HashMap, HashSet, hash_map::Entry}, - io::{self, BufRead, BufReader}, - ops::Deref, - process::{Command, Stdio}, - sync::{Arc, Mutex}, + io, time::{Duration, SystemTime, UNIX_EPOCH}, }, }; #[cfg(target_os = "linux")] use { + bollard::models::SystemInfo, std::path::{Path, PathBuf}, sysinfo::{Disk, DiskExt, RefreshKind, System, SystemExt}, }; -// When querying Docker for the image IDs corresponding to a list of container IDs, this is the -// maximum number of container IDs to query at once. -const CONTAINER_IDS_CHUNK_SIZE: usize = 100; - // [tag:container_status_removing] The `docker container inspect` command seems to fail on // containers with this status. Source: https://github.com/stepchowfun/docuum/issues/237 const CONTAINER_STATUS_REMOVING: &str = "removing"; @@ -45,44 +45,6 @@ const CONTAINER_STATUSES: [&str; 7] = [ CONTAINER_STATUS_REMOVING, ]; -// A Docker event (a line of output from `docker events --format '{{json .}}'`) -#[derive(Deserialize, Serialize, Debug)] -struct Event { - #[serde(rename = "Type")] - r#type: String, - - #[serde(rename = "Action")] - action: String, - - #[serde(rename = "Actor")] - actor: EventActor, - - id: String, -} - -// A Docker event actor -#[derive(Deserialize, Serialize, Debug)] -struct EventActor { - #[serde(rename = "Attributes")] - attributes: EventActorAttributes, -} - -// Docker event actor attributes -#[derive(Deserialize, Serialize, Debug)] -struct EventActorAttributes { - image: Option, -} - -// A line of output from `docker system df --format '{{json .}}'` -#[derive(Deserialize, Serialize, Debug)] -struct SpaceRecord { - #[serde(rename = "Type")] - r#type: String, - - #[serde(rename = "Size")] - size: String, -} - // Each image may be associated with multiple of these repository-tag pairs. Docker will always // report at least one repository-tag pair for each image. For untagged images, `tag` will be // ``, and `repository` may also take on that value [tag:at_least_one_repository_tag]. @@ -112,118 +74,88 @@ struct ImageNode { } // Ask Docker for the ID of an image. -fn image_id(image: &str) -> io::Result { - // Query Docker for the image ID. - let output = Command::new("docker") - .args(["image", "inspect", "--format", "{{.ID}}", image]) - .stderr(Stdio::inherit()) - .output()?; - - // Ensure the command succeeded. - if !output.status.success() { - return Err(io::Error::other(format!( - "Unable to determine ID of image {}.", - image.code_str(), - ))); - } - - // Interpret the output bytes as UTF-8 and trim any leading/trailing whitespace. - String::from_utf8(output.stdout) - .map(|output| output.trim().to_owned()) +async fn image_id(docker: &Docker, image: &str) -> io::Result { + docker + .inspect_image(image) + .await .map_err(io::Error::other) + .and_then(|details| { + details.id.ok_or_else(|| { + io::Error::other(format!( + "Unable to determine ID of image {}.", + image.code_str(), + )) + }) + }) } // Get the ID of the parent of an image (if the parent exists), querying Docker if necessary. -fn parent_id(state: &State, image_id: &str) -> io::Result> { +async fn parent_id(docker: &Docker, state: &State, image_id: &str) -> io::Result> { // If we already know the parent, just return it. if let Some(image) = state.images.get(image_id) { return Ok(image.parent_id.clone()); } - // Query Docker for the parent image ID. - let output = Command::new("docker") - .args(["image", "inspect", "--format", "{{.Parent}}", image_id]) - .stderr(Stdio::inherit()) - .output()?; - - // Ensure the command succeeded. - if !output.status.success() { - return Err(io::Error::other(format!( - "Unable to determine ID of the parent of image {}.", - image_id.code_str(), - ))); - } - - // Interpret the output bytes as UTF-8 and trim any leading/trailing whitespace. - String::from_utf8(output.stdout) - .map(|output| { - let trimmed_output = output.trim(); - - // Does the image even have a parent? - if trimmed_output.is_empty() { - None - } else { - Some(trimmed_output.to_owned()) - } - }) + docker + .inspect_image(image_id) + .await .map_err(io::Error::other) + .map(|details| details.parent) } // Query Docker for all the images. -fn list_image_records(state: &State) -> io::Result> { +async fn list_image_records(docker: &Docker) -> io::Result> { // Get the IDs and creation timestamps of all the images. - let output = Command::new("docker") - .args([ - "image", - "ls", - "--all", - "--no-trunc", - "--format", - "{{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}", - ]) - .stderr(Stdio::inherit()) - .output()?; - - // Ensure the command succeeded. - if !output.status.success() { - return Err(io::Error::other("Unable to list images.")); - } + let images = docker + .list_images(Some(ListImagesOptions:: { + all: true, + ..Default::default() + })) + .await + .map_err(io::Error::other)?; - // Interpret the output bytes as UTF-8 and parse the lines. let mut image_records = HashMap::<_, ImageRecord>::new(); - for line in String::from_utf8(output.stdout) - .map_err(io::Error::other)? - .lines() - { - let trimmed_line = line.trim(); - - if trimmed_line.is_empty() { - continue; - } - - let image_parts = trimmed_line.split('\t').collect::>(); - if let [id, repository, tag, date_str] = image_parts[..] { - let repository_tag = RepositoryTag { - repository: repository.to_owned(), - tag: tag.to_owned(), - }; + for img in images { + let id = img.id.clone(); + let created_since_epoch = if img.created >= 0 { + #[allow(clippy::cast_sign_loss)] + Duration::from_secs(img.created as u64) + } else { + Duration::ZERO + }; - match image_records.entry(id.to_owned()) { - Entry::Occupied(mut entry) => { - (entry.get_mut()).repository_tags.push(repository_tag); - } - Entry::Vacant(entry) => { - entry.insert(ImageRecord { - parent_id: parent_id(state, id)?, - created_since_epoch: parse_docker_date(date_str)?, - repository_tags: vec![repository_tag], - }); + // Use inspect to get accurate parent and repo tags + let details = docker.inspect_image(&id).await.map_err(io::Error::other)?; + let parent = details.parent; + let repository_tags = details + .repo_tags + .unwrap_or_default() + .into_iter() + .filter_map(|rt| { + let parts = rt.rsplitn(2, ':').collect::>(); + let (repository, tag) = (parts.last(), parts.first()); + if let (Some(repository), Some(tag)) = (repository, tag) { + Some(RepositoryTag { + repository: (*repository).to_string(), + tag: (*tag).to_string(), + }) + } else { + None } + }) + .collect::>(); + + match image_records.entry(id.clone()) { + Entry::Occupied(mut entry) => { + (entry.get_mut()).repository_tags.extend(repository_tags); + } + Entry::Vacant(entry) => { + entry.insert(ImageRecord { + parent_id: parent, + created_since_epoch, + repository_tags, + }); } - } else { - return Err(io::Error::other( - "Failed to parse image list output from Docker.", - )); } } @@ -231,92 +163,32 @@ fn list_image_records(state: &State) -> io::Result> } // Ask Docker for the IDs of the images currently in use by containers. -fn image_ids_in_use() -> io::Result> { - // Query Docker for all the container IDs. - let container_ids_output = Command::new("docker") - .args( - ["container", "ls", "--all"] - .into_iter() - .map(std::string::ToString::to_string) - .chain( - CONTAINER_STATUSES - .iter() - .filter(|&&status| status != CONTAINER_STATUS_REMOVING) - .flat_map(|&status| [String::from("--filter"), format!("status={status}")]), - ) - .chain( - ["--no-trunc", "--format", "{{.ID}}"] - .into_iter() - .map(std::string::ToString::to_string), - ) - .collect::>(), - ) - .stderr(Stdio::inherit()) - .output()?; - - // Ensure the command succeeded. - if !container_ids_output.status.success() { - return Err(io::Error::other("Unable to list containers.")); - } - - // Interpret the output bytes as UTF-8 and parse the lines. - let container_ids = String::from_utf8(container_ids_output.stdout) - .map_err(io::Error::other) - .map(|output| { - output - .lines() - .filter_map(|line| { - let trimmed_line = line.trim(); - - if trimmed_line.is_empty() { - None - } else { - Some(trimmed_line.to_owned()) - } - }) - .collect::>() - })?; - - // Group the container IDs into chunks and query Docker for the image IDs for each chunk. +async fn image_ids_in_use(docker: &Docker) -> io::Result> { + // Build filter for statuses (exclude "removing" status). + let mut filters = HashMap::new(); + let status_filters = CONTAINER_STATUSES + .iter() + .filter(|&&status| status != CONTAINER_STATUS_REMOVING) + .map(|s| (*s).to_string()) + .collect::>(); + filters.insert("status".to_string(), status_filters); + + // Query Docker for all containers. + let containers = docker + .list_containers(Some(ListContainersOptions { + all: true, + filters, + ..Default::default() + })) + .await + .map_err(io::Error::other)?; + + // Extract image IDs from containers. let mut image_ids = HashSet::new(); - for chunk in container_ids.chunks(CONTAINER_IDS_CHUNK_SIZE) { - // Query Docker for the image IDs for this chunk. - let image_ids_output = Command::new("docker") - .args( - ["container", "inspect", "--format", "{{.Image}}"] - .iter() - .map(Deref::deref) - .chain(chunk.iter().map(AsRef::as_ref)), - ) - .stderr(Stdio::inherit()) - .output()?; - - // Ensure the command succeeded. - if !image_ids_output.status.success() { - return Err(io::Error::other( - "Unable to determine IDs of images currently in use by containers.", - )); + for container in containers { + if let Some(image_id) = container.image_id { + image_ids.insert(image_id); } - - // Interpret the output bytes as UTF-8 and parse the lines. - image_ids.extend( - String::from_utf8(image_ids_output.stdout) - .map_err(io::Error::other) - .map(|output| { - output - .lines() - .filter_map(|line| { - let trimmed_line = line.trim(); - - if trimmed_line.is_empty() { - None - } else { - Some(trimmed_line.to_owned()) - } - }) - .collect::>() - })?, - ); } Ok(image_ids) @@ -324,24 +196,11 @@ fn image_ids_in_use() -> io::Result> { // Determine Docker's root directory. #[cfg(target_os = "linux")] -fn docker_root_dir() -> io::Result { - // Query Docker for it. - let output = Command::new("docker") - .args(["info", "--format", "{{.DockerRootDir}}"]) - .stderr(Stdio::inherit()) - .output()?; - - // Ensure the command succeeded. - if !output.status.success() { - return Err(io::Error::other( - "Unable to determine the Docker root directory.", - )); - } - - // Trim the output. - String::from_utf8(output.stdout) - .map(|s| PathBuf::from(s.trim())) - .map_err(io::Error::other) +async fn docker_root_dir(docker: &Docker) -> io::Result { + let info: SystemInfo = docker.info().await.map_err(io::Error::other)?; + info.docker_root_dir + .map(PathBuf::from) + .ok_or_else(|| io::Error::other("Unable to determine the Docker root directory.")) } // Find the disk containing a path. @@ -361,8 +220,8 @@ fn get_disk_by_file<'a>(disks: &'a [Disk], path: &Path) -> io::Result<&'a Disk> // Find size of filesystem on which docker root directory is stored. #[cfg(target_os = "linux")] -fn docker_root_dir_filesystem_size() -> io::Result { - let root_dir = docker_root_dir()?; +async fn docker_root_dir_filesystem_size(docker: &Docker) -> io::Result { + let root_dir = docker_root_dir(docker).await?; let system = System::new_with_specifics(RefreshKind::new().with_disks_list()); let disks = system.disks(); let disk = get_disk_by_file(disks, &root_dir)?; @@ -371,71 +230,56 @@ fn docker_root_dir_filesystem_size() -> io::Result { // Get the total space used by Docker images. #[allow(clippy::map_err_ignore)] -fn space_usage() -> io::Result { - // Query Docker for the space usage. - let output = Command::new("docker") - .args(["system", "df", "--format", "{{json .}}"]) - .stderr(Stdio::inherit()) - .output()?; - - // Ensure the command succeeded. - if !output.status.success() { - return Err(io::Error::other( - "Unable to determine the disk space used by Docker images.", - )); +async fn space_usage(docker: &Docker) -> io::Result { + // Sum image sizes via inspect + let images = docker + .list_images(Some(ListImagesOptions:: { + all: true, + ..Default::default() + })) + .await + .map_err(io::Error::other)?; + + let mut total: u128 = 0; + + #[allow(clippy::cast_sign_loss)] + for img in images { + let id = img.id; + if let Ok(details) = docker.inspect_image(&id).await + && let Some(sz) = details.size + && sz > 0 + { + total = total.saturating_add(sz as u128); + } } - - // Find the relevant line of output. - String::from_utf8(output.stdout) - .map_err(io::Error::other) - .and_then(|output| { - for line in output.lines() { - // Parse the line as a space record. - if let Ok(space_record) = serde_json::from_str::(line) { - // Return early if we found the record we're looking for. - if space_record.r#type == "Images" { - return Byte::from_str(&space_record.size).map_err(|_| { - io::Error::other(format!( - "Unable to parse {} from {}.", - space_record.size.code_str(), - "docker system df".code_str(), - )) - }); - } - } - } - - Err(io::Error::other(format!( - "Unable to parse output of {}: {}", - "docker system df".code_str(), - output.code_str(), - ))) - }) + Ok(Byte::from_bytes(total)) } // Delete a Docker image. -fn delete_image(image: &str) -> io::Result<()> { +async fn delete_image(docker: &Docker, image: &str) -> io::Result<()> { info!("Deleting image {}\u{2026}", image.code_str()); - - // Tell Docker to delete the image. - let mut child = Command::new("docker") - .args(["image", "rm", "--force", "--no-prune", image]) - .spawn()?; - - // Ensure the command succeeded. - if !child.wait()?.success() { - return Err(io::Error::other(format!( - "Unable to delete image {}.", - image.code_str(), - ))); - } - - Ok(()) + docker + .remove_image( + image, + Some(RemoveImageOptions { + force: true, + noprune: true, + }), + None, + ) + .await + .map_err(io::Error::other) + .map(|_| ()) } // Update the timestamp for an image. // Returns a boolean indicating if a new entry was created for the image. -fn touch_image(state: &mut State, image_id: &str, verbose: bool) -> io::Result { +async fn touch_image( + docker: &Docker, + state: &mut State, + image_id: &str, + verbose: bool, +) -> io::Result { if verbose { debug!( "Updating last-used timestamp for image {}\u{2026}", @@ -457,7 +301,7 @@ fn touch_image(state: &mut State, image_id: &str, verbose: bool) -> io::Result io::Result io::Result { + use chrono::DateTime; + // Chrono can't read the "EST", so remove it before parsing. let timestamp_without_timezone_triad = timestamp .trim() @@ -623,7 +470,8 @@ fn construct_polyforest( } // The main vacuum logic -fn vacuum( +async fn vacuum( + docker: &Docker, state: &mut State, first_run: bool, threshold: Byte, @@ -632,10 +480,10 @@ fn vacuum( min_age: Option, ) -> io::Result<()> { // Find all images. - let image_records = list_image_records(state)?; + let image_records = list_image_records(docker).await?; // Find all images in use by containers. - let image_ids_in_use = image_ids_in_use()?; + let image_ids_in_use = image_ids_in_use(docker).await?; // Construct a polyforest of image nodes that reflects their parent-child relationships. let polyforest = construct_polyforest(state, first_run, &image_records, &image_ids_in_use)?; @@ -656,8 +504,7 @@ fn vacuum( for repository_tag in &image_node.image_record.repository_tags { if regex_set.is_match(&format!( "{}:{}", - repository_tag.repository, - repository_tag.tag, + repository_tag.repository, repository_tag.tag, )) { debug!( "Ignored image {} due to the {} flag.", @@ -697,7 +544,7 @@ fn vacuum( // Check if we're over the threshold. let mut deleted_image_ids = HashSet::new(); - let space = space_usage()?; + let space = space_usage(docker).await?; if space > threshold { info!( "Docker images are currently using {}, but the limit is {}.", @@ -709,7 +556,7 @@ fn vacuum( for image_ids in sorted_image_nodes.chunks_mut(deletion_chunk_size) { for (image_id, _) in image_ids { // Delete the image. - if let Err(error) = delete_image(image_id) { + if let Err(error) = delete_image(docker, image_id).await { // The deletion failed. Just log the error and proceed. error!("{}", error); } else { @@ -719,7 +566,7 @@ fn vacuum( } // Break if we're within the threshold. - let new_space = space_usage()?; + let new_space = space_usage(docker).await?; if new_space <= threshold { info!( "Docker images are now using {}, which is within the limit of {}.", @@ -756,27 +603,11 @@ fn vacuum( // Stream Docker events and vacuum when necessary. #[allow(clippy::type_complexity)] -pub fn run( - settings: &Settings, - state: &mut State, - first_run: &mut bool, - destructors: &Arc>>>, -) -> io::Result<()> { - // Determine the threshold in bytes. - let threshold = match settings.threshold { - Threshold::Absolute(b) => b, +pub async fn run(settings: &Settings, state: &mut State, first_run: &mut bool) -> io::Result<()> { + let docker = Docker::connect_with_local_defaults().map_err(io::Error::other)?; - #[cfg(target_os = "linux")] - Threshold::Percentage(p) => - { - #[allow( - clippy::cast_precision_loss, - clippy::cast_possible_truncation, - clippy::cast_sign_loss - )] - Byte::from_bytes((p * docker_root_dir_filesystem_size()?.get_bytes() as f64) as u128) - } - }; + // Determine the threshold in bytes. + let threshold = threshold_unit(&settings.threshold, &docker).await?; // NOTE: Don't change this log line, since the test in the Homebrew formula // (https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/d/docuum.rb) relies on it. @@ -784,92 +615,96 @@ pub fn run( // Run the main vacuum logic. vacuum( + &docker, state, *first_run, threshold, settings.keep.as_ref(), settings.deletion_chunk_size, settings.min_age, - )?; + ) + .await?; state::save(state)?; *first_run = false; - // Spawn `docker events --format '{{json .}}'`. - let mut child = Command::new("docker") - .args(["events", "--format", "{{json .}}"]) - .stdout(Stdio::piped()) // [tag:stdout] - .spawn()?; - - // Buffer the data as we read it line-by-line. The `unwrap` is safe due to [ref:stdout]. - let reader = BufReader::new(child.stdout.take().unwrap()); - - // When this run is done (e.g., due to an error) or when a termination signal is received, kill - // the child process. - destructors.lock().unwrap().push(Box::new(move || { - if let Err(error) = child.kill() { - error!("{}", error); - } else if let Err(error) = child.wait() { - error!("{}", error); - } - })); + // Stream Docker events via the API. + let mut events_stream = docker.events::(None); // Handle each incoming event. info!("Listening for Docker events\u{2026}"); - for line_option in reader.lines() { - // Unwrap the line. - let line = line_option?; - trace!("Incoming event: {}", line.code_str()); - - // Parse the line as an event. - let event = match serde_json::from_str::(&line) { - Ok(event) => { - trace!("Parsed as: {}", format!("{event:?}").code_str()); - event - } + while let Some(msg) = events_stream.next().await { + let msg: EventMessage = match msg { + Ok(m) => m, Err(error) => { - trace!("Skipping due to: {}", error); - continue; + return Err(io::Error::other(error)); } }; + trace!("Incoming event: {}", format!("{msg:?}").code_str()); // Get the ID of the image. - let image_id = image_id(&if event.r#type == "container" - && (event.action == "create" || event.action == "destroy") - { - if let Some(image_name) = event.actor.attributes.image { - image_name + let image_id = { + let typ = msg.typ; + let action = msg.action.unwrap_or_default(); + if typ == Some(EventMessageTypeEnum::CONTAINER) + && (action == "create" || action == "destroy") + { + if let Some(actor) = msg.actor { + if let Some(attrs) = actor.attributes { + if let Some(image_name) = attrs.get("image").cloned() { + image_id(&docker, &image_name).await? + } else { + trace!("Invalid Docker event."); + continue; + } + } else { + trace!("Invalid Docker event."); + continue; + } + } else { + trace!("Invalid Docker event."); + continue; + } + } else if typ == Some(EventMessageTypeEnum::IMAGE) + && (action == "import" + || action == "load" + || action == "pull" + || action == "push" + || action == "save" + || action == "tag") + { + if let Some(actor) = msg.actor { + if let Some(id) = actor.id { + id + } else { + trace!("Invalid Docker event."); + continue; + } + } else { + trace!("Invalid Docker event."); + continue; + } } else { - trace!("Invalid Docker event."); + trace!("Skipping due to irrelevance."); continue; } - } else if event.r#type == "image" - && (event.action == "import" - || event.action == "load" - || event.action == "pull" - || event.action == "push" - || event.action == "save" - || event.action == "tag") - { - event.id - } else { - trace!("Skipping due to irrelevance."); - continue; - })?; + }; // Inform the user that we're about to vacuum. debug!("Waking up\u{2026}"); // Update the timestamp for this image. - if touch_image(state, &image_id, true)? { + if touch_image(&docker, state, &image_id, true).await? { // Run the main vacuum logic only if a new image came in. vacuum( + &docker, state, *first_run, threshold, settings.keep.as_ref(), settings.deletion_chunk_size, settings.min_age, - )?; + ) + .await?; } // Persist the state. @@ -879,13 +714,38 @@ pub fn run( debug!("Going back to sleep\u{2026}"); } - // The `for` loop above will only terminate if something happened to `docker events`. + // The loop above will only terminate if something happened to the events stream. Err(io::Error::other(format!( "{} terminated.", - "docker events".code_str(), + "Docker events stream".code_str(), ))) } +#[allow(clippy::unused_async)] +#[cfg(not(target_os = "linux"))] +async fn threshold_unit(threshold: &Threshold, _docker: &Docker) -> io::Result { + let Threshold::Absolute(b) = threshold; + Ok(*b) +} + +#[cfg(target_os = "linux")] +async fn threshold_unit(threshold: &Threshold, docker: &Docker) -> io::Result { + match threshold { + Threshold::Absolute(b) => Ok(*b), + Threshold::Percentage(p) => + { + #[allow( + clippy::cast_precision_loss, + clippy::cast_possible_truncation, + clippy::cast_sign_loss + )] + Ok(Byte::from_bytes( + (p * docker_root_dir_filesystem_size(docker).await?.get_bytes() as f64) as u128, + )) + } + } +} + #[cfg(test)] mod tests { use { diff --git a/src/state.rs b/src/state.rs index 5731c98..b6b637d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,5 +1,6 @@ use { crate::format::CodeStr, + log::trace, serde::{Deserialize, Serialize}, std::{ collections::HashMap,