diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e168758..8dc9b51 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,12 +15,15 @@ jobs: steps: - uses: actions/checkout@v2 - run: rustup update stable && rustup default stable + - run: sudo apt-get install libdbus-1-dev pkg-config libdbus-1-3 - run: rustup component add rustfmt - run: cargo fmt --all -- --check clippy_check: runs-on: ubuntu-latest steps: + - name: Install dbus dependencies + run: sudo apt-get install libdbus-1-dev pkg-config libdbus-1-3 - uses: actions/checkout@v1 - uses: actions-rs/toolchain@v1 with: @@ -36,6 +39,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - name: Install dbus dependencies + run: sudo apt-get install libdbus-1-dev pkg-config libdbus-1-3 - name: Build run: cargo build --verbose - name: Run tests diff --git a/Cargo.lock b/Cargo.lock index c41363a..b1da201 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,9 +106,9 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25f9db3b38af870bf7e5cc649167533b493928e50744e2c30ae350230b414670" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -117,9 +117,9 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3548b8efc9f8e8a5a0a2808c5bd8451a9031b9e5b879a79590304ae928b0a70" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -128,9 +128,9 @@ version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -461,15 +461,25 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "dbus" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1334c0161ddfccd239ac81b188d62015b049c986c5cd0b7f9447cf2c54f4a3" +dependencies = [ + "libc", + "libdbus-sys", +] + [[package]] name = "derivative" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb582b60359da160a9477ee80f15c8d784c477e69c217ef2cdd4169c24ea380f" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -499,6 +509,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.5" @@ -506,7 +526,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" dependencies = [ "libc", - "redox_users", + "redox_users 0.3.5", + "winapi 0.3.9", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.0", "winapi 0.3.9", ] @@ -570,7 +601,7 @@ checksum = "0c122a393ea57648015bf06fbd3d372378992e86b9ff5a7a497b076a28c79efe" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.1.57", "winapi 0.3.9", ] @@ -719,9 +750,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556" dependencies = [ "proc-macro-hack", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -1201,10 +1232,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd71bf282e5551ac0852afcf25352b7fb8dd9a66eed7b6e66a6ebbf6b5b2f475" dependencies = [ "Inflector", - "proc-macro2", - "quote", + "proc-macro2 1.0.24", + "quote 1.0.7", "serde_json", - "syn", + "syn 1.0.50", ] [[package]] @@ -1279,8 +1310,8 @@ name = "kubelet-derive" version = "0.1.0" source = "git+https://github.com/deislabs/krustlet.git?rev=ac218b38ba564de806568e49d9e38aaef9f41537#ac218b38ba564de806568e49d9e38aaef9f41537" dependencies = [ - "quote", - "syn", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -1307,6 +1338,15 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +[[package]] +name = "libdbus-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc12a3bc971424edbbf7edaf6e5740483444db63aa8e23d3751ff12a30f306f0" +dependencies = [ + "pkg-config", +] + [[package]] name = "linked-hash-map" version = "0.5.3" @@ -1671,9 +1711,9 @@ checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" dependencies = [ "pest", "pest_meta", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -1697,6 +1737,48 @@ dependencies = [ "indexmap", ] +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +dependencies = [ + "phf_shared", + "rand 0.6.5", +] + +[[package]] +name = "phf_macros" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb45e833315153371697760dad1831da99ce41884162320305e4f123ca3fe37" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "0.4.27" @@ -1721,9 +1803,9 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -1732,9 +1814,9 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -1863,9 +1945,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", "version_check 0.9.2", ] @@ -1875,8 +1957,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.24", + "quote 1.0.7", "version_check 0.9.2", ] @@ -1892,6 +1974,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + [[package]] name = "proc-macro2" version = "1.0.24" @@ -1937,9 +2028,9 @@ checksum = "537aa19b95acde10a12fec4301466386f757403de4cd4e5b4fa78fb5ecb18f72" dependencies = [ "anyhow", "itertools", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -1964,13 +2055,22 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ac73b1112776fc109b2e61909bc46c7e1bf0d7f690ffb1676553acce16d5cda" +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + [[package]] name = "quote" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" dependencies = [ - "proc-macro2", + "proc-macro2 1.0.24", ] [[package]] @@ -2157,6 +2257,15 @@ version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +[[package]] +name = "redox_syscall" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" +dependencies = [ + "bitflags 1.2.1", +] + [[package]] name = "redox_users" version = "0.3.5" @@ -2164,10 +2273,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" dependencies = [ "getrandom 0.1.15", - "redox_syscall", + "redox_syscall 0.1.57", "rust-argon2", ] +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.0", + "redox_syscall 0.2.4", +] + [[package]] name = "regex" version = "1.4.2" @@ -2264,10 +2383,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dec448bc157977efdc0a71369cf923915b0c4806b1b2449c3fb011071d6f7c38" dependencies = [ "cfg-if 0.1.10", - "proc-macro2", - "quote", + "proc-macro2 1.0.24", + "quote 1.0.7", "rustc_version", - "syn", + "syn 1.0.50", ] [[package]] @@ -2420,9 +2539,9 @@ version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -2503,6 +2622,15 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +[[package]] +name = "shellexpand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bdb7831b2d85ddf4a7b148aa19d0587eddbe8671a436b7bd1182eaad0f2829" +dependencies = [ + "dirs-next", +] + [[package]] name = "signal-hook-registry" version = "1.2.2" @@ -2512,6 +2640,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + [[package]] name = "slab" version = "0.4.2" @@ -2542,9 +2676,9 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7073448732a89f2f3e6581989106067f403d378faeafb4a50812eb814170d3e5" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -2555,7 +2689,7 @@ checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.1.57", "winapi 0.3.9", ] @@ -2571,6 +2705,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "dbus", "env_logger", "flate2", "handlebars", @@ -2582,6 +2717,7 @@ dependencies = [ "kubelet", "log 0.4.11", "oci-distribution", + "phf", "pnet", "reqwest", "rstest", @@ -2589,6 +2725,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_yaml", + "shellexpand", "stackable_config", "tar", "thiserror", @@ -2641,11 +2778,11 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.24", + "quote 1.0.7", "serde", "serde_derive", - "syn", + "syn 1.0.50", ] [[package]] @@ -2655,13 +2792,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" dependencies = [ "base-x", - "proc-macro2", - "quote", + "proc-macro2 1.0.24", + "quote 1.0.7", "serde", "serde_derive", "serde_json", "sha1", - "syn", + "syn 1.0.50", ] [[package]] @@ -2695,9 +2832,20 @@ checksum = "65e51c492f9e23a220534971ff5afc14037289de430e3c83f9daf6a1b6ae91e8" dependencies = [ "heck", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", +] + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", ] [[package]] @@ -2706,8 +2854,8 @@ version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443b4178719c5a851e1bde36ce12da21d74a0e60b4d982ec3385a933c812f0f6" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.24", + "quote 1.0.7", "unicode-xid 0.2.1", ] @@ -2768,7 +2916,7 @@ checksum = "489997b7557e9a43e192c527face4feacc78bfbe6eed67fd55c4c9e381cba290" dependencies = [ "filetime", "libc", - "redox_syscall", + "redox_syscall 0.1.57", "xattr", ] @@ -2781,7 +2929,7 @@ dependencies = [ "cfg-if 0.1.10", "libc", "rand 0.7.3", - "redox_syscall", + "redox_syscall 0.1.57", "remove_dir_all", "winapi 0.3.9", ] @@ -2840,9 +2988,9 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -2897,10 +3045,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" dependencies = [ "proc-macro-hack", - "proc-macro2", - "quote", + "proc-macro2 1.0.24", + "quote 1.0.7", "standback", - "syn", + "syn 1.0.50", ] [[package]] @@ -2955,9 +3103,9 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -3045,10 +3193,10 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19970cf58f3acc820962be74c4021b8bbc8e8a1c4e3a02095d0aa60cde5f3633" dependencies = [ - "proc-macro2", + "proc-macro2 1.0.24", "prost-build", - "quote", - "syn", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -3248,9 +3396,9 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e0ccfc3378da0cce270c946b676a376943f5cd16aeba64568e7939806f4ada" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", ] [[package]] @@ -3381,6 +3529,12 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36dff09cafb4ec7c8cf0023eb0b686cb6ce65499116a12201c9e11840ca01beb" +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + [[package]] name = "unicode-xid" version = "0.2.1" @@ -3550,9 +3704,9 @@ dependencies = [ "bumpalo", "lazy_static", "log 0.4.11", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", "wasm-bindgen-shared", ] @@ -3574,7 +3728,7 @@ version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" dependencies = [ - "quote", + "quote 1.0.7", "wasm-bindgen-macro-support", ] @@ -3584,9 +3738,9 @@ version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.50", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3617,8 +3771,8 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8fb9c67be7439ee8ab1b7db502a49c05e51e2835b66796c705134d9b8e1a585" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.24", + "quote 1.0.7", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0c0ce56..a38b694 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,9 +29,17 @@ thiserror = "1.0" url = "2.2" pnet = "0.26.0" stackable_config = { git = "https://github.com/stackabletech/common.git", branch = "main" } +phf = { version = "0.7.24", features = ["macros"] } +dbus = "0.9.0" hostname = "0.3" +shellexpand = "2.1" [dev-dependencies] indoc = "1.0" rstest = "0.6" serde_yaml = "0.8" + +[profile.release] +opt-level = "s" +lto = true +codegen-units = 1 diff --git a/README.adoc b/README.adoc index aa7f496..ffb46a3 100644 --- a/README.adoc +++ b/README.adoc @@ -38,6 +38,27 @@ After rust is installed simply run To create the binary file in _target/debug_. +==== Build dependencies +The agent depends on native dbus libraries to communicate with systemd. +For the build process to work these need to be installed on the build system. + + + +|=== +|Distribution |Package Names + +|Ubuntu +a|- libdbus-1-dev +- pkg-config +- libdbus-1-3 + +|Centos +a|- dbus-devel + +|=== + + + === Download binary We do not at this time provide pre-compiled binaries, as we are still in the process of setting up the build jobs to create these. This readme will be updated as soon as binary downloads are available! diff --git a/src/bin/agent.rs b/src/bin/agent.rs index 011a440..07566ba 100644 --- a/src/bin/agent.rs +++ b/src/bin/agent.rs @@ -57,7 +57,7 @@ async fn main() -> anyhow::Result<()> { info!("args: {:?}", env::args()); let server_config = ServerConfig { - addr: agent_config.server_ip_address.clone(), + addr: agent_config.server_ip_address, port: agent_config.server_port, cert_file: agent_config.server_cert_file.unwrap_or_default(), private_key_file: agent_config.server_key_file.unwrap_or_default(), @@ -68,8 +68,8 @@ async fn main() -> anyhow::Result<()> { hostname: agent_config.hostname.clone(), node_name: agent_config.hostname, server_config, - data_dir: agent_config.data_directory, - plugins_dir: Default::default(), + data_dir: agent_config.data_directory.clone(), + plugins_dir: agent_config.data_directory.join("plugins"), node_labels: agent_config.tags, // TODO: Discuss whether we want this configurable or leave it at a high number for now max_pods: 110, @@ -87,6 +87,7 @@ async fn main() -> anyhow::Result<()> { agent_config.parcel_directory.clone(), agent_config.config_directory.clone(), agent_config.log_directory.clone(), + agent_config.session, agent_config.pod_cidr, ) .await diff --git a/src/bin/generate_doc.rs b/src/bin/generate_doc.rs index 7396294..e161fe1 100644 --- a/src/bin/generate_doc.rs +++ b/src/bin/generate_doc.rs @@ -1,4 +1,4 @@ -/// This is a helper binary which generates the file _documentation/commandline_args.adoc_ which +/// This is a helper binary which generates the file `documentation/commandline_args.adoc` which /// contains documentation of the available command line options for the agent binary. /// /// It gets the content by calling [`stackable_agent::config::AgentConfig::get_documentation()`] diff --git a/src/config/config_documentation/session.adoc b/src/config/config_documentation/session.adoc new file mode 100644 index 0000000..ce125fb --- /dev/null +++ b/src/config/config_documentation/session.adoc @@ -0,0 +1,12 @@ +This parameter specifies whether to use a session or the system DBus connection when talking to systemd. +For our purposps the difference between the two can be explained as the session bus being restricted to the current user, whereas the system bus rolls out services that are available for every user. +In reality is is a bit more involved than that, please refer to the https://dbus.freedesktop.org/doc/dbus-specification.html[official docs] for more information. + +When this flag is specified it causes symlinks for loaded services to be created in the currently active users systemd directory `~/.config/systemd/user` instead of one of the globally valid locations: + +- `/lib/systemd/system` +- `/etc/systemd/system` + +The default is to use the system bus, for which it is necessary that the agent either run as root or have passwordless sudo rights. + +Using the session bus will mainly be useful for scenarios without root access and for testing on developer machines. \ No newline at end of file diff --git a/src/config/mod.rs b/src/config/mod.rs index 178e243..d47aace 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -33,6 +33,7 @@ pub struct AgentConfig { pub server_cert_file: Option, pub server_key_file: Option, pub tags: HashMap, + pub session: bool, pub pod_cidr: String, } @@ -159,6 +160,16 @@ impl AgentConfig { list: true }; + pub const SESSION_SYSTEMD: ConfigOption = ConfigOption { + name: "session", + default: None, + required: false, + takes_argument: false, + help: "When specified causes the agent to run services in the session instance of systemd, not the system wide systemd.", + documentation: include_str!("config_documentation/session.adoc"), + list: false + }; + pub const POD_CIDR: ConfigOption = ConfigOption { name: "pod-cidr", default: Some(""), @@ -183,6 +194,7 @@ impl AgentConfig { AgentConfig::NO_CONFIG, AgentConfig::TAG, AgentConfig::BOOTSTRAP_FILE, + AgentConfig::SESSION_SYSTEMD, AgentConfig::POD_CIDR, ] .iter() @@ -260,6 +272,7 @@ impl AgentConfig { /// This tries to find the first non loopback interface with an ip address assigned. /// This should usually be the default interface according to: /// + /// /// https://docs.rs/pnet/0.27.2/pnet/datalink/fn.interfaces.html fn get_default_ipaddress() -> Option { let all_interfaces = datalink::interfaces(); @@ -447,6 +460,15 @@ impl Configurable for AgentConfig { } } + // The first unwrap defaults to none in case the option is not se + + let final_session = parsed_values + .get(&AgentConfig::SESSION_SYSTEMD) + .expect( + "No value for session parameter found in parsed values, this should not happen!", + ) + .is_some(); + // Panic if we encountered any errors during parsing of the values if !error_list.is_empty() { panic!( @@ -473,6 +495,7 @@ impl Configurable for AgentConfig { server_cert_file: final_server_cert_file, server_key_file: final_server_key_file, tags: final_tags, + session: final_session, pod_cidr: final_pod_cidr.unwrap(), }) } diff --git a/src/provider/mod.rs b/src/provider/mod.rs index d865778..c73dd25 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -1,7 +1,6 @@ use std::convert::TryFrom; use std::fs; use std::path::PathBuf; -use std::process::Child; use anyhow::anyhow; use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition; @@ -20,13 +19,17 @@ use crate::provider::error::StackableError::{ use crate::provider::repository::package::Package; use crate::provider::states::downloading::Downloading; use crate::provider::states::terminated::Terminated; +use crate::provider::systemdmanager::manager::SystemdManager; +use crate::provider::systemdmanager::systemdunit::SystemDUnit; use kube::error::ErrorResponse; +use std::time::Duration; pub struct StackableProvider { client: Client, parcel_directory: PathBuf, config_directory: PathBuf, log_directory: PathBuf, + session: bool, pod_cidr: String, } @@ -35,6 +38,7 @@ pub const CRDS: &[&str] = &["repositories.stable.stackable.de"]; mod error; mod repository; mod states; +mod systemdmanager; pub struct PodState { client: Client, @@ -46,7 +50,8 @@ pub struct PodState { service_name: String, service_uid: String, package: Package, - process_handle: Option, + systemd_manager: SystemdManager, + service_units: Option>, } impl PodState { @@ -63,6 +68,16 @@ impl PodState { pub fn get_service_log_directory(&self) -> PathBuf { self.log_directory.join(&self.service_name) } + + /// Resolve the directory in which the systemd unit files will be placed for this + /// service. + /// This defaults to "{{config_root}}/_service" + /// + /// From this place the unit files will be symlinked to the relevant systemd + /// unit directories so that they are picked up by systemd. + pub fn get_service_service_directory(&self) -> PathBuf { + self.get_service_config_directory().join("_service") + } } impl StackableProvider { @@ -71,6 +86,7 @@ impl StackableProvider { parcel_directory: PathBuf, config_directory: PathBuf, log_directory: PathBuf, + session: bool, pod_cidr: String, ) -> Result { let provider = StackableProvider { @@ -78,6 +94,7 @@ impl StackableProvider { parcel_directory, config_directory, log_directory, + session, pod_cidr, }; let missing_crds = provider.check_crds().await?; @@ -161,7 +178,7 @@ impl Provider for StackableProvider { } async fn initialize_pod_state(&self, pod: &Pod) -> anyhow::Result { - let service_name = pod.name(); + let service_name = format!("{}-{}", pod.namespace(), pod.name()); // Extract uid from pod object, if this fails we return an error - // this should not happen, as all objects we get from Kubernetes should have @@ -178,6 +195,7 @@ impl Provider for StackableProvider { let download_directory = parcel_directory.join("_download"); let config_directory = self.config_directory.clone(); let log_directory = self.log_directory.clone(); + let session = self.session; let package = Self::get_package(pod)?; if !(&download_directory.is_dir()) { @@ -187,6 +205,9 @@ impl Provider for StackableProvider { fs::create_dir_all(&config_directory)?; } + // TODO: investigate if we can share one DBus connection across all pods + let systemd_manager = SystemdManager::new(session, Duration::from_secs(5))?; + Ok(PodState { client: self.client.clone(), parcel_directory, @@ -194,10 +215,13 @@ impl Provider for StackableProvider { log_directory, config_directory: self.config_directory.clone(), package_download_backoff_strategy: ExponentialBackoffStrategy::default(), - service_name: String::from(service_name), + service_name, service_uid, package, - process_handle: None, + // TODO: Check if we can work with a reference or a Mutex Guard here to only keep + // one connection open to DBus instead of one per tracked Pod + systemd_manager, + service_units: None, }) } diff --git a/src/provider/repository/stackablerepository.rs b/src/provider/repository/stackablerepository.rs index 95a13d3..51865d1 100644 --- a/src/provider/repository/stackablerepository.rs +++ b/src/provider/repository/stackablerepository.rs @@ -51,6 +51,8 @@ struct StackablePackage { } impl StackableRepoProvider { + // This is only used in a test case and hence warned about as dead code + #[allow(dead_code)] pub fn new(name: String, base_url: String) -> Result { let base_url = Url::parse(&base_url)?; diff --git a/src/provider/states.rs b/src/provider/states.rs index fe97857..2ee58ec 100644 --- a/src/provider/states.rs +++ b/src/provider/states.rs @@ -11,8 +11,6 @@ pub(crate) mod installing; pub(crate) mod running; pub(crate) mod setup_failed; pub(crate) mod starting; -pub(crate) mod stopped; -pub(crate) mod stopping; pub(crate) mod terminated; pub(crate) mod waiting_config_map; diff --git a/src/provider/states/creating_config.rs b/src/provider/states/creating_config.rs index c733edc..152fdc0 100644 --- a/src/provider/states/creating_config.rs +++ b/src/provider/states/creating_config.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, HashMap}; use std::fs; use std::fs::read_to_string; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use handlebars::Handlebars; use k8s_openapi::api::core::v1::ConfigMap; @@ -157,7 +157,7 @@ impl CreatingConfig { /// fn apply_config_map( map: &ConfigMap, - target_directory: &PathBuf, + target_directory: &Path, template_data: &BTreeMap, ) -> Result<(), StackableError> { if map.metadata.name.is_none() { @@ -219,7 +219,7 @@ impl CreatingConfig { Ok(()) } - fn needs_update(target_file: &PathBuf, content: &str) -> Result { + fn needs_update(target_file: &Path, content: &str) -> Result { if target_file.is_file() { let current_content = read_to_string(target_file)?; debug!("Compared config file {:?} with result of", target_file); diff --git a/src/provider/states/creating_service.rs b/src/provider/states/creating_service.rs index 05e045c..72d07d0 100644 --- a/src/provider/states/creating_service.rs +++ b/src/provider/states/creating_service.rs @@ -1,11 +1,13 @@ use kubelet::pod::Pod; use kubelet::state::prelude::*; use kubelet::state::{State, Transition}; -use log::info; +use log::{debug, error, info}; use crate::provider::states::setup_failed::SetupFailed; use crate::provider::states::starting::Starting; +use crate::provider::systemdmanager::systemdunit::SystemDUnit; use crate::provider::PodState; +use std::fs::create_dir_all; #[derive(Default, Debug, TransitionTo)] #[transition_to(Starting, SetupFailed)] @@ -13,11 +15,84 @@ pub struct CreatingService; #[async_trait::async_trait] impl State for CreatingService { - async fn next(self: Box, pod_state: &mut PodState, _pod: &Pod) -> Transition { + async fn next(self: Box, pod_state: &mut PodState, pod: &Pod) -> Transition { + let service_name: &str = pod_state.service_name.as_ref(); info!( "Creating service unit for service {}", &pod_state.service_name ); + let service_directory = &pod_state.get_service_service_directory(); + if !service_directory.is_dir() { + debug!( + "Creating config directory for service [{}]: {:?}", + pod_state.service_name, service_directory + ); + if let Err(error) = create_dir_all(service_directory) { + return Transition::Complete(Err(anyhow::Error::from(error))); + } + } + + // Naming schema + // Service name: `namespace-podname` + // SystemdUnit: `namespace-podname-containername` + // TODO: add this to the docs in more detail + let service_prefix = format!("{}-{}-", pod.namespace(), pod.name()); + + // Create a template from those settings that are derived directly from the pod, not + // from container objects + let unit_template = match SystemDUnit::new_from_pod(&pod) { + Ok(unit) => unit, + Err(pod_error) => { + error!( + "Unable to create systemd unit template from pod [{}]: [{}]", + service_name, pod_error + ); + return Transition::Complete(Err(anyhow::Error::from(pod_error))); + } + }; + + // Each pod can map to multiple systemd units/services as each container will get its own + // systemd unit file/service. + // Map every container from the pod object to a systemdunit + let systemd_units: Vec = match pod + .containers() + .iter() + .map(|container| { + SystemDUnit::new(&unit_template, &service_prefix, container, pod_state) + }) + .collect() + { + Ok(units) => units, + Err(err) => return Transition::Complete(Err(anyhow::Error::from(err))), + }; + + // This will iterate over all systemd units, write the service files to disk and link + // the service to systemd. + for unit in &systemd_units { + // Create the service + // As per ADR005 we currently write the unit files directly in the systemd + // unit directory (by passing None as [unit_file_path]). + match pod_state + .systemd_manager + .create_unit(&unit, None, true, true) + { + Ok(()) => {} + Err(e) => { + // TODO: We need to discuss what to do here, in theory we could have loaded + // other services already, do we want to stop those? + error!( + "Failed to create systemd unit for service [{}]", + service_name + ); + return Transition::Complete(Err(e)); + } + } + // Done for now, if the service was created successfully we are happy + // Starting and enabling comes in a later state after all service have been createddy + } + pod_state.service_units = Some(systemd_units); + + // All services were loaded successfully, otherwise we'd have returned early above Transition::next(self, Starting) } diff --git a/src/provider/states/downloading.rs b/src/provider/states/downloading.rs index 23bb2cb..964a9f7 100644 --- a/src/provider/states/downloading.rs +++ b/src/provider/states/downloading.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::Path; use kubelet::pod::Pod; use kubelet::state::prelude::*; @@ -16,7 +16,7 @@ use crate::provider::PodState; pub struct Downloading; impl Downloading { - fn package_downloaded>(package: T, download_directory: &PathBuf) -> bool { + fn package_downloaded>(package: T, download_directory: &Path) -> bool { let package = package.into(); let package_file_name = download_directory.join(package.get_file_name()); debug!( diff --git a/src/provider/states/running.rs b/src/provider/states/running.rs index b53522f..693bae3 100644 --- a/src/provider/states/running.rs +++ b/src/provider/states/running.rs @@ -4,18 +4,17 @@ use k8s_openapi::api::core::v1::{ use kubelet::pod::Pod; use kubelet::state::prelude::*; use kubelet::state::{State, Transition}; -use log::{debug, error, trace}; +use log::{debug, trace}; use crate::provider::states::failed::Failed; use crate::provider::states::installing::Installing; use crate::provider::states::make_status_with_containers_and_condition; -use crate::provider::states::stopping::Stopping; use crate::provider::PodState; use k8s_openapi::apimachinery::pkg::apis::meta::v1::Time; use k8s_openapi::chrono; #[derive(Debug, TransitionTo)] -#[transition_to(Stopping, Failed, Running, Installing)] +#[transition_to(Failed, Running, Installing)] pub struct Running { pub transition_time: Time, } @@ -37,43 +36,12 @@ impl State for Running { ) -> Transition { loop { tokio::select! { - _ = tokio::time::delay_for(std::time::Duration::from_secs(1)) => { + _ = tokio::time::delay_for(std::time::Duration::from_secs(10)) => { trace!("Checking if service {} is still running.", &pod_state.service_name); } } - - // Obtain a mutable reference to the process handle - let child = if let Some(testproc) = pod_state.process_handle.as_mut() { - testproc - } else { - return Transition::next( - self, - Failed { - message: "Unable to obtain process handle from podstate!".to_string(), - }, - ); - }; - - // Check if an exit code is available for the process - if yes, it exited - match child.try_wait() { - Ok(None) => debug!( - "Service {} is still running with pid {}", - &pod_state.service_name, - child.id() - ), - _ => { - error!( - "Service {} died unexpectedly, moving to failed state", - pod_state.service_name - ); - return Transition::next( - self, - Failed { - message: "ProcessDiedUnexpectedly".to_string(), - }, - ); - } - } + // TODO: We are not watching the service yet, need to subscribe to events and + // react to those } } @@ -88,14 +56,14 @@ impl State for Running { }; let container = &pod.containers()[0]; - let mut container_status = vec![]; - container_status.push(KubeContainerStatus { + // TODO: Change to support multiple containers + let container_status = vec![KubeContainerStatus { name: container.name().to_string(), ready: true, started: Some(false), state: Some(state), ..Default::default() - }); + }]; let condition = PodCondition { last_probe_time: None, last_transition_time: Some(self.transition_time.clone()), diff --git a/src/provider/states/starting.rs b/src/provider/states/starting.rs index 58db677..75cdada 100644 --- a/src/provider/states/starting.rs +++ b/src/provider/states/starting.rs @@ -1,17 +1,12 @@ -use std::ffi::OsStr; -use std::process::{Command, Stdio}; - use kubelet::pod::Pod; use kubelet::state::prelude::*; use kubelet::state::{State, Transition}; -use log::{debug, error, info, trace}; -use tokio::time::Duration; -use crate::provider::states::creating_config::CreatingConfig; use crate::provider::states::failed::Failed; use crate::provider::states::running::Running; use crate::provider::states::setup_failed::SetupFailed; use crate::provider::PodState; +use log::{error, info, warn}; #[derive(Default, Debug, TransitionTo)] #[transition_to(Running, Failed, SetupFailed)] @@ -19,158 +14,41 @@ pub struct Starting; #[async_trait::async_trait] impl State for Starting { - async fn next(self: Box, pod_state: &mut PodState, pod: &Pod) -> Transition { - let container = pod.containers()[0].clone(); - let template_data = if let Ok(data) = CreatingConfig::create_render_data(&pod_state) { - data - } else { - error!("Unable to parse directories for command template as UTF8"); - return Transition::next( - self, - SetupFailed { - message: "DirectoryParseError".to_string(), - }, - ); - }; - if let Some(mut command) = container.command().clone() { - // We need to reverse the vec here, because pop works on the wrong "end" of - // the vec for our purposes - debug!("Reversing {:?}", &command); - command.reverse(); - debug!("Processing {:?}", &command); - if let Some(binary) = command.pop() { - let binary = pod_state - .parcel_directory - .join(pod_state.package.clone().get_directory_name()) - .join(binary); - - let binary = OsStr::new(&binary); - command.reverse(); - - let os_args: Vec = command - .iter() - .map(|s| CreatingConfig::render_config_template(&template_data, s).unwrap()) - .collect(); - - debug!( - "Starting command: {:?} with arguments {:?}", - binary, os_args - ); - - // Check if environment variables are set on the container - if some are present - // we render all values as templates to replace configroot, packageroot and logroot - // directories in case they are referenced in the values - // - // If even one of these renderings fails the entire pod will be failed and - // transitioned to a complete state with the error that occurred. - // If all renderings work, the vec<(String,String)> is returned as value and used - // later when starting the process - // This works because Result implements - // (FromIterator)[https://doc.rust-lang.org/std/result/enum.Result.html#method.from_iter] - // which returns a Result that is Ok(..) if none of the internal results contained - // an Error. If any error occurred, iteration stops on the first error and returns - // that in the outer result. - let env_variables = if let Some(vars) = container.env() { - debug!( - "Got environment vars: {:?} service {}", - vars, pod_state.service_name + async fn next(self: Box, pod_state: &mut PodState, _: &Pod) -> Transition { + if let Some(systemd_units) = &pod_state.service_units { + for unit in systemd_units { + info!("Starting systemd unit [{}]", unit); + if let Err(start_error) = pod_state.systemd_manager.start(&unit.get_name()) { + error!( + "Error occurred starting systemd unit [{}]: [{}]", + unit.get_name(), + start_error ); - let render_result = vars - .iter() - .map(|env_var| { - // Replace variables in value - CreatingConfig::render_config_template( - &template_data, - &env_var.value.as_deref().unwrap_or_default(), - ) - .map(|value| (&env_var.name, value)) - }) - .collect(); + return Transition::Complete(Err(start_error)); + } - // If any single rendering failed, the overall result for the map will have - // collected the Err which we can check for here - match render_result { - Ok(rendered_values) => rendered_values, - Err(error) => { - error!("Failed to render value for env var due to: {:?}", error); - return Transition::Complete(Err(anyhow::Error::from(error))); - } - } - } else { - // No environment variables present for this container -> empty vec - debug!( - "No environment vars set for service {}", - pod_state.service_name + info!("Enabling systemd unit [{}]", unit); + if let Err(enable_error) = pod_state.systemd_manager.enable(&unit.get_name()) { + error!( + "Error occurred starting systemd unit [{}]: [{}]", + unit.get_name(), + enable_error ); - vec![] - }; - debug!( - "Setting environment for service {} to {:?}", - pod_state.service_name, &env_variables - ); - - let start_result = Command::new(binary) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .envs(env_variables) - .args(&os_args) - .spawn(); - - match start_result { - Ok(mut child) => { - info!( - "Successfully executed command \"{:?}\" with args {:?}", - binary, &os_args - ); - - debug!("Waiting if startup fails.."); - for i in 1..10 { - tokio::time::delay_for(Duration::from_secs(1)).await; - if let Ok(None) = child.try_wait() { - trace!("Process still alive after {} seconds ..", i); - } else { - error!( - "Process died {} after {} seconds during startup!", - pod_state.service_name, i - ); - return Transition::next( - self, - Failed { - message: "ProcessFailedDuringStartup".to_string(), - }, - ); - } - } - // Store the child handle in the podstate so that later states - // can use it - pod_state.process_handle = Some(child); - return Transition::next( - self, - Running { - ..Default::default() - }, - ); - } - Err(error) => { - let error_message = format!("Failed to start process with error {}", error); - error!("{}", error_message); - return Transition::next( - self, - Failed { - message: "ProcessStartFailed".to_string(), - }, - ); - } + return Transition::Complete(Err(enable_error)); } } + } else { + warn!( + "No unit definitions found, not starting anything for pod [{}]!", + pod_state.service_name + ); } - error!("No command found, not starting anything.."); - return Transition::next( + Transition::next( self, - Failed { - message: "MissingCommandObject".to_string(), + Running { + ..Default::default() }, - ); + ) } async fn json_status( diff --git a/src/provider/states/stopped.rs b/src/provider/states/stopped.rs deleted file mode 100644 index f0a9538..0000000 --- a/src/provider/states/stopped.rs +++ /dev/null @@ -1,34 +0,0 @@ -use kubelet::pod::Pod; -use kubelet::state::prelude::*; -use kubelet::state::{State, Transition}; -use log::info; -use tokio::time::Duration; - -use crate::provider::states::starting::Starting; -use crate::provider::PodState; - -#[derive(Default, Debug, TransitionTo)] -#[transition_to(Starting)] -pub struct Stopped; - -#[async_trait::async_trait] -impl State for Stopped { - async fn next(self: Box, pod_state: &mut PodState, _pod: &Pod) -> Transition { - let delay = Duration::from_secs(2); - info!( - "Service {} stopped, waiting {} seconds before restart.", - pod_state.service_name, - delay.as_secs() - ); - tokio::time::delay_for(delay).await; - Transition::next(self, Starting) - } - - async fn json_status( - &self, - _pod_state: &mut PodState, - _pod: &Pod, - ) -> anyhow::Result { - make_status(Phase::Pending, &"Stopped") - } -} diff --git a/src/provider/states/stopping.rs b/src/provider/states/stopping.rs deleted file mode 100644 index 65d5a4a..0000000 --- a/src/provider/states/stopping.rs +++ /dev/null @@ -1,34 +0,0 @@ -use kubelet::pod::Pod; -use kubelet::state::prelude::*; -use kubelet::state::{State, Transition}; -use log::info; - -use crate::provider::states::failed::Failed; -use crate::provider::states::stopped::Stopped; -use crate::provider::PodState; - -#[derive(Default, Debug, TransitionTo)] -#[transition_to(Stopped, Failed)] -pub struct Stopping; - -#[async_trait::async_trait] -impl State for Stopping { - async fn next(self: Box, pod_state: &mut PodState, _pod: &Pod) -> Transition { - if let Some(child) = &pod_state.process_handle { - info!( - "Received stop command for service {}, stopping process with pid {}", - pod_state.service_name, - child.id() - ); - } - Transition::next(self, Stopped) - } - - async fn json_status( - &self, - _pod_state: &mut PodState, - _pod: &Pod, - ) -> anyhow::Result { - make_status(Phase::Pending, &"Stopping") - } -} diff --git a/src/provider/states/terminated.rs b/src/provider/states/terminated.rs index 528d214..354e2bb 100644 --- a/src/provider/states/terminated.rs +++ b/src/provider/states/terminated.rs @@ -1,6 +1,5 @@ -use anyhow::anyhow; use kubelet::state::prelude::*; -use log::{debug, error, info}; +use log::{error, info, warn}; use crate::provider::PodState; @@ -14,28 +13,50 @@ pub struct Terminated { impl State for Terminated { async fn next(self: Box, pod_state: &mut PodState, _pod: &Pod) -> Transition { info!( - "Pod {} was terminated, stopping process!", + "Pod {} was terminated, stopping service!", &pod_state.service_name ); - // Obtain a mutable reference to the process handle - let child = if let Some(testproc) = pod_state.process_handle.as_mut() { - testproc - } else { - return Transition::Complete(Err(anyhow!("Unable to retrieve process handle"))); - }; - return match child.kill() { - Ok(()) => { - debug!("Successfully killed process {}", pod_state.service_name); - Transition::Complete(Ok(())) + // TODO: We need some additional error handling here, wait for the services to actually + // shut down and try to remove the rest of the services if one fails (tbd, do we want that?) + if let Some(systemd_units) = &pod_state.service_units { + for unit in systemd_units { + info!("Stopping systemd unit [{}]", unit); + if let Err(stop_error) = pod_state.systemd_manager.stop(&unit.get_name()) { + error!( + "Error occurred stopping systemd unit [{}]: [{}]", + unit.get_name(), + stop_error + ); + return Transition::Complete(Err(stop_error)); + } + + // Daemon reload is false here, we'll do that once after all units have been removed + info!("Removing systemd unit [{}]", &unit); + if let Err(remove_error) = pod_state + .systemd_manager + .remove_unit(&unit.get_name(), false) + { + error!( + "Error occurred removing systemd unit [{}]: [{}]", + unit, remove_error + ); + return Transition::Complete(Err(remove_error)); + } } - Err(e) => { - error!( - "Failed to stop process with pid {} due to: {:?}", - child.id(), - e - ); - Transition::Complete(Err(anyhow::Error::new(e))) + } else { + warn!( + "No unit definitions found, not stopping anything for pod [{}]!", + pod_state.service_name + ); + } + + info!("Performing daemon-reload"); + return match pod_state.systemd_manager.reload() { + Ok(()) => Transition::Complete(Ok(())), + Err(reload_error) => { + error!("Failed to perform daemon-reload: [{}]", reload_error); + Transition::Complete(Err(reload_error)) } }; } diff --git a/src/provider/systemdmanager/manager.rs b/src/provider/systemdmanager/manager.rs new file mode 100644 index 0000000..5928b96 --- /dev/null +++ b/src/provider/systemdmanager/manager.rs @@ -0,0 +1,387 @@ +//! A module to allow managing systemd units - mostly services currently +//! +//! The module offers the ability to create, remove, start, stop, enable and +//! disable systemd units. +//! +use crate::provider::systemdmanager::systemdunit::SystemDUnit; +use anyhow::anyhow; +use dbus::arg::{AppendAll, ReadAll}; +use dbus::blocking::SyncConnection; +use dbus::strings::Member; +use dbus::Path; +use log::debug; +use std::fs; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; +use std::time::Duration; + +/// Enum that lists the supported unit types +#[derive(Clone, Debug)] +pub enum UnitTypes { + Service, +} + +const SYSTEMD_DESTINATION: &str = "org.freedesktop.systemd1"; +const SYSTEMD_NODE: &str = "/org/freedesktop/systemd1"; +const SYSTEMD_MANAGER_INTERFACE: &str = "org.freedesktop.systemd1.Manager"; + +/// The main way of interacting with this module, this struct offers +/// the public methods for managing service units. +/// +/// Use [`SystemdManager::new`] to create a new instance. +pub struct SystemdManager { + units_directory: PathBuf, + connection: SyncConnection, //TODO does this need to be closed? + timeout: Duration, +} + +/// By default the manager will connect to the system-wide instance of systemd, +/// which requires root access to the os. +impl Default for SystemdManager { + fn default() -> Self { + // If this panics we broke something in the code, as this is all constant values that + // should work + SystemdManager::new(false, Duration::from_secs(5)).unwrap() + } +} + +impl SystemdManager { + /// Create a new instance, takes a flag whether to run within the user session or manage services + /// system-wide and a timeout value for dbus communications. + pub fn new(user_mode: bool, timeout: Duration) -> Result { + // Connect to session or system bus depending on the value of [user_mode] + let connection = if user_mode { + SyncConnection::new_session()? + } else { + SyncConnection::new_system()? + }; + + // Depending on whether we are supposed to run in user space or system-wide + // we'll pick the default directory to initialize the systemd manager with + // This allows creating unit files either directly in the systemd folder by + // passing in just a filename, or symlink them by passing in an absolute + // path + let units_directory = if user_mode { + PathBuf::from(shellexpand::tilde("~/.config/systemd/user").to_string()) + } else { + PathBuf::from("/lib/systemd/system") + }; + + Ok(SystemdManager { + units_directory, + connection, + timeout, + }) + } + + // The main method for interacting with dbus, all other functions will delegate the actual + // dbus access to this function. + // Private on purpose as this should not be used by external dependencies + fn method_call<'m, R: ReadAll, A: AppendAll, M: Into>>( + &self, + m: M, + args: A, + ) -> Result { + let proxy = self + .connection + .with_proxy(SYSTEMD_DESTINATION, SYSTEMD_NODE, self.timeout); + proxy.method_call(SYSTEMD_MANAGER_INTERFACE, m, args) + } + + // Internal helper method to remove an existing unit file or symlink + fn delete_unit_file(&self, unit: &str) -> Result<(), anyhow::Error> { + let unit_file = self.units_directory.clone().join(&unit); + debug!("Removing [{:?}]", unit_file); + + match fs::remove_file(&unit_file) { + Ok(()) => Ok(()), + Err(delete_error) => { + debug!( + "Failed to remove existing unit file [{:?}] for systemd unit [{}]", + unit_file, unit + ); + Err(anyhow::Error::from(delete_error)) + } + } + } + + /// Write the proper unit file for [unit] to disk. + /// The location of the unit file is determined by the value of [unit_file_path]: + /// + /// * None, the unit file will be created in the base directory that this manager was initialized + /// with, which is either /lib/systemd/system or ~/.config/systemd/user depending on the value of + /// [session]. + /// * Some, the unit file will be created at this location and linked into the proper + /// systemd unit directory + /// + /// [force] determines if an existing unit file should be overwritten, if no external unit file + /// path is specified in [unit_file_path]. If this is false and the target file exists an error + /// is returned. + /// + /// The value of [daemon_reload] controls whether a daemon reload is triggered after creating or + /// linking the unit file. + pub fn create_unit( + &self, + unit: &SystemDUnit, + unit_file_path: Option, + force: bool, + daemon_reload: bool, + ) -> Result<(), anyhow::Error> { + // Appends .service to name if necessary + let linked_unit_file = unit_file_path.is_some(); + let unit_name = SystemdManager::get_unit_file_name(&unit.name, &unit.unit_type)?; + + // Check if a path was provided for the unit file, otherwise use the base directory + let target_file = if let Some(path) = unit_file_path { + path.join(&unit_name) + } else { + // TODO: I think we can get away with a reference here, but not sure yet, + // that would mean looking into get_unit_file_name returning a &str, _I think_ + self.units_directory.clone().join(&unit_name) + }; + + debug!( + "Target file for service [{}] : [{:?}]", + &unit_name, &target_file + ); + + // The following behavior distinguishes between a systemd unit that is defined in a file + // external to the systemd units directory which is then symlinked to and a file that is + // created directly in the systemd units dir. + // + // For the first case the _external_ file that will be symlinked to should have been written + // or potentially overwritten above, which is why we bypass this entire conditional in that + // case. + // For the case where we need to symlink we check if a symlink already exists and if so + // if force has been specified - only then do we remove an existing link before recreating + // it. + + // Perform some pre-flight checks to ensure that writing the unit file doesn't clash + // with any existing files + if !linked_unit_file + && target_file.exists() + && fs::symlink_metadata(&target_file)?.file_type().is_symlink() + { + // Handle the special case where we need to replace a symlink with an actual file + // This only occurs when switching from using a linked file to writing the file + // directly into the units folder - should not happen in practice + // In this case we need to remove the symlink + fs::remove_file(&target_file)?; + } + + let unit_file = self.units_directory.join(&unit_name); + if linked_unit_file + && unit_file.exists() + && unit_file.symlink_metadata()?.file_type().is_file() + { + // Handle the special case where we need to replace an actual file with a symlink + // This only occurs when switching from writing the file + // directly into the units folder to using a linked file - should not happen in practice + // In this case we need to remove the file + fs::remove_file(&unit_file)?; + } + + // We have handled the special case above, if the target file does not exist + // at this point in time we write the file - doesn't matter if inside or outside + // the systemd folder + if !target_file.exists() { + // Write unit file, no matter where + // TODO: implement check for content equality + let mut unit_file = match File::create(&target_file) { + Ok(file) => file, + Err(e) => { + debug!( + "Error occurred when creating unit file [{}]: [{}]", + unit_name, e + ); + return Err(anyhow::Error::from(e)); + } + }; + unit_file.write_all(unit.get_unit_file_content().as_bytes())?; + unit_file.flush()?; + } + + // If this is a linked unit file we need to call out to systemd to link this file + if linked_unit_file { + self.link_unit_file(&target_file.into_os_string().to_string_lossy(), force)?; + } + + // Perform daemon reload if requested + if daemon_reload { + self.reload()?; + } + Ok(()) + } + + /// Removes a unit from systemd. + /// Depending on what is passed in the [unit] parameter this means one of two things: + /// + /// * if an absolute file path is passed, the symlink to this file is deleted from the + /// systemd unit folder + /// * if a unit name is passed an attempt is made to unlink the unit via a dbus call + /// + /// Calling this function means an implicit disabling of the service, if it was enabled. + /// + pub fn remove_unit(&self, unit: &str, daemon_reload: bool) -> Result<(), anyhow::Error> { + debug!("Disabling unit [{}]", unit); + if let Err(disable_error) = self.disable(unit) { + debug!( + "Error disabling systemd unit [{}]: [{}]", + unit, disable_error + ); + return Err(disable_error); + } + + // If we are not linking to the unit file but writing it directly in the + // units folder it won't be removed by the dbus method call to `DisableUnitFiles` + //from [disable], so we delete explicitly + let unit_file = self.units_directory.join(&unit); + if unit_file.exists() { + debug!("Removing unit [{}] from systemd", unit); + self.delete_unit_file(&unit)?; + } + + if daemon_reload { + self.reload()?; + } + Ok(()) + } + + /// Enables a systemd unit to be stared automatically at system boot - expects a fully named + /// unit (which means: including the .service or other unit type). + /// This either requires that the unit is known to systemd or an absolute path to a unit file + /// to work. + /// + /// For a unit file to be _known_ it needs to either be located in the systemd unit folder, or + /// linked into that folder - both actions can be performed by calling [create_unit] + pub fn enable(&self, unit: &str) -> Result<(), anyhow::Error> { + // We don't do any checking around this and simply trust the user that either the name + // of an existing and linked service was provided or this is an absolute path + debug!("Trying to enable systemd unit [{}]", unit); + + match self + .method_call("EnableUnitFiles", (&[unit][..], false, true)) + .map(|_: ()| ()) + { + Ok(()) => { + debug!("Successfully started service [{}]", unit); + Ok(()) + } + Err(e) => { + debug!("Error: [{}]", e); + Err(anyhow!("Error starting service [{}]: {}", unit, e)) + } + } + } + + // Disable the systemd unit - which effectively means removing the symlink from the + // multi-user.target subdirectory. + pub fn disable(&self, unit: &str) -> Result, anyhow::Error> { + debug!("Trying to disable systemd unit [{}]", unit); + match self + .method_call("DisableUnitFiles", (&[unit][..], false)) + .map(|r: (Vec<(String, String, String)>,)| r.0) + { + Ok(result) => { + debug!("Successfully disabled service [{}]", unit); + Ok(result) + } + Err(e) => { + debug!("Error: [{}]", e); + Err(anyhow!("Error disabling service [{}]: {}", unit, e)) + } + } + } + + /// Attempts to start a systemd unit + /// [unit] is expected to be the name (including .) of a service that is known to + /// systemd at the time this is called. + /// To make a service known please take a look at the [enable] function. + pub fn start(&self, unit: &str) -> Result<(), anyhow::Error> { + debug!("Attempting to start unit {}", unit); + + match self + .method_call("StartUnit", (unit, "fail")) + .map(|r: (Path,)| r.0) + { + Ok(result) => { + debug!("Successfully started service [{}]: [{}]", unit, result); + Ok(()) + } + Err(e) => { + debug!("Error: [{}]", e); + Err(anyhow!("Error starting service [{}]: {}", unit, e)) + } + } + } + + /// Attempts to stop a systemd unit + /// [unit] is expected to be the name (including .) of a service that is known to + /// systemd at the time this is called. + /// To make a service known please take a look at the [enable] function. + pub fn stop(&self, unit: &str) -> Result<(), anyhow::Error> { + debug!("Trying to stop systemd unit [{}]", unit); + + match self + .method_call("StopUnit", (unit, "fail")) + .map(|r: (Path,)| r.0) + { + Ok(result) => { + debug!("Successfully stopped service [{}]: [{}]", unit, result); + Ok(()) + } + Err(e) => { + debug!("Error: [{}]", e); + Err(anyhow!("Error stopping service [{}]: {}", unit, e)) + } + } + } + + // Perform a daemon-reload, this causes systemd to re-read all unit files on disk and + // discover changes that have been performed since the last reload + // This needs to be done after creating a new service unit before it can be targeted by + // start / stop and similar commands. + pub fn reload(&self) -> Result<(), anyhow::Error> { + debug!("Performing daemon-reload.."); + + match self.method_call("Reload", ()).map(|_: ()| ()) { + Ok(_) => { + debug!("Successfully performed daemon-reload"); + Ok(()) + } + Err(e) => { + debug!("Error: [{}]", e); + Err(anyhow!("Error performing daemon-reload: [{}]", e)) + } + } + } + + // Symlink a unit file into the systemd unit folder + // This is not public on purpose, as [create] should be the normal way to link unit files + // when using this crate + fn link_unit_file(&self, unit: &str, force: bool) -> Result<(), dbus::Error> { + debug!("Linking [{}]", unit); + self.method_call("LinkUnitFiles", (&[unit][..], false, force)) + .map(|_: ()| ()) + } + + // Check if the unit name is valid and append .service if needed + // Cannot currently fail, I'll need to dig into what is a valid unit + // name before adding checks + #[allow(clippy::unnecessary_wraps)] + fn get_unit_file_name(name: &str, unit_type: &UnitTypes) -> Result { + // TODO: what are valid systemd unit names? + + // Append proper extension for unit type to file name + let extension = match unit_type { + UnitTypes::Service => ".service", + }; + + let mut result = String::from(name); + if !name.ends_with(extension) { + result.push_str(extension); + } + Ok(result) + } +} diff --git a/src/provider/systemdmanager/mod.rs b/src/provider/systemdmanager/mod.rs new file mode 100644 index 0000000..d87d4b5 --- /dev/null +++ b/src/provider/systemdmanager/mod.rs @@ -0,0 +1,2 @@ +pub mod manager; +pub mod systemdunit; diff --git a/src/provider/systemdmanager/systemdunit.rs b/src/provider/systemdmanager/systemdunit.rs new file mode 100644 index 0000000..934f5b0 --- /dev/null +++ b/src/provider/systemdmanager/systemdunit.rs @@ -0,0 +1,360 @@ +use std::collections::HashMap; + +use kubelet::container::Container; +use kubelet::pod::Pod; +use phf::{Map, OrderedSet}; + +use crate::provider::error::StackableError; + +use crate::provider::error::StackableError::PodValidationError; +use crate::provider::states::creating_config::CreatingConfig; +use crate::provider::systemdmanager::manager::UnitTypes; +use crate::provider::PodState; +use log::{debug, error, trace, warn}; +use std::fmt; +use std::fmt::{Display, Formatter}; + +// This is used to map from Kubernetes restart lingo to systemd restart terms +static RESTART_POLICY_MAP: Map<&'static str, &'static str> = phf::phf_map! { + "Always" => "always", + "OnFailure" => "on-failure", + "Never" => "no", +}; + +pub const SECTION_SERVICE: &str = "Service"; +pub const SECTION_UNIT: &str = "Unit"; +pub const SECTION_INSTALL: &str = "Install"; + +// TODO: This will be used later to ensure the same ordering of known sections in +// unit files, I'll leave it in for now +#[allow(dead_code)] +static SECTION_ORDER: OrderedSet<&'static str> = + phf::phf_ordered_set! {"Unit", "Service", "Install"}; + +/// A struct that represents an individual systemd unit +#[derive(Clone)] +pub struct SystemDUnit { + pub name: String, + pub unit_type: UnitTypes, + pub sections: HashMap>, + pub environment: HashMap, +} + +// TODO: The parsing code is also highly stackable specific, we should +// at some point consider splitting this out and have systemdunit live +// inside the systemd crate and the parsing in the agent +impl SystemDUnit { + /// Create a new unit which inherits all common elements from ['common_properties'] and parses + /// everything else from the ['container'] + pub fn new( + common_properties: &SystemDUnit, + name_prefix: &str, + container: &Container, + pod_state: &PodState, + ) -> Result { + let mut unit = common_properties.clone(); + + let trimmed_name = match container + .name() + .strip_suffix(common_properties.get_type_string()) + { + None => container.name().to_string(), + Some(name_without_suffix) => name_without_suffix.to_string(), + }; + + unit.name = format!("{}{}", name_prefix, trimmed_name); + + unit.add_property(SECTION_UNIT, "Description", &unit.name.clone()); + + unit.add_property( + SECTION_SERVICE, + "ExecStart", + &SystemDUnit::get_command(container, pod_state)?, + ); + + let env_vars = SystemDUnit::get_environment(container, pod_state)?; + + for (name, value) in env_vars { + unit.add_env_var(&name, &value); + } + + // These are currently hard-coded, as this is not something we expect to change soon + unit.add_property(SECTION_SERVICE, "StandardOutput", "journal"); + unit.add_property(SECTION_SERVICE, "StandardError", "journal"); + // This one is mandatory, as otherwise enabling the unit fails + unit.add_property(SECTION_INSTALL, "WantedBy", "multi-user.target"); + + Ok(unit) + } + + /// Parse a pod object and retrieve the generic settings which will be the same across + /// all service units created for containers in this pod. + /// This is designed to then be used as `common_properties` parameter when calling + ///[`SystemdUnit::new`] + pub fn new_from_pod(pod: &Pod) -> Result { + let mut unit = SystemDUnit { + name: pod.name().to_string(), + unit_type: UnitTypes::Service, + sections: Default::default(), + environment: Default::default(), + }; + + let restart_policy = match &pod.as_kube_pod().spec { + // if no restart policy is present we default to "never" + Some(spec) => spec.restart_policy.as_deref().unwrap_or("Never"), + None => "Never", + }; + + // if however one is specified but we do not know about this policy then we do not default + // to never but fail the service instead to avoid unpredictable behavior + let restart_policy = match RESTART_POLICY_MAP.get(restart_policy) { + Some(policy) => policy, + None => { + return Err(PodValidationError { + msg: format!( + "Unknown value [{}] for RestartPolicy in pod [{}]", + restart_policy, unit.name + ), + }) + } + }; + + unit.add_property(SECTION_SERVICE, "Restart", restart_policy); + Ok(unit) + } + + /// Convenience function to retrieve the _fully qualified_ systemd name, which includes the + /// `.servicetype` part. + pub fn get_name(&self) -> String { + let lower_type = format!("{:?}", self.unit_type).to_lowercase(); + format!("{}.{}", self.name, lower_type) + } + + /// Add a key=value entry to the specified section + fn add_property(&mut self, section: &'static str, key: &str, value: &str) { + let section = self + .sections + .entry(String::from(section)) + .or_insert_with(HashMap::new); + section.insert(String::from(key), String::from(value)); + } + + fn add_env_var(&mut self, name: &str, value: &str) { + self.environment + .insert(String::from(name), String::from(value)); + } + + /// Retrieve content of the unit file as it should be written to disk + pub fn get_unit_file_content(&self) -> String { + let mut unit_file_content = String::new(); + + // Iterate over all sections and write out its header and content + for (section, entries) in &self.sections { + unit_file_content.push_str(&format!("[{}]\n", section)); + for (key, value) in entries { + unit_file_content.push_str(&format!("{}={}\n", key, value)); + } + if section == SECTION_SERVICE { + // Add environment variables to Service section + for (name, value) in &self.environment { + unit_file_content.push_str(&format!("Environment=\"{}={}\"\n", name, value)); + } + } + unit_file_content.push('\n'); + } + unit_file_content + } + + fn get_type_string(&self) -> &str { + match &self.unit_type { + UnitTypes::Service => ".service", + } + } + + fn get_environment( + container: &Container, + pod_state: &PodState, + ) -> Result, StackableError> { + // Create template data to be used when rendering template strings + let template_data = if let Ok(data) = CreatingConfig::create_render_data(&pod_state) { + data + } else { + error!("Unable to parse directories for command template as UTF8"); + return Err(PodValidationError { + msg: format!( + "Unable to parse directories for command template as UTF8 for container [{}].", + container.name() + ), + }); + }; + + // Check if environment variables are set on the container - if some are present + // we render all values as templates to replace configroot, packageroot and logroot + // directories in case they are referenced in the values + // + // If even one of these renderings fails the entire pod will be failed and + // transitioned to a complete state with the error that occurred. + // If all renderings work, the vec<(String,String)> is returned as value and used + // later when starting the process + // This works because Result implements + // (FromIterator)[https://doc.rust-lang.org/std/result/enum.Result.html#method.from_iter] + // which returns a Result that is Ok(..) if none of the internal results contained + // an Error. If any error occurred, iteration stops on the first error and returns + // that in the outer result. + let env_variables = if let Some(vars) = container.env() { + debug!( + "Got environment vars: {:?} service {}", + vars, pod_state.service_name + ); + let render_result = vars + .iter() + .map(|env_var| { + // Replace variables in value + CreatingConfig::render_config_template( + &template_data, + &env_var.value.as_deref().unwrap_or_default(), + ) + .map(|value| (env_var.name.clone(), value)) + }) + .collect(); + + // If any single rendering failed, the overall result for the map will have + // collected the Err which we can check for here + match render_result { + Ok(rendered_values) => rendered_values, + Err(error) => { + error!("Failed to render value for env var due to: {:?}", error); + return Err(PodValidationError { + msg: String::from("Failed to render a template"), + }); + } + } + } else { + // No environment variables present for this container -> empty vec + debug!( + "No environment vars set for service {}", + pod_state.service_name + ); + vec![] + }; + debug!( + "Setting environment for service {} to {:?}", + pod_state.service_name, &env_variables + ); + + Ok(env_variables) + } + + // Retrieve a copy of the command object in the pod, or return an error if it is missing + fn get_command(container: &Container, pod_state: &PodState) -> Result { + // Return an error if no command was specified in the container + // TODO: We should discuss if there can be a valid scenario for this + // This clones because we perform some in place mutations on the elements + let mut command = match container.command() { + Some(command) => command.clone(), + _ => { + return Err(PodValidationError { + msg: format!( + "Error creating systemd unit for container {}, due to missing command element.", + container.name() + ), + }) + } + }; + + let package_root = pod_state.get_service_package_directory(); + + trace!( + "Command before replacing variables and adding packageroot: {:?}", + command + ); + // Get a mutable reference to the first element of the command array as we might need to + // add the package directory to this to make it an absolute path + let binary = match command.get_mut(0) { + Some(binary_string) => binary_string, + None => { + return Err(PodValidationError { + msg: format!( + "Unable to convert command for container [{}] to utf8.", + container.name() + ), + }) + } + }; + + // Warn if the user tried to add the packageroot directory to the command themselves + // This warning only triggers if the command starts with the packageroot as this is the + // only hard coded replacement we perform + // It might be perfectly reasonable to reference the packageroot directory somewhere + // later on in the command + if binary.starts_with("{{packageroot}}") { + warn!("Command for [{}] starts with \"{{packageroot}}\" - this would usually be automatically prepended to the command. Skipping prepending the directory and relying on string replacement instead, which is not recommended!", container.name()); + } else { + // Prepend package root to first element of the command array, which should be the binary + // this service has to execute + debug!( + "Prepending [{:?}] as package directory to the command for container [{}]", + package_root, + container.name() + ); + let binary_with_path = match package_root.join(&binary).into_os_string().into_string() { + Ok(path_string) => path_string, + Err(_) => { + return Err(PodValidationError { + msg: format!( + "Unable to convert command for container [{}] to utf8.", + container.name() + ), + }) + } + }; + binary.replace_range(.., &binary_with_path); + } + + // Create template data to be used when rendering template strings + let template_data = if let Ok(data) = CreatingConfig::create_render_data(&pod_state) { + data + } else { + error!("Unable to parse directories for command template as UTF8"); + return Err(PodValidationError { + msg: format!( + "Unable to parse directories for command template as UTF8 for container [{}].", + container.name() + ), + }); + }; + + // Append values from args array to command array + // This is necessary as we only have the ExecStart field in a systemd service unit. + // There is no specific place to put arguments separate from the command. + if let Some(mut args) = container.args().clone() { + debug!( + "Appending arguments [{:?}] to command for [{}]", + args, + container.name() + ); + command.append(args.as_mut()); + } + + // Replace variables in command array + let command_render_result = command + .iter() + .map(|command_part| { + CreatingConfig::render_config_template(&template_data, command_part) + }) + .collect::, StackableError>>()?; + + trace!( + "Command after replacing variables and adding packageroot: {:?}", + command_render_result + ); + + Ok(command_render_result.join(" ")) + } +} + +impl Display for SystemDUnit { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.get_name()) + } +}