From 6b928d6bbf33863fa058b1c5f6ae6ca9233828a3 Mon Sep 17 00:00:00 2001 From: David Calavera <1050+calavera@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:13:48 -0800 Subject: [PATCH 1/3] Remove the dependency with Docker CLI. Change the code to use the Docker API directly, so it doesn't require additional binaries to exist in the container. Change the Dockerfile to build the application statically, allowing to build it with a Scratch image, making it distroless. That way, you don't have to worry about updating versions of the base image, because there is nothing else except the docuum binary. --- Cargo.lock | 943 ++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 6 + Dockerfile | 50 ++- src/main.rs | 40 +-- src/run.rs | 607 ++++++++++++--------------------- src/state.rs | 1 + 6 files changed, 1197 insertions(+), 450 deletions(-) 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..933e011 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,29 +196,17 @@ 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() -> io::Result { + let docker = Docker::connect_with_local_defaults().map_err(io::Error::other)?; + 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. #[cfg(target_os = "linux")] -fn get_disk_by_file<'a>(disks: &'a [Disk], path: &Path) -> io::Result<&'a Disk> { +async fn get_disk_by_file<'a>(disks: &'a [Disk], path: &Path) -> io::Result<&'a Disk> { disks .iter() .filter(|d| path.starts_with(d.mount_point())) @@ -361,81 +221,66 @@ 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() -> io::Result { + let root_dir = docker_root_dir().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)?; + let disk = get_disk_by_file(disks, &root_dir).await?; Ok(Byte::from(disk.total_space())) } // 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 +302,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 +471,7 @@ fn construct_polyforest( } // The main vacuum logic -fn vacuum( +async fn vacuum( state: &mut State, first_run: bool, threshold: Byte, @@ -632,10 +480,11 @@ fn vacuum( min_age: Option, ) -> io::Result<()> { // Find all images. - let image_records = list_image_records(state)?; + let docker = Docker::connect_with_local_defaults().map_err(io::Error::other)?; + 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 +505,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 +545,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 +557,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 +567,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 +604,9 @@ 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<()> { +pub async fn run(settings: &Settings, state: &mut State, first_run: &mut bool) -> io::Result<()> { // Determine the threshold in bytes. - let threshold = match settings.threshold { - Threshold::Absolute(b) => b, - - #[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) - } - }; + let Threshold::Absolute(threshold) = settings.threshold; // 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. @@ -790,77 +620,79 @@ pub fn run( 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 docker = Docker::connect_with_local_defaults().map_err(io::Error::other)?; + 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( state, @@ -869,7 +701,8 @@ pub fn run( settings.keep.as_ref(), settings.deletion_chunk_size, settings.min_age, - )?; + ) + .await?; } // Persist the state. @@ -879,10 +712,10 @@ 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(), ))) } 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, From 1c6691808f347a1662edc793cef297ee8e6bd6a7 Mon Sep 17 00:00:00 2001 From: David Calavera <1050+calavera@users.noreply.github.com> Date: Sat, 22 Nov 2025 10:50:13 -0800 Subject: [PATCH 2/3] Allow percentage threshold on Linux. --- src/run.rs | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/run.rs b/src/run.rs index 933e011..2f172c3 100644 --- a/src/run.rs +++ b/src/run.rs @@ -472,6 +472,7 @@ fn construct_polyforest( // The main vacuum logic async fn vacuum( + docker: &Docker, state: &mut State, first_run: bool, threshold: Byte, @@ -480,11 +481,10 @@ async fn vacuum( min_age: Option, ) -> io::Result<()> { // Find all images. - let docker = Docker::connect_with_local_defaults().map_err(io::Error::other)?; - let image_records = list_image_records(&docker).await?; + let image_records = list_image_records(docker).await?; // Find all images in use by containers. - let image_ids_in_use = image_ids_in_use(&docker).await?; + 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)?; @@ -545,7 +545,7 @@ async fn vacuum( // Check if we're over the threshold. let mut deleted_image_ids = HashSet::new(); - let space = space_usage(&docker).await?; + let space = space_usage(docker).await?; if space > threshold { info!( "Docker images are currently using {}, but the limit is {}.", @@ -557,7 +557,7 @@ async 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(&docker, image_id).await { + if let Err(error) = delete_image(docker, image_id).await { // The deletion failed. Just log the error and proceed. error!("{}", error); } else { @@ -567,7 +567,7 @@ async fn vacuum( } // Break if we're within the threshold. - let new_space = space_usage(&docker).await?; + let new_space = space_usage(docker).await?; if new_space <= threshold { info!( "Docker images are now using {}, which is within the limit of {}.", @@ -606,7 +606,24 @@ async fn vacuum( #[allow(clippy::type_complexity)] pub async fn run(settings: &Settings, state: &mut State, first_run: &mut bool) -> io::Result<()> { // Determine the threshold in bytes. - let Threshold::Absolute(threshold) = settings.threshold; + let threshold = match settings.threshold { + Threshold::Absolute(b) => b, + + #[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().await?.get_bytes() as f64) as u128, + ) + } + }; + + let docker = Docker::connect_with_local_defaults().map_err(io::Error::other)?; // 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. @@ -614,6 +631,7 @@ pub async fn run(settings: &Settings, state: &mut State, first_run: &mut bool) - // Run the main vacuum logic. vacuum( + &docker, state, *first_run, threshold, @@ -626,7 +644,6 @@ pub async fn run(settings: &Settings, state: &mut State, first_run: &mut bool) - *first_run = false; // Stream Docker events via the API. - let docker = Docker::connect_with_local_defaults().map_err(io::Error::other)?; let mut events_stream = docker.events::(None); // Handle each incoming event. @@ -695,6 +712,7 @@ pub async fn run(settings: &Settings, state: &mut State, first_run: &mut bool) - 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, From 5b7c514aabd7c5b3b500100dcc019499bf86200f Mon Sep 17 00:00:00 2001 From: David Calavera <1050+calavera@users.noreply.github.com> Date: Sat, 22 Nov 2025 11:11:20 -0800 Subject: [PATCH 3/3] Extract threshold conversion into a function. This will ensure that `cargo fix` doesn't remove the target specific code when it's not exercised. --- src/run.rs | 57 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/run.rs b/src/run.rs index 2f172c3..4e09071 100644 --- a/src/run.rs +++ b/src/run.rs @@ -196,8 +196,7 @@ async fn image_ids_in_use(docker: &Docker) -> io::Result> { // Determine Docker's root directory. #[cfg(target_os = "linux")] -async fn docker_root_dir() -> io::Result { - let docker = Docker::connect_with_local_defaults().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) @@ -206,7 +205,7 @@ async fn docker_root_dir() -> io::Result { // Find the disk containing a path. #[cfg(target_os = "linux")] -async fn get_disk_by_file<'a>(disks: &'a [Disk], path: &Path) -> io::Result<&'a Disk> { +fn get_disk_by_file<'a>(disks: &'a [Disk], path: &Path) -> io::Result<&'a Disk> { disks .iter() .filter(|d| path.starts_with(d.mount_point())) @@ -221,11 +220,11 @@ async fn get_disk_by_file<'a>(disks: &'a [Disk], path: &Path) -> io::Result<&'a // Find size of filesystem on which docker root directory is stored. #[cfg(target_os = "linux")] -async fn docker_root_dir_filesystem_size() -> io::Result { - let root_dir = docker_root_dir().await?; +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).await?; + let disk = get_disk_by_file(disks, &root_dir)?; Ok(Byte::from(disk.total_space())) } @@ -605,26 +604,11 @@ async fn vacuum( // Stream Docker events and vacuum when necessary. #[allow(clippy::type_complexity)] pub async fn run(settings: &Settings, state: &mut State, first_run: &mut bool) -> io::Result<()> { - // Determine the threshold in bytes. - let threshold = match settings.threshold { - Threshold::Absolute(b) => b, - - #[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().await?.get_bytes() as f64) as u128, - ) - } - }; - let docker = Docker::connect_with_local_defaults().map_err(io::Error::other)?; + // 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. info!("Performing an initial vacuum on startup\u{2026}"); @@ -737,6 +721,31 @@ pub async fn run(settings: &Settings, state: &mut State, first_run: &mut bool) - ))) } +#[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 {