diff --git a/Cargo.lock b/Cargo.lock index 02678db..251e002 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,21 +44,6 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "0.6.13" @@ -173,7 +158,7 @@ source = "git+https://github.com/aya-rs/aya.git#ac58601fb9073f4b5bfff5d1acf4df70 dependencies = [ "bytes", "core-error", - "hashbrown 0.14.3", + "hashbrown", "log", "object 0.34.0", "thiserror", @@ -194,18 +179,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" - [[package]] name = "bitflags" version = "1.3.2" @@ -218,56 +191,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -[[package]] -name = "bollard" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aed08d3adb6ebe0eff737115056652670ae290f177759aac19c30456135f94c" -dependencies = [ - "base64 0.22.0", - "bollard-stubs", - "bytes", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - [[package]] name = "bytes" version = "1.6.0" @@ -286,19 +209,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "num-traits", - "serde", - "windows-targets 0.52.5", -] - [[package]] name = "clap" version = "4.5.4" @@ -353,8 +263,6 @@ dependencies = [ "async-stream", "aya", "bitflags 2.5.0", - "bollard", - "bytes", "clap", "env_logger", "humantime", @@ -366,7 +274,6 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", - "tokio-util", "udev", "walkdir", ] @@ -380,22 +287,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "core-foundation-sys" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", - "serde", -] - [[package]] name = "env_filter" version = "0.1.0" @@ -419,12 +310,6 @@ dependencies = [ "log", ] -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - [[package]] name = "errno" version = "0.3.8" @@ -435,91 +320,18 @@ dependencies = [ "windows-sys 0.52.0", ] -[[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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -dependencies = [ - "futures-core", -] - [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" -[[package]] -name = "futures-io" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-macro" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" - -[[package]] -name = "futures-task" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" - -[[package]] -name = "futures-util" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" -dependencies = [ - "futures-core", - "futures-macro", - "futures-task", - "pin-project-lite", - "pin-utils", - "slab", -] - [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.3" @@ -542,182 +354,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "http" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" -[[package]] -name = "hyper" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", - "socket2", - "tokio", - "tower", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" -dependencies = [ - "equivalent", - "hashbrown 0.14.3", - "serde", -] - [[package]] name = "io-lifetimes" version = "1.0.11" @@ -735,15 +377,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" -[[package]] -name = "js-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" -dependencies = [ - "wasm-bindgen", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -814,21 +447,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[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.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" -dependencies = [ - "autocfg", -] - [[package]] name = "num_cpus" version = "1.16.0" @@ -886,56 +504,18 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[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.81" @@ -1063,46 +643,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_repr" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" -dependencies = [ - "base64 0.21.7", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.2.6", - "serde", - "serde_derive", - "serde_json", - "time", -] - [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -1112,15 +652,6 @@ dependencies = [ "libc", ] -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "smallvec" version = "1.13.2" @@ -1174,52 +705,6 @@ dependencies = [ "syn", ] -[[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.37.0" @@ -1261,78 +746,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-util" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "futures-util", - "hashbrown 0.14.3", - "pin-project-lite", - "slab", - "tokio", - "tracing", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "log", - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -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 = "udev" version = "0.8.0" @@ -1345,38 +758,12 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "url" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - [[package]] name = "utf8parse" version = "0.2.1" @@ -1399,75 +786,12 @@ dependencies = [ "winapi-util", ] -[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasm-bindgen" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" - [[package]] name = "winapi" version = "0.3.9" @@ -1499,15 +823,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.5", -] - [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index da9528f..1a7d2bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,21 +11,17 @@ anyhow = { version = "1", features = ["backtrace"] } log = "0.4" env_logger = "0.11" clap = { version = "4", features = ["derive"] } -bytes = "1" thiserror = "1" tokio = { version = "1", features = ["full"] } tokio-stream = "0.1" -tokio-util = { version = "0.7", features = ["full"] } async-stream = "0.3" udev = "0.8" -bollard = "0.16" -rustix = { version = "0.38", features = ["fs", "stdio", "termios", "process", "thread"] } +rustix = { version = "0.38", features = ["fs", "stdio", "process", "thread", "runtime", "pipe"] } bitflags = "2" once_cell = "1" humantime = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" - aya = { git = "https://github.com/aya-rs/aya.git" } [build-dependencies] diff --git a/README.md b/README.md index 03baa33..17a28e0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # container-hotplug -Hot-plug (and unplug) devices into a Docker container as they are (un)plugged. +Hot-plug (and unplug) devices into a container as they are (un)plugged. ## Description @@ -18,32 +18,82 @@ It then interfaces directly with the container's cgroup to grant it access to th To limit the devices the container can access, a _root device_ is specified. The container will receive access to any device descending from the root device. This is particularly useful if the root device is set to a USB hub. -However, since hubs are rarely interesting, it can be specified as "the parent of device X", +The hub can be specified directly, or it can be specified as "the parent of device X", e.g., we can giving a container access to all devices connected to the same hub as an Arduino board. Another concern is providing a container with well known paths for the devices. On bare-metal systems this would usually be achieved with a `SYMLINK` directive in a udev rule. This program tries to provide a similar functionality for containers, allowing you to specify symlinks for certain devices. -This tool supports both cgroup v1 and v2. +## Usage -## Example +This tool wraps `runc` command, with the additional hotplug feature. Therefore, it can be used as a drop in replace for +many container managers/orchestrators that makes use of runc as runtime. You need to ensure `runc` is available in your `PATH` +so `container-hotplug` can find it. -Give a container access to all devices connected to the same hub as a CW310 board. +It supports two annotations, `org.lowrisc.hotplug.device` and `org.lowrisc.hotplug.symlinks`. -1. Find the USB VID and PID of the device using `lsusb`, for a CW310 that is `2b3e:c310` -2. Run (as root) the container using `container-hotplug`: +For Docker, you can specify an alternative runtime by [changing /etc/docker/daemon.json](https://docs.docker.com/engine/alternative-runtimes/#youki): +```json +{ + "runtimes": { + "hotplug": { + "path": "/path/to/container-hotplug/binary" + } + } +} ``` -container-hotplug run \ - -d parent-of:usb:2b3e:c310 \ - -- -it ubuntu:22.04 bash +and use it by `--runtime hotplug` and appropriate annotation, e.g. +```bash +sudo docker run --runtime hotplug -it --annotation org.lowrisc.hotplug.device=parent-of:usb:2b2e:c310 ubuntu:latest ``` -If you want symlinks to the `tty` devices created by interfaces 1 and 3 of the CW310, run: +For podman, you can specify the path directly, by: +```bash +sudo podman run --runtime /path/to/container-hotplug/binary -it --annotation org.lowrisc.hotplug.device=parent-of:usb:2b2e:c310 ubuntu:latest ``` -container-hotplug run \ - -d parent-of:usb:2b3e:c310 \ - -l usb:2b3e:c310:1=/dev/ttyACM_CW310_0 \ - -l usb:2b3e:c310:3=/dev/ttyACM_CW310_1 \ - -- -it ubuntu:22.04 bash -``` \ No newline at end of file + +For containerd (e.g. when using kubernetes), you can `/etc/containerd/config.toml` to add: +```toml +[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.hotplug] + runtime_type = "io.containerd.runc.v2" + pod_annotations = ["org.lowrisc.hotplug.*"] + +[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.hotplug.options] + SystemdCgroup = true + BinaryName = "/path/to/container-hotplug/binary" +``` +this would allow you to use `hotplug` as handler in k8s, e.g. add a runtime class with +```yaml +apiVersion: node.k8s.io/v1 +kind: RuntimeClass +metadata: + name: hotplug +handler: hotplug +``` +and use it in pod with +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: ubuntu + annotations: + org.lowrisc.hotplug.device: usb:0bda:5634 +spec: + runtimeClassName: hotplug + containers: + - name: ubuntu + image: ubuntu:latest + stdin: true + tty: true +``` + +If you want symlinks to the `tty` devices created by interfaces 1 and 3 of the CW310, add +``` +--annotation org.lowrisc.hotplug.symlinks=usb:2b3e:c310:1=/dev/ttyACM_CW310_0,usb:2b3e:c310:3=/dev/ttyACM_CW310_1 +``` +to docker/podman command line or +``` +org.lowrisc.hotplug.symlinks: usb:2b3e:c310:1=/dev/ttyACM_CW310_0,usb:2b3e:c310:3=/dev/ttyACM_CW310_1 +``` +to k8s config. diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 0741318..8f2d2f6 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,48 +1,5 @@ pub mod device; pub mod symlink; -use clap::{Parser, Subcommand}; - pub use device::DeviceRef; pub use symlink::Symlink; - -#[derive(Parser)] -pub struct Args { - #[command(subcommand)] - pub action: Action, -} - -#[derive(Subcommand)] -#[command(max_term_width = 180)] -pub enum Action { - /// Wraps a call to `docker run` to allow hot-plugging devices into a - /// container as they are plugged - Run(Run), -} - -#[derive(clap::Args)] -pub struct Run { - #[arg(short = 'd', long, id = "DEVICE")] - /// Root hotplug device: [[parent-of:]*]: {n} - /// PREFIX can be: {n} - /// - usb: A USB device identified as [:[:]] {n} - /// - syspath: A directory path in /sys/** {n} - /// - devnode: A device path in /dev/** {n} - /// e.g., parent-of:usb:2b3e:c310 - pub root_device: DeviceRef, - - #[arg(short = 'l', long, id = "SYMLINK")] - /// Create a symlink for a device: := {n} - /// PREFIX can be: {n} - /// - usb: A USB device identified as :: {n} - /// e.g., usb:2b3e:c310:1=/dev/ttyACM_CW310_0 - pub symlink: Vec, - - #[arg(short = 'u', long, default_value = "5", id = "CODE")] - /// Exit code to return when the root device is unplugged - pub root_unplugged_exit_code: u8, - - #[arg(trailing_var_arg = true, id = "ARGS")] - /// Arguments to pass to `docker run` - pub docker_args: Vec, -} diff --git a/src/docker/container.rs b/src/docker/container.rs deleted file mode 100644 index 8b8faf9..0000000 --- a/src/docker/container.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::path::Path; -use std::pin::pin; -use std::sync::Arc; - -use anyhow::{anyhow, ensure, Context, Error, Result}; -use rustix::fs::{FileType, Gid, Mode, Uid}; -use rustix::process::{Pid, Signal}; -use tokio::signal::unix::{signal, SignalKind}; -use tokio::sync::Mutex; -use tokio::task::JoinHandle; -use tokio_stream::StreamExt; - -use super::IoStream; -use crate::cgroup::{ - Access, DeviceAccessController, DeviceAccessControllerV1, DeviceAccessControllerV2, DeviceType, -}; - -pub struct Container { - docker: bollard::Docker, - id: String, - pid: Pid, - uid: Uid, - gid: Gid, - cgroup_device_filter: Mutex>>, -} - -impl Container { - pub(super) async fn new(docker: &bollard::Docker, id: String, pid: u32) -> Result { - // Dropping the device filter will cause the container to have arbitrary device access. - // So keep it alive until we're sure that the container is stopped. - let cgroup_device_filter: Option> = - match DeviceAccessControllerV2::new( - format!("/sys/fs/cgroup/system.slice/docker-{id}.scope").as_ref(), - ) { - Ok(v) => Some(Box::new(v)), - Err(err2) => match DeviceAccessControllerV1::new( - format!("/sys/fs/cgroup/devices/docker/{id}").as_ref(), - ) { - Ok(v) => Some(Box::new(v)), - Err(err1) => { - log::error!("neither cgroup v1 and cgroup v2 works"); - log::error!("cgroup v2: {err2}"); - log::error!("cgroup v1: {err1}"); - None - } - }, - }; - - let mut this = Self { - docker: docker.clone(), - id, - pid: Pid::from_raw(pid.try_into()?).context("Invalid PID")?, - uid: Uid::ROOT, - gid: Gid::ROOT, - cgroup_device_filter: Mutex::new(cgroup_device_filter), - }; - - let uid = this.exec(&["id", "-u"]).await?.trim().parse()?; - let gid = this.exec(&["id", "-g"]).await?.trim().parse()?; - // Only invalid uid/gid for Linux is negative one. - ensure!(uid != u32::MAX && gid != u32::MAX); - // SAFETY: We just checked that the uid/gid is not -1. - this.uid = unsafe { Uid::from_raw(uid) }; - this.gid = unsafe { Gid::from_raw(gid) }; - - Ok(this) - } - - pub fn id(&self) -> &str { - &self.id - } - - pub async fn exec(&self, cmd: &[T]) -> Result { - let cmd = cmd.iter().map(|s| s.to_string()).collect(); - let options = bollard::exec::CreateExecOptions { - cmd: Some(cmd), - attach_stdin: Some(true), - attach_stdout: Some(true), - attach_stderr: Some(true), - tty: Some(true), - detach_keys: Some("ctrl-c".to_string()), - ..Default::default() - }; - let response = self.docker.create_exec::(&self.id, options).await?; - let id = response.id; - - let options = bollard::exec::StartExecOptions { - detach: false, - ..Default::default() - }; - let response = self.docker.start_exec(&id, Some(options)).await?; - let bollard::exec::StartExecResults::Attached { - input: _, - mut output, - } = response - else { - unreachable!("we asked for attached IO streams"); - }; - - let mut result = String::new(); - while let Some(output) = output.next().await { - result.push_str(&output?.to_string()); - } - Ok(result) - } - - pub async fn attach(&self) -> Result { - let options = bollard::container::AttachContainerOptions:: { - stdin: Some(true), - stdout: Some(true), - stderr: Some(true), - stream: Some(true), - logs: Some(true), - ..Default::default() - }; - - let response = self - .docker - .attach_container(&self.id, Some(options)) - .await?; - - Ok(IoStream { - output: response.output, - input: response.input, - id: self.id.clone(), - docker: self.docker.clone(), - }) - } - - pub async fn name(&self) -> Result { - let inspect = self - .docker - .inspect_container(self.id.as_ref(), None) - .await?; - let name = inspect.name.context("Failed to obtain container name")?; - Ok(name) - } - - pub async fn kill(&self, signal: Signal) -> Result<()> { - rustix::process::kill_process(self.pid, signal).context("Failed to kill container init")?; - Ok(()) - } - - pub async fn wait(&self) -> Result { - let options = bollard::container::WaitContainerOptions { - condition: "not-running", - }; - - let response = self - .docker - .wait_container(self.id.as_str(), Some(options)) - .next() - .await - .context("No response received for wait")?; - - let code = match response { - Ok(response) => response.status_code, - // If the container does not complete, e.g. it's killed, then we will receive - // an error code through docker. - Err(bollard::errors::Error::DockerContainerWaitError { error: _, code }) => code, - Err(err) => Err(err)?, - }; - - Ok(u8::try_from(code).unwrap_or(1)) - } - - pub async fn mknod(&self, node: &Path, (major, minor): (u32, u32)) -> Result<()> { - crate::util::namespace::MntNamespace::of_pid(self.pid)?.enter(|| { - if let Some(parent) = node.parent() { - let _ = std::fs::create_dir_all(parent); - } - let _ = std::fs::remove_file(node); - rustix::fs::mknodat( - rustix::fs::CWD, - node, - FileType::CharacterDevice, - Mode::from(0o644), - rustix::fs::makedev(major, minor), - )?; - if !self.uid.is_root() { - rustix::fs::chown(node, Some(self.uid), Some(self.gid))?; - } - Ok(()) - })? - } - - pub async fn symlink(&self, source: &Path, link: &Path) -> Result<()> { - crate::util::namespace::MntNamespace::of_pid(self.pid)?.enter(|| { - if let Some(parent) = link.parent() { - let _ = std::fs::create_dir_all(parent); - } - let _ = std::fs::remove_file(link); - std::os::unix::fs::symlink(source, link)?; - // No need to chown symlink. Permission is determined by the target. - Ok(()) - })? - } - - pub async fn rm(&self, node: &Path) -> Result<()> { - crate::util::namespace::MntNamespace::of_pid(self.pid)?.enter(|| { - let _ = std::fs::remove_file(node); - }) - } - - pub async fn device(&self, (major, minor): (u32, u32), access: Access) -> Result<()> { - let mut controller = self.cgroup_device_filter.lock().await; - controller - .as_mut() - .context("Device controller does not exist")? - .set_permission(DeviceType::Character, major, minor, access)?; - Ok(()) - } - - pub async fn pipe_signals(self: Arc) -> JoinHandle> { - let container = Arc::downgrade(&self); - tokio::spawn(async move { - let mut stream = pin!(signal_stream(SignalKind::alarm()) - .merge(signal_stream(SignalKind::hangup())) - .merge(signal_stream(SignalKind::interrupt())) - .merge(signal_stream(SignalKind::quit())) - .merge(signal_stream(SignalKind::terminate())) - .merge(signal_stream(SignalKind::user_defined1())) - .merge(signal_stream(SignalKind::user_defined2()))); - - while let Some(signal) = stream.next().await { - container - .upgrade() - .context("Container dropped")? - .kill(Signal::from_raw(signal?.as_raw_value()).unwrap()) - .await?; - } - - Err::<_, Error>(anyhow!("Failed to listen for signals")) - }) - } -} - -fn signal_stream(kind: SignalKind) -> impl tokio_stream::Stream> { - async_stream::try_stream! { - let sig_kind = SignalKind::hangup(); - let mut sig_stream = signal(kind)?; - while sig_stream.recv().await.is_some() { - yield sig_kind; - } - } -} diff --git a/src/docker/docker.rs b/src/docker/docker.rs deleted file mode 100644 index 15f42c2..0000000 --- a/src/docker/docker.rs +++ /dev/null @@ -1,45 +0,0 @@ -use super::Container; - -use anyhow::{ensure, Context, Result}; - -pub struct Docker(bollard::Docker); - -impl Docker { - pub fn connect_with_defaults() -> Result { - Ok(Docker(bollard::Docker::connect_with_local_defaults()?)) - } - - pub async fn get>(&self, name: T) -> Result { - let response = self.0.inspect_container(name.as_ref(), None).await?; - let id = response.id.context("Failed to obtain container ID")?; - let pid = response - .state - .context("Failed to obtain container state")? - .pid - .context("Failed to obtain container pid")?; - Container::new(&self.0, id, pid.try_into()?).await - } - - pub async fn run, T: AsRef<[U]>>(&self, args: T) -> Result { - let args = args.as_ref().iter().map(|arg| arg.as_ref()); - let args = ["run", "-d", "--rm", "--restart=no"] - .into_iter() - .chain(args); - - let output = tokio::process::Command::new("docker") - .args(args) - .stdout(std::process::Stdio::piped()) - .spawn()? - .wait_with_output() - .await?; - - ensure!( - output.status.success(), - "Failed to create container: {}", - String::from_utf8_lossy(output.stderr.as_slice()) - ); - - let id = String::from_utf8(output.stdout)?; - self.get(id.trim()).await - } -} diff --git a/src/docker/iostream.rs b/src/docker/iostream.rs deleted file mode 100644 index d07e220..0000000 --- a/src/docker/iostream.rs +++ /dev/null @@ -1,120 +0,0 @@ -use anyhow::{Context, Result}; -use async_stream::try_stream; -use bollard::container::LogOutput; -use bollard::errors::Error; -use bytes::Bytes; -use std::pin::{pin, Pin}; -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; -use tokio::signal::unix::{signal, SignalKind}; -use tokio::task::JoinHandle; -use tokio_stream::{Stream, StreamExt}; -use tokio_util::io::ReaderStream; - -pub struct IoStream { - pub output: std::pin::Pin> + Send>>, - pub input: Pin>, - pub(super) id: String, - pub(super) docker: bollard::Docker, -} - -enum StreamData { - Resize(u16, u16), - StdIn(Bytes), - StdOut(Bytes), - StdErr(Bytes), -} - -impl IoStream { - pub fn pipe_std(self) -> JoinHandle> { - let stdin = crate::util::tty_mode_guard::TtyModeGuard::new(tokio::io::stdin(), |mode| { - // Switch input to raw mode, but don't touch output modes (as it can also be connected - // to stdout and stderr). - let outmode = mode.output_modes; - mode.make_raw(); - mode.output_modes = outmode; - }); - let stdout = tokio::io::stdout(); - let stderr = tokio::io::stderr(); - - let resize_stream = try_stream! { - let mut stream = signal(SignalKind::window_change())?; - loop { - match rustix::termios::tcgetwinsize(rustix::stdio::stdout()) { - Ok(size) => yield (size.ws_row, size.ws_col), - _ => {}, - } - stream.recv().await; - } - }; - - self.pipe(stdin, stdout, stderr, resize_stream) - } - - pub fn pipe( - self, - stdin: impl AsyncRead + Send + 'static, - stdout: impl AsyncWrite + Send + 'static, - stderr: impl AsyncWrite + Send + 'static, - resize_stream: impl Stream> + Send + 'static, - ) -> JoinHandle> { - let mut input = self.input; - let docker = self.docker; - let id = self.id; - - let resize_stream = resize_stream.map(|data| { - let (rows, cols) = data.context("Listening for tty resize")?; - Ok(StreamData::Resize(rows, cols)) - }); - - let input_stream = ReaderStream::new(stdin).map(|data| { - Ok(StreamData::StdIn( - data.context("Reading container input stream")?, - )) - }); - - let output_stream = self.output.filter_map(|output| match output { - Ok(LogOutput::Console { message }) => Some(Ok(StreamData::StdOut(message))), - Ok(LogOutput::StdOut { message }) => Some(Ok(StreamData::StdOut(message))), - Ok(LogOutput::StdErr { message }) => Some(Ok(StreamData::StdErr(message))), - Err(err) => Some(Err(err).context("Reading container output stream")), - _ => None, - }); - - tokio::spawn(async move { - let mut streams = pin!(resize_stream.merge(input_stream).merge(output_stream)); - let mut stdout = pin!(stdout); - let mut stderr = pin!(stderr); - - while let Some(data) = streams.next().await { - match data? { - StreamData::Resize(rows, cols) => { - resize_tty(&docker, &id, (rows, cols)).await?; - } - StreamData::StdIn(mut buf) => { - input.write_all_buf(&mut buf).await?; - input.flush().await?; - } - StreamData::StdOut(mut buf) => { - stdout.write_all_buf(&mut buf).await?; - stdout.flush().await?; - } - StreamData::StdErr(mut buf) => { - stderr.write_all_buf(&mut buf).await?; - stdout.flush().await?; - } - }; - } - - Ok(()) - }) - } -} - -async fn resize_tty(docker: &bollard::Docker, id: &str, (rows, cols): (u16, u16)) -> Result<()> { - let options = bollard::container::ResizeContainerTtyOptions { - height: rows, - width: cols, - }; - docker.resize_container_tty(id, options).await?; - Ok(()) -} diff --git a/src/docker/mod.rs b/src/docker/mod.rs deleted file mode 100644 index 32c48f5..0000000 --- a/src/docker/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod container; -mod docker; -mod iostream; - -pub use container::Container; -pub use docker::Docker; -pub use iostream::IoStream; diff --git a/src/hotplug/mod.rs b/src/hotplug/mod.rs index 1e0b94b..3c03ac3 100644 --- a/src/hotplug/mod.rs +++ b/src/hotplug/mod.rs @@ -13,7 +13,7 @@ use super::Event; use crate::cgroup::Access; use crate::cli; use crate::dev::{DeviceEvent, DeviceMonitor}; -use crate::docker::Container; +use crate::runc::Container; pub struct HotPlug { pub container: Arc, diff --git a/src/main.rs b/src/main.rs index f8c5a4c..1f040da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,17 @@ mod cgroup; mod cli; mod dev; -mod docker; mod hotplug; mod runc; mod util; -use cli::Action; -use docker::Docker; +use cli::{DeviceRef, Symlink}; use hotplug::{AttachedDevice, HotPlug}; use std::fmt::Display; +use std::fs::File; use std::mem::ManuallyDrop; +use std::os::unix::process::CommandExt; use std::pin::pin; use std::process::ExitCode; use std::sync::Arc; @@ -19,7 +19,12 @@ use std::sync::Arc; use anyhow::{bail, Context, Result}; use clap::Parser; use log::info; -use rustix::process::Signal; +use runc::cli::{CreateOptions, GlobalOptions}; +use runc::Container; +use rustix::fd::OwnedFd; +use rustix::pipe::PipeFlags; +use rustix::process::{Signal, WaitOptions}; +use rustix::runtime::Fork; use tokio_stream::StreamExt; #[derive(Clone)] @@ -27,7 +32,7 @@ enum Event { Attach(AttachedDevice), Detach(AttachedDevice), Initialized, - Stopped(u8), + Stopped, } impl Display for Event { @@ -42,38 +47,75 @@ impl Display for Event { Event::Initialized => { write!(f, "Container initialized") } - Event::Stopped(status) => { - write!(f, "Container exited with status {status}") + Event::Stopped => { + write!(f, "Container stopped") } } } } -async fn run(param: cli::Run) -> Result { - let hub_path = param.root_device.device()?.syspath().to_owned(); +async fn create(global: GlobalOptions, create: CreateOptions, notifier: OwnedFd) -> Result<()> { + let mut notifier = Some(notifier); - let docker = Docker::connect_with_defaults()?; - let container = Arc::new(docker.run(param.docker_args).await?); - // Dropping the `Container` will detach the device cgroup filter. - // To prevent accidentally detaching it, wrap it in `ManuallyDrop` and only do so - // when we're certain that the container stopped. + let config = runc::config::Config::from_bundle(&create.bundle)?; + let device: DeviceRef = config + .annotations + .get("org.lowrisc.hotplug.device") + .context( + "Cannot find annotation `org.lowrisc.hotplug.device`. Please use normal runc instead.", + )? + .parse()?; + + let mut symlinks = Vec::::new(); + if let Some(symlink_annotation) = config.annotations.get("org.lowrisc.hotplug.symlinks") { + for symlink in symlink_annotation.split(',') { + symlinks.push(symlink.parse()?); + } + } + + // Run this before calling into runc to create the container. + let hub_path = device.device()?.syspath().to_owned(); + + // Switch the logger to syslog. The runc logs are barely forwarded to the user or syslog by + // container managers and orchestrators, while we do want to preserve the hotplug events. + util::log::global_replace(Box::new(util::log::SyslogLogger::new()?)); + + // Delegate to runc to create container. + let runc = std::process::Command::new("runc") + .args(std::env::args().skip(1)) + .spawn() + .context("Cannot start runc")? + .wait() + .context("Failed waiting for runc")?; + if !runc.success() { + std::process::exit(runc.code().unwrap_or(1)); + } + + let state: runc::state::State = + runc::state::State::from_root_and_id(&global.root, &create.container_id)?; + + // Create a container handler. + // To avoid race where the container is deleted before the daemon is started, do this + // before forking. + let container = Arc::new(Container::new(&config, &state)?); + // Prevent the container's destructor from being executed in abnormal exit. let container_keep = ManuallyDrop::new(container.clone()); - drop(container.clone().pipe_signals()); - info!( - "Attaching to container {} ({})", - container.name().await?, - container.id() - ); + // Before the parent process returns, we need to ensure that all stdio are redirected, otherwise it can cause + // issue to the spawning process since it will be unable to read EOF. Redirect all of them to /dev/null. + let null = File::options().append(true).open("/dev/null")?; + rustix::stdio::dup2_stdin(&null)?; + rustix::stdio::dup2_stdout(&null)?; + rustix::stdio::dup2_stderr(null)?; - let mut hotplug = HotPlug::new(container.clone(), hub_path.clone(), param.symlink)?; + let mut hotplug = HotPlug::new(Arc::clone(&container), hub_path.clone(), symlinks)?; let hotplug_stream = hotplug.run(); let container_stream = { let container = container.clone(); async_stream::try_stream! { - let status = container.wait().await?; - yield Event::Stopped(status) + container.wait().await?; + yield Event::Stopped; } }; @@ -81,36 +123,31 @@ async fn run(param: cli::Run) -> Result { .merge(hotplug_stream) .merge(container_stream)); - let status = loop { + loop { let event = stream.try_next().await?.context("No more events")?; info!("{}", event); match event { Event::Initialized => { - container.attach().await?.pipe_std(); + // Notify the parent process that we are ready to proceed. + let notifier = notifier.take().context("Initialized event seen twice")?; + rustix::io::write(notifier, &[0])?; } Event::Detach(dev) if dev.syspath() == hub_path => { info!("Hub device detached. Stopping container."); - container.kill(Signal::Kill).await?; - - let _ = container.wait().await?; - break param.root_unplugged_exit_code; + let _ = container.kill(Signal::Kill).await; + container.wait().await?; + break; } - Event::Stopped(code) => { - // Use the container exit code, but only if it won't be confused - // with the pre-defined root_unplugged_exit_code. - if code != param.root_unplugged_exit_code { - break code; - } else { - break 1; - } + Event::Stopped => { + break; } _ => {} } - }; + } drop(ManuallyDrop::into_inner(container_keep)); - Ok(status) + Ok(()) } fn initialize_logger() { @@ -126,8 +163,25 @@ fn initialize_logger() { util::log::global_replace(Box::new(logger)); } -fn do_main() -> Result { - let args = cli::Args::parse(); +fn do_main() -> Result<()> { + // This program is a wrapper around runc. + // + // When a container is started, runc is executed multiple times with different verbs: + // * "create": create cgroup for the container + // * "start": start the entrypoint + // * "kill": kill the container (skipped if the container init exits cleanly) + // * "delete": delete the cgroup + // + // We want to ensure that hotplug controller runs after the cgroup is created and last + // until the cgroup is deleted. So we start a daemon process when we received the + // "create" verb. + // + // To avoid having to communicate with the daemon process, the process + // itself monitors the state of the container and exits when the cgroup is removed. + // Therefore, we only need to intercept "create" command, and the rest can be forwarded + // to runc directly. + + let args = runc::cli::Command::parse(); initialize_logger(); @@ -136,22 +190,68 @@ fn do_main() -> Result { bail!("This program must be run as root"); } - let param = match args.action { - Action::Run(param) => param, + // Switch log target if specified. + if let Some(log) = &args.global.log { + let log_target = File::options() + .create(true) + .append(true) + .open(log) + .context("Cannot open log file")?; + if args.global.log_format == runc::cli::LogFormat::Json { + util::log::global_replace(Box::new(runc::log::JsonLogger::new(Box::new(log_target)))); + } else { + rustix::stdio::dup2_stderr(log_target)?; + } + } + + let create_options = match args.command { + runc::cli::Subcommand::Create(create_options) => create_options, + runc::cli::Subcommand::Run { .. } => { + // `run` is a shorthand for `create` and `start`. + // It is never used by containerd shims, so there is no need to support it. + bail!("container-hotplug does not support `run` command."); + } + runc::cli::Subcommand::Other(_) => Err(std::process::Command::new("runc") + .args(std::env::args().skip(1)) + .exec())?, }; - let rt = tokio::runtime::Runtime::new()?; - let result = rt.block_on(run(param)); - rt.shutdown_background(); - result + // We need a daemon process to handle hotplug events. + // To avoid having to serialize and deserialize all information, `fork`-ing is easiest. + // However there is a limitation that `fork` is only safe in a single-threaded program. + // `tokio` runtime is multithreaded, so we need to `fork` before that. + // However for early forking, we need to know whether a container is successfully created. + // We do this by creating a pipe. The pipe can be closed by either the child process exiting, + // or the child process closing it. + let (parent, child) = rustix::pipe::pipe_with(PipeFlags::CLOEXEC)?; + // SAFETY: forking is safe because we are single-threaded now. + match unsafe { rustix::runtime::fork()? } { + Fork::Child(_) => { + drop(parent); + tokio::runtime::Runtime::new()?.block_on(create(args.global, create_options, child))?; + } + Fork::Parent(pid) => { + drop(child); + if rustix::io::read(parent, &mut [0])? == 0 { + // In this case, the child process exited before notifying us. + std::process::exit( + rustix::process::waitpid(Some(pid), WaitOptions::empty())? + .unwrap() + .exit_status() + .unwrap_or(1) as _, + ); + } + } + } + + Ok(()) } fn main() -> ExitCode { - match do_main() { - Ok(code) => code.into(), - Err(err) => { - log::error!("{:?}", err); - ExitCode::from(125) - } + if let Err(err) = do_main() { + log::error!("{:?}", err); + ExitCode::FAILURE + } else { + ExitCode::SUCCESS } } diff --git a/src/runc/container.rs b/src/runc/container.rs new file mode 100644 index 0000000..0cd54ea --- /dev/null +++ b/src/runc/container.rs @@ -0,0 +1,157 @@ +use std::fs::File; +use std::io::{BufRead, BufReader, Seek}; +use std::path::Path; + +use anyhow::{bail, ensure, Context, Result}; +use rustix::fs::{FileType, Gid, Mode, Uid}; +use rustix::process::{Pid, Signal}; +use tokio::io::unix::AsyncFd; +use tokio::io::Interest; +use tokio::sync::Mutex; + +use crate::cgroup::{ + Access, DeviceAccessController, DeviceAccessControllerV1, DeviceAccessControllerV2, DeviceType, +}; + +struct CgroupEventNotifier { + file: AsyncFd, +} + +impl CgroupEventNotifier { + fn new(cgroup: &Path) -> Result { + let file = AsyncFd::with_interest( + File::open(cgroup.join("cgroup.events")).context("Cannot open cgroup.events")?, + Interest::PRIORITY | Interest::ERROR, + )?; + Ok(Self { file }) + } + + fn populated(&mut self) -> Result { + let file = self.file.get_mut(); + file.seek(std::io::SeekFrom::Start(0)) + .context("Cannot seek to start")?; + for line in BufReader::new(file).lines() { + let line = line.context("Cannot read line")?; + if line.starts_with("populated ") { + return Ok(line.ends_with('1')); + } + } + bail!("Cannot find populated field"); + } + + pub async fn wait(&mut self) -> Result<()> { + if !self.populated()? { + return Ok(()); + } + + loop { + self.file + .ready(Interest::PRIORITY | Interest::ERROR) + .await? + .clear_ready(); + + if !self.populated()? { + return Ok(()); + } + } + } +} + +pub struct Container { + uid: Uid, + gid: Gid, + pid: Pid, + wait: tokio::sync::watch::Receiver, + cgroup_device_filter: Mutex>, +} + +impl Container { + pub fn new(config: &super::config::Config, state: &super::state::State) -> Result { + let (send, recv) = tokio::sync::watch::channel(false); + let mut notifier = CgroupEventNotifier::new(&state.cgroup_paths.unified)?; + tokio::task::spawn(async move { + if notifier.wait().await.is_ok() { + send.send_replace(true); + } + }); + + let cgroup_device_filter: Box = + if let Some(device_cgroup) = &state.cgroup_paths.devices { + Box::new(DeviceAccessControllerV1::new(device_cgroup)?) + } else { + Box::new(DeviceAccessControllerV2::new(&state.cgroup_paths.unified)?) + }; + + ensure!(config.process.user.uid != u32::MAX && config.process.user.gid != u32::MAX); + + Ok(Self { + uid: unsafe { Uid::from_raw(config.process.user.uid) }, + gid: unsafe { Gid::from_raw(config.process.user.gid) }, + pid: Pid::from_raw(state.init_process_pid.try_into()?).context("Invalid PID")?, + wait: recv, + cgroup_device_filter: Mutex::new(cgroup_device_filter), + }) + } + + pub async fn kill(&self, signal: Signal) -> Result<()> { + rustix::process::kill_process(self.pid, signal)?; + Ok(()) + } + + pub async fn wait(&self) -> Result<()> { + self.wait + .clone() + .wait_for(|state| *state) + .await + .context("Failed to wait for container")?; + Ok(()) + } + + pub async fn mknod(&self, node: &Path, (major, minor): (u32, u32)) -> Result<()> { + crate::util::namespace::MntNamespace::of_pid(self.pid)?.enter(|| { + if let Some(parent) = node.parent() { + let _ = std::fs::create_dir_all(parent); + } + let _ = std::fs::remove_file(node); + rustix::fs::mknodat( + rustix::fs::CWD, + node, + FileType::CharacterDevice, + Mode::from(0o644), + rustix::fs::makedev(major, minor), + )?; + if !self.uid.is_root() { + rustix::fs::chown(node, Some(self.uid), Some(self.gid))?; + } + Ok(()) + })? + } + + pub async fn symlink(&self, source: &Path, link: &Path) -> Result<()> { + crate::util::namespace::MntNamespace::of_pid(self.pid)?.enter(|| { + if let Some(parent) = link.parent() { + let _ = std::fs::create_dir_all(parent); + } + let _ = std::fs::remove_file(link); + std::os::unix::fs::symlink(source, link)?; + // No need to chown symlink. Permission is determined by the target. + Ok(()) + })? + } + + pub async fn rm(&self, node: &Path) -> Result<()> { + crate::util::namespace::MntNamespace::of_pid(self.pid)?.enter(|| { + let _ = std::fs::remove_file(node); + }) + } + + pub async fn device(&self, (major, minor): (u32, u32), access: Access) -> Result<()> { + self.cgroup_device_filter.lock().await.set_permission( + DeviceType::Character, + major, + minor, + access, + )?; + Ok(()) + } +} diff --git a/src/runc/mod.rs b/src/runc/mod.rs index 3048d96..66fd3fd 100644 --- a/src/runc/mod.rs +++ b/src/runc/mod.rs @@ -2,3 +2,6 @@ pub mod cli; pub mod config; pub mod log; pub mod state; + +mod container; +pub use container::Container; diff --git a/src/util/mod.rs b/src/util/mod.rs index 8cfb6d2..f12ae75 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,4 +1,3 @@ pub mod escape; pub mod log; pub mod namespace; -pub mod tty_mode_guard; diff --git a/src/util/tty_mode_guard.rs b/src/util/tty_mode_guard.rs deleted file mode 100644 index c41f30c..0000000 --- a/src/util/tty_mode_guard.rs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright lowRISC contributors. -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -use std::io::{IsTerminal, Result}; -use std::ops::{Deref, DerefMut}; -use std::os::fd::{AsFd, BorrowedFd}; -use std::pin::Pin; - -use rustix::termios::{self, Termios}; -use tokio::io::AsyncRead; - -/// TTY mode guard over any file descriptor. -/// -/// The fd is converted into selected terminal mode and restored to its original state on drop, if it's a terminal. -/// For non-terminal fds, nothing will be performed. -pub struct TtyModeGuard { - termios: Option, - fd: T, -} - -impl Deref for TtyModeGuard { - type Target = T; - - fn deref(&self) -> &T { - &self.fd - } -} - -impl DerefMut for TtyModeGuard { - fn deref_mut(&mut self) -> &mut T { - &mut self.fd - } -} - -impl AsFd for TtyModeGuard { - fn as_fd(&self) -> BorrowedFd<'_> { - self.fd.as_fd() - } -} - -impl AsyncRead for TtyModeGuard { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> std::task::Poll> { - // SAFETY: structural projection - unsafe { self.map_unchecked_mut(|x| &mut x.fd) }.poll_read(cx, buf) - } -} - -impl TtyModeGuard { - pub fn new(fd: T, mode: impl FnOnce(&mut Termios)) -> Self { - let termios = if fd.as_fd().is_terminal() { - let termios: Result<_> = (|| { - let termios = termios::tcgetattr(&fd)?; - - let mut new_termios = termios.clone(); - mode(&mut new_termios); - termios::tcsetattr(&fd, termios::OptionalActions::Now, &new_termios)?; - - Ok(termios) - })(); - - match termios { - Ok(termios) => Some(termios), - Err(err) => { - log::warn!("Failed to set terminal mode: {}", err); - None - } - } - } else { - None - }; - - Self { termios, fd } - } -} - -impl Drop for TtyModeGuard { - fn drop(&mut self) { - if let Some(termios) = self.termios.as_ref() { - if let Err(err) = termios::tcsetattr(&self.fd, termios::OptionalActions::Now, termios) { - log::warn!("Failed to restore terminal mode: {}", err); - } - } - } -}