diff --git a/Cargo.lock b/Cargo.lock index 08ac3bfc..5f3155ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,20 +1,16 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aho-corasick" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] -[[package]] -name = "anymap" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344" - [[package]] name = "atty" version = "0.2.14" @@ -23,7 +19,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -34,15 +30,15 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bytes" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cbitset" @@ -53,12 +49,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -75,14 +65,14 @@ dependencies = [ "num-integer", "num-traits", "time", - "winapi 0.3.9", + "winapi", ] [[package]] name = "clap" -version = "3.0.0-beta.2" +version = "3.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" +checksum = "098d281b47bf725a0bddd829e0070ee76560faab8af123050a86c440d7f0a1fd" dependencies = [ "atty", "bitflags", @@ -93,15 +83,13 @@ dependencies = [ "strsim", "termcolor", "textwrap", - "unicode-width", - "vec_map", ] [[package]] name = "clap_derive" -version = "3.0.0-beta.2" +version = "3.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" +checksum = "26de8102ffb96701066cea36f9a104285b67fbcc302a520640289d476c15ed8a" dependencies = [ "heck", "proc-macro-error", @@ -112,22 +100,21 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.4.4" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" dependencies = [ + "cfg-if", "crossbeam-utils", - "maybe-uninit", ] [[package]] name = "crossbeam-utils" -version = "0.7.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" dependencies = [ - "autocfg", - "cfg-if 0.1.10", + "cfg-if", "lazy_static", ] @@ -136,9 +123,10 @@ name = "deploy-rs" version = "0.1.0" dependencies = [ "clap", + "envmnt", "flexi_logger", - "fork", "futures-util", + "linked_hash_set", "log", "merge", "notify", @@ -154,23 +142,39 @@ dependencies = [ "yn", ] +[[package]] +name = "dunce" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" + +[[package]] +name = "envmnt" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f96dd862f12fac698dec3932dff0e6fb34bffeb5515ae5932d620cfe076571e" +dependencies = [ + "fsio", + "indexmap", +] + [[package]] name = "filetime" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c122a393ea57648015bf06fbd3d372378992e86b9ff5a7a497b076a28c79efe" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "redox_syscall", - "winapi 0.3.9", + "winapi", ] [[package]] name = "flexi_logger" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c37586928c27a25ff5fce49ff3f8e071b3beeef48b4f004fe7d40d75a26e3db5" +checksum = "291b6ce7b3ed2dda82efa6aee4c6bdb55fd11bc88b06c55b01851e94b96e5322" dependencies = [ "atty", "chrono", @@ -182,63 +186,36 @@ dependencies = [ "yansi", ] -[[package]] -name = "fork" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4c5b9b0bce249a456f83ac4404e8baad0d2ba81cf651949719a4f74eb7323bb" -dependencies = [ - "libc", -] - -[[package]] -name = "fsevent" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97f347202c95c98805c216f9e1df210e8ebaec9fdb2365700a43c10797a35e63" -dependencies = [ - "bitflags", - "fsevent-sys", -] - [[package]] name = "fsevent-sys" -version = "3.0.2" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a29c77f1ca394c3e73a9a5d24cfcabb734682d9634fc398f2204a63c994120" +checksum = "5c0e564d24da983c053beff1bb7178e237501206840a3e6bf4e267b9e8ae734a" dependencies = [ "libc", ] [[package]] -name = "fuchsia-zircon" -version = "0.3.3" +name = "fsio" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +checksum = "09e87827efaf94c7a44b562ff57de06930712fe21b530c3797cdede26e6377eb" dependencies = [ - "bitflags", - "fuchsia-zircon-sys", + "dunce", ] -[[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" - [[package]] name = "futures-core" -version = "0.3.8" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" +checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" [[package]] name = "futures-macro" -version = "0.3.8" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556" +checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd" dependencies = [ - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -246,26 +223,21 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.8" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d" -dependencies = [ - "once_cell", -] +checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" [[package]] name = "futures-util" -version = "0.3.8" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" +checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" dependencies = [ "futures-core", "futures-macro", "futures-task", - "pin-project", + "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] @@ -277,33 +249,33 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "heck" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" dependencies = [ "unicode-segmentation", ] [[package]] name = "hermit-abi" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "indexmap" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg", "hashbrown", @@ -311,9 +283,9 @@ dependencies = [ [[package]] name = "inotify" -version = "0.8.3" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46dd0a94b393c730779ccfd2a872b67b1eb67be3fc33082e733bdb38b5fde4d4" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" dependencies = [ "bitflags", "inotify-sys", @@ -322,45 +294,46 @@ dependencies = [ [[package]] name = "inotify-sys" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4563555856585ab3180a5bf0b2f9f8d301a728462afffc8195b3f5394229c55" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" dependencies = [ "libc", ] [[package]] name = "instant" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] -name = "iovec" -version = "0.1.4" +name = "itoa" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] -name = "itoa" -version = "0.4.6" +name = "kqueue" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +checksum = "058a107a784f8be94c7d35c1300f4facced2e93d2fbe5b1452b44e905ddca4a9" +dependencies = [ + "kqueue-sys", + "libc", +] [[package]] -name = "kernel32-sys" -version = "0.2.2" +name = "kqueue-sys" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "bitflags", + "libc", ] [[package]] @@ -370,46 +343,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] -name = "lazycell" -version = "1.3.0" +name = "libc" +version = "0.2.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98a04dce437184842841303488f70d0188c5f51437d2a834dc097eafa909a01" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] -name = "libc" -version = "0.2.81" +name = "linked_hash_set" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" +checksum = "47186c6da4d81ca383c7c47c1bfc80f4b95f4720514d860a5407aaf4233f9588" +dependencies = [ + "linked-hash-map", +] [[package]] name = "lock_api" -version = "0.4.2" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] [[package]] name = "log" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", ] -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - [[package]] name = "memchr" -version = "2.3.4" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "merge" @@ -435,99 +411,42 @@ dependencies = [ [[package]] name = "mio" -version = "0.6.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" -dependencies = [ - "cfg-if 0.1.10", - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", - "libc", - "log", - "miow 0.2.2", - "net2", - "slab", - "winapi 0.2.8", -] - -[[package]] -name = "mio" -version = "0.7.6" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33bc887064ef1fd66020c9adfc45bb9f33d75a42096c81e7c56c65b75dd1a8b" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", - "miow 0.3.6", + "miow", "ntapi", - "winapi 0.3.9", -] - -[[package]] -name = "mio-extras" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" -dependencies = [ - "lazycell", - "log", - "mio 0.6.23", - "slab", -] - -[[package]] -name = "miow" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" -dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", + "winapi", ] [[package]] name = "miow" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ - "socket2", - "winapi 0.3.9", -] - -[[package]] -name = "net2" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" -dependencies = [ - "cfg-if 0.1.10", - "libc", - "winapi 0.3.9", + "winapi", ] [[package]] name = "notify" -version = "5.0.0-pre.4" +version = "5.0.0-pre.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8b946889dfdad884379cd56367d93b6d0ce8889cc027d26a69a3a31c0a03bb5" +checksum = "245d358380e2352c2d020e8ee62baac09b3420f1f6c012a31326cfced4ad487d" dependencies = [ - "anymap", "bitflags", "crossbeam-channel", "filetime", - "fsevent", "fsevent-sys", "inotify", + "kqueue", "libc", - "mio 0.6.23", - "mio-extras", + "mio", "walkdir", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -536,7 +455,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -570,21 +489,24 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.5.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] name = "os_str_bytes" -version = "2.4.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", @@ -593,43 +515,23 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.1" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c6d9b8427445284a09c55be860a15855ab580a417ccad9da88f5a06787ced0" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "instant", "libc", "redox_syscall", "smallvec", - "winapi 0.3.9", -] - -[[package]] -name = "pin-project" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ccc2237c2c489783abd8c4c80e5450fc0e98644555b1364da68cc29aa151ca7" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "winapi", ] [[package]] name = "pin-project-lite" -version = "0.2.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" [[package]] name = "pin-utils" @@ -661,65 +563,55 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro-nested" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" - [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "fb37d2df5df740e582f28f8560cf425f52bb267d872fe58358eadb554909f07a" dependencies = [ "unicode-xid", ] [[package]] name = "quote" -version = "1.0.7" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.1.57" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] [[package]] name = "regex" -version = "1.4.2" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] name = "regex-syntax" -version = "0.6.21" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "rnix" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbbea4c714e5bbf462fa4316ddf45875d8f0e28e5db81050b5f9ce99746c6863" +checksum = "0a9b645f0edba447dbfc6473dd22999f46a1d00ab39e777a2713a1cf34a1597b" dependencies = [ "cbitset", "rowan", @@ -745,9 +637,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" [[package]] name = "same-file" @@ -766,18 +658,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.118" +version = "1.0.131" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" +checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.118" +version = "1.0.131" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" +checksum = "b710a83c4e0dff6a3d511946b95274ad9ca9e5d3ae497b63fda866ac955358d2" dependencies = [ "proc-macro2", "quote", @@ -786,9 +678,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.60" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" +checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" dependencies = [ "itoa", "ryu", @@ -797,9 +689,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.1" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3799fa361789a685db59e3986fb5f6f949e478728b9913c6759f7b014d0372" +checksum = "c35dfd12afb7828318348b8c408383cf5071a086c1d4ab1c0f9840ec92dbb922" dependencies = [ "libc", "signal-hook-registry", @@ -807,24 +699,24 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.2" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" -version = "1.5.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "smol_str" @@ -832,18 +724,6 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f7909a1d8bc166a862124d84fdc11bda0ea4ed3157ccca662296919c2972db1" -[[package]] -name = "socket2" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "redox_syscall", - "winapi 0.3.9", -] - [[package]] name = "strsim" version = "0.10.0" @@ -852,9 +732,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.73" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" dependencies = [ "proc-macro2", "quote", @@ -878,12 +758,9 @@ checksum = "20431e104bfecc1a40872578dbc390e10290a0e9c35fffe3ce6f73c15a9dbfc2" [[package]] name = "textwrap" -version = "0.12.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" -dependencies = [ - "unicode-width", -] +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" [[package]] name = "thin-dst" @@ -893,33 +770,24 @@ checksum = "db3c46be180f1af9673ebb27bc1235396f61ef6965b3fe0dbb2e624deb604f0e" [[package]] name = "thiserror" -version = "1.0.22" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.22" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "thread_local" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" -dependencies = [ - "lazy_static", -] - [[package]] name = "time" version = "0.1.44" @@ -928,34 +796,34 @@ checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", "wasi", - "winapi 0.3.9", + "winapi", ] [[package]] name = "tokio" -version = "1.9.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b7b349f11a7047e6d1276853e612d152f5e8a352c61917887cc2169e2366b4c" +checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" dependencies = [ "autocfg", "bytes", "libc", "memchr", - "mio 0.7.6", + "mio", "num_cpus", "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", "tokio-macros", - "winapi 0.3.9", + "winapi", ] [[package]] name = "tokio-macros" -version = "1.3.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" dependencies = [ "proc-macro2", "quote", @@ -964,51 +832,39 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ "serde", ] [[package]] name = "unicode-segmentation" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" - -[[package]] -name = "unicode-width" -version = "0.1.8" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" [[package]] name = "unicode-xid" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" - -[[package]] -name = "vec_map" -version = "0.8.2" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "version_check" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" [[package]] name = "walkdir" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", - "winapi 0.3.9", + "winapi", "winapi-util", ] @@ -1024,12 +880,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7884773ab69074615cb8f8425d0e53f11710786158704fca70f53e71b0e05504" -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" - [[package]] name = "winapi" version = "0.3.9" @@ -1040,12 +890,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -1058,7 +902,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1067,16 +911,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "ws2_32-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - [[package]] name = "yansi" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index 0ded1259..8477a6c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,15 +6,15 @@ name = "deploy-rs" version = "0.1.0" authors = ["notgne2 ", "Serokell "] -edition = "2018" +edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = "3.0.0-beta.2" +clap = { version = "3.0.0-rc.3", features = [ "derive", "env" ] } flexi_logger = "0.16" -fork = "0.1" futures-util = "0.3.6" +linked_hash_set = "0.1.4" log = "0.4" merge = "0.1.0" notify = "5.0.0-pre.3" @@ -27,6 +27,7 @@ tokio = { version = "1.9.0", features = [ "full" ] } toml = "0.5" whoami = "0.9.0" yn = "0.1" +envmnt = "0.9.0" # smol_str is required by rnix, but 0.1.17 doesn't build on rustc # 1.45.2 (shipped in nixos-20.09); it requires rustc 1.46.0. See @@ -37,3 +38,7 @@ smol_str = "=0.1.16" [lib] name = "deploy" path = "src/lib.rs" + +[profile.release] +lto = "thin" +opt-level = 3 diff --git a/README.md b/README.md index acd2b7f3..276ded97 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ You can try out this tool easily with `nix run`: In you want to deploy multiple flakes or a subset of profiles with one invocation, instead of calling `deploy ` you can issue `deploy --targets [ ...]` where `` is supposed to take the same format as discussed before. -Running in this mode, if any of the deploys fails, the deploy will be aborted and all successful deploys rolled back. `--rollback-succeeded false` can be used to override this behavior, otherwise the `auto-rollback` argument takes precedent. +Running in this mode, if any of the deploys fails, the deploy will be aborted and all successful deploys rolled back. `--rollback-succeeded false` can be used to override this behavior, otherwise the `no-auto-rollback` argument takes precedent. If you require a signing key to push closures to your server, specify the path to it in the `LOCAL_KEY` environment variable. @@ -48,7 +48,7 @@ This type of design (as opposed to more traditional tools like NixOps or morph) ### Magic Rollback -There is a built-in feature to prevent you making changes that might render your machine unconnectable or unusuable, which works by connecting to the machine after profile activation to confirm the machine is still available, and instructing the target node to automatically roll back if it is not confirmed. If you do not disable `magicRollback` in your configuration (see later sections) or with the CLI flag, you will be unable to make changes to the system which will affect you connecting to it (changing SSH port, changing your IP, etc). +There is a built-in feature to prevent you making changes that might render your machine unconnectable or unusuable, which works by connecting to the machine after profile activation to confirm the machine is still available, and instructing the target node to automatically roll back if it is not confirmed. If you do not disable `noMagicRollback` in your configuration (see later sections) or with the CLI flag, you will be unable to make changes to the system which will affect you connecting to it (changing SSH port, changing your IP, etc). ## API @@ -166,17 +166,15 @@ This is a set of options that can be put in any of the above definitions, with t # This defaults to `false` fastConnection = false; - # If the previous profile should be re-activated if activation fails. - # This defaults to `true` - autoRollback = true; + # If the previous profile should NOT be re-activated if activation fails. + noAutoRollback = true; - # See the earlier section about Magic Rollback for more information. - # This defaults to `true` - magicRollback = true; + # See the earlier section about Magic Rollback for more information, disable with this attr. + noMagicRollback = true; - # The path which deploy-rs will use for temporary files, this is currently only used by `magicRollback` to create an inotify watcher in for confirmations + # The path which deploy-rs will use for temporary files, this is currently only used by the magic rollback to create an inotify watcher in for confirmations # If not specified, this will default to `/tmp` - # (if `magicRollback` is in use, this _must_ be writable by `user`) + # (if magic rollback is in use, this _must_ be writable by `user`) tempPath = "/home/someuser/.deploy-rs"; } ``` diff --git a/examples/system/flake.nix b/examples/system/flake.nix index bcc841c7..bea9a9ce 100644 --- a/examples/system/flake.nix +++ b/examples/system/flake.nix @@ -23,8 +23,8 @@ defaultPackage.x86_64-linux = import ./hello.nix nixpkgs; deploy.nodes.example = { - sshOpts = [ "-p" "2221" ]; - hostname = "localhost"; + sshOpts = [ "-i" "./path/to/private/key" ]; + hostname = "localhost:2221"; fastConnection = true; profiles = { system = { diff --git a/flake.lock b/flake.lock index f54ce57e..0d7b1725 100644 --- a/flake.lock +++ b/flake.lock @@ -1,73 +1,89 @@ { "nodes": { - "flake-compat": { - "flake": false, + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, "locked": { - "lastModified": 1606424373, - "narHash": "sha256-oq8d4//CJOrVj+EcOaSXvMebvuTkmBJuT5tzlfewUnQ=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "99f1c2157fba4bfe6211a321fd0ee43199025dbf", + "lastModified": 1645597478, + "narHash": "sha256-axsWwzGMMMcvHKXyrEC99RHkU/8EecIcmrESGzZMD/k=", + "owner": "nix-community", + "repo": "fenix", + "rev": "6c8d60c1d8deba8c360537c47e2b86aefaea0fd5", "type": "github" }, "original": { - "owner": "edolstra", - "repo": "flake-compat", + "owner": "nix-community", + "repo": "fenix", "type": "github" } }, - "naersk": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, + "flake-compat": { + "flake": false, "locked": { - "lastModified": 1622810282, - "narHash": "sha256-4wmvM3/xfD0hCdNDIXVzRMfL4yB1J+DjH6Zte2xbAxk=", - "owner": "nmattia", - "repo": "naersk", - "rev": "e8061169e1495871b56be97c5c51d310fae01374", + "lastModified": 1627913399, + "narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2", "type": "github" }, "original": { - "owner": "nmattia", - "ref": "master", - "repo": "naersk", + "owner": "edolstra", + "repo": "flake-compat", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1622972307, - "narHash": "sha256-ENOu0FPCf95iLLoq2txhJtnA2ZpOFhIVBqQVbKM8ra0=", + "lastModified": 1645433236, + "narHash": "sha256-4va4MvJ076XyPp5h8sm5eMQvCrJ6yZAbBmyw95dGyw4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d8eb97e3801bde96491535f40483d550b57605b9", + "rev": "7f9b6e2babf232412682c09e57ed666d8f84ac2d", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" } }, "root": { "inputs": { + "fenix": "fenix", "flake-compat": "flake-compat", - "naersk": "naersk", "nixpkgs": "nixpkgs", "utils": "utils" } }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1645480664, + "narHash": "sha256-1+6YSK1hn6PX5qC3JwjrYktMwtq5GeFgNbyaGzk8Kuo=", + "owner": "rust-analyzer", + "repo": "rust-analyzer", + "rev": "c0ee2f23ff70349704dfe8448027a41b7788eb37", + "type": "github" + }, + "original": { + "owner": "rust-analyzer", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, "utils": { "locked": { - "lastModified": 1622445595, - "narHash": "sha256-m+JRe6Wc5OZ/mKw2bB3+Tl0ZbtyxxxfnAWln8Q5qs+Y=", + "lastModified": 1637014545, + "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=", "owner": "numtide", "repo": "flake-utils", - "rev": "7d706970d94bc5559077eb1a6600afddcd25a7c8", + "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 03f4a253..193bf179 100644 --- a/flake.nix +++ b/flake.nix @@ -7,52 +7,58 @@ description = "A Simple multi-profile Nix-flake deploy tool."; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - naersk = { - url = "github:nmattia/naersk/master"; - inputs.nixpkgs.follows = "nixpkgs"; - }; utils.url = "github:numtide/flake-utils"; flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + nixpkgs.url = "nixpkgs/nixos-unstable"; }; - outputs = { self, nixpkgs, utils, naersk, ... }: - { - overlay = final: prev: - let - naersk-lib = final.callPackage naersk { }; - system = final.system; - isDarwin = final.lib.strings.hasSuffix "-darwin" system; - darwinOptions = final.lib.optionalAttrs isDarwin { - nativeBuildInputs = [ - final.darwin.apple_sdk.frameworks.SystemConfiguration - ]; - }; - in - { - deploy-rs = { - - deploy-rs = naersk-lib.buildPackage (darwinOptions // { - root = ./.; - }) // { meta.description = "A Simple multi-profile Nix-flake deploy tool"; }; - - lib = rec { - - setActivate = builtins.trace - "deploy-rs#lib.setActivate is deprecated, use activate.noop, activate.nixos or activate.custom instead" - activate.custom; - - activate = rec { - custom = - { - __functor = customSelf: base: activate: - final.buildEnv { - name = ("activatable-" + base.name); - paths = - [ + outputs = { self, nixpkgs, utils, fenix, ... }: + let toolchain = "stable"; + in { + overlay = final: prev: + let + system = final.system; + darwinOptions = final.lib.optionalAttrs final.stdenv.isDarwin { + buildInputs = with final.darwin.apple_sdk.frameworks; [ + SystemConfiguration + CoreServices + ]; + }; + in { + deploy-rs = { + + deploy-rs = (final.makeRustPlatform { + inherit (final.fenix.${toolchain}) cargo rustc; + }).buildRustPackage (darwinOptions // { + pname = "deploy-rs"; + version = "0.1.0"; + + src = nixpkgs.lib.cleanSource ./.; + + cargoLock.lockFile = ./Cargo.lock; + }) // { + meta.description = "A Simple multi-profile Nix-flake deploy tool"; + }; + + lib = rec { + + setActivate = builtins.trace + "deploy-rs#lib.setActivate is deprecated, use activate.noop, activate.nixos or activate.custom instead" + activate.custom; + + activate = rec { + custom = { + __functor = customSelf: base: activate: + (final.buildEnv { + name = ("activatable-" + base.name); + paths = [ base (final.writeTextFile { name = base.name + "-activate-path"; @@ -62,7 +68,11 @@ if [[ "''${DRY_ACTIVATE:-}" == "1" ]] then - ${customSelf.dryActivate or "echo ${final.writeScript "activate" activate}"} + ${ + customSelf.dryActivate or "echo ${ + final.writeScript "activate" activate + }" + } else ${activate} fi @@ -71,67 +81,96 @@ destination = "/deploy-rs-activate"; }) (final.writeTextFile { - name = base.name + "-activate-rs"; - text = '' + name = base.name + "-activate-rs"; + text = '' #!${final.runtimeShell} - exec ${self.defaultPackage.${system}}/bin/activate "$@" + exec ${ + self.defaultPackage.${system} + }/bin/activate "$@" ''; executable = true; destination = "/activate-rs"; }) ]; - }; + } // customSelf); + }; + + nixos = base: + (custom // { + inherit base; + dryActivate = + "$PROFILE/bin/switch-to-configuration dry-activate"; + }) base.config.system.build.toplevel '' + # work around https://github.com/NixOS/nixpkgs/issues/73404 + cd /tmp + + $PROFILE/bin/switch-to-configuration switch + + # https://github.com/serokell/deploy-rs/issues/31 + ${with base.config.boot.loader; + final.lib.optionalString systemd-boot.enable + "sed -i '/^default /d' ${efi.efiSysMountPoint}/loader/loader.conf"} + ''; + + home-manager = base: + custom base.activationPackage "$PROFILE/activate"; + + noop = base: custom base ":"; }; - nixos = base: (custom // { dryActivate = "$PROFILE/bin/switch-to-configuration dry-activate"; }) base.config.system.build.toplevel '' - # work around https://github.com/NixOS/nixpkgs/issues/73404 - cd /tmp - - $PROFILE/bin/switch-to-configuration switch - - # https://github.com/serokell/deploy-rs/issues/31 - ${with base.config.boot.loader; - final.lib.optionalString systemd-boot.enable - "sed -i '/^default /d' ${efi.efiSysMountPoint}/loader/loader.conf"} - ''; - - home-manager = base: custom base.activationPackage "$PROFILE/activate"; - - noop = base: custom base ":"; - }; - - deployChecks = deploy: builtins.mapAttrs (_: check: check deploy) { - schema = deploy: final.runCommandNoCC "jsonschema-deploy-system" { } '' - ${final.python3.pkgs.jsonschema}/bin/jsonschema -i ${final.writeText "deploy.json" (builtins.toJSON deploy)} ${./interface.json} && touch $out - ''; - - activate = deploy: - let - profiles = builtins.concatLists (final.lib.mapAttrsToList (nodeName: node: final.lib.mapAttrsToList (profileName: profile: [ (toString profile.path) nodeName profileName ]) node.profiles) deploy.nodes); - in - final.runCommandNoCC "deploy-rs-check-activate" { } '' - for x in ${builtins.concatStringsSep " " (map (p: builtins.concatStringsSep ":" p) profiles)}; do - profile_path=$(echo $x | cut -f1 -d:) - node_name=$(echo $x | cut -f2 -d:) - profile_name=$(echo $x | cut -f3 -d:) - - test -f "$profile_path/deploy-rs-activate" || (echo "#$node_name.$profile_name is missing the deploy-rs-activate activation script" && exit 1); - - test -f "$profile_path/activate-rs" || (echo "#$node_name.$profile_name is missing the activate-rs activation script" && exit 1); - done - - touch $out - ''; + deployChecks = deploy: + builtins.mapAttrs (_: check: check deploy) { + schema = deploy: + final.runCommandNoCC "jsonschema-deploy-system" { } '' + ${final.python3.pkgs.jsonschema}/bin/jsonschema -i ${ + final.writeText "deploy.json" (builtins.toJSON deploy) + } ${self}/interface.json && touch $out + ''; + + activate = deploy: + let + profiles = builtins.concatLists (final.lib.mapAttrsToList + (nodeName: node: + final.lib.mapAttrsToList (profileName: profile: [ + (toString profile.path) + nodeName + profileName + ]) node.profiles) deploy.nodes); + in final.runCommandNoCC "deploy-rs-check-activate" { } '' + for x in ${ + builtins.concatStringsSep " " + (map (p: builtins.concatStringsSep ":" p) profiles) + }; do + profile_path=$(echo $x | cut -f1 -d:) + node_name=$(echo $x | cut -f2 -d:) + profile_name=$(echo $x | cut -f3 -d:) + + test -f "$profile_path/deploy-rs-activate" || (echo "#$node_name.$profile_name is missing the deploy-rs-activate activation script" && exit 1); + + test -f "$profile_path/activate-rs" || (echo "#$node_name.$profile_name is missing the activate-rs activation script" && exit 1); + done + + touch $out + ''; + }; + }; }; }; - }; - }; - } // - utils.lib.eachDefaultSystem (system: + } // utils.lib.eachSystem (utils.lib.defaultSystems ++ [ "aarch64-darwin" ]) + (system: let - pkgs = import nixpkgs { inherit system; overlays = [ self.overlay ]; }; - in - { + pkgs = import nixpkgs { + inherit system; + overlays = [ self.overlay fenix.overlay ]; + }; + rustPkg = pkgs.fenix.${toolchain}.withComponents [ + "cargo" + "clippy" + "rust-src" + "rustc" + "rustfmt" + ]; + in { defaultPackage = self.packages."${system}".deploy-rs; packages.deploy-rs = pkgs.deploy-rs.deploy-rs; @@ -142,22 +181,13 @@ }; devShell = pkgs.mkShell { - inputsFrom = [ self.packages.${system}.deploy-rs ]; - RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; - buildInputs = with pkgs; [ - nixUnstable - cargo - rustc - rust-analyzer - rustfmt - clippy - reuse - rust.packages.stable.rustPlatform.rustLibSrc - ]; + RUST_SRC_PATH = "${rustPkg}/lib/rustlib/src/rust/library"; + buildInputs = with pkgs; [ rust-analyzer-nightly reuse rustPkg ]; }; checks = { - deploy-rs = self.defaultPackage.${system}.overrideAttrs (super: { doCheck = true; }); + deploy-rs = self.defaultPackage.${system}.overrideAttrs + (super: { doCheck = true; }); }; lib = pkgs.deploy-rs.lib; diff --git a/interface.json b/interface.json index a9471f4f..eb47cf48 100644 --- a/interface.json +++ b/interface.json @@ -21,10 +21,10 @@ "fastConnection": { "type": "boolean" }, - "autoRollback": { + "noAutoRollback": { "type": "boolean" }, - "magicRollback": { + "noMagicRollback": { "type": "boolean" }, "confirmTimeout": { @@ -64,10 +64,7 @@ }, "additionalProperties": false } - }, - "required": [ - "hostname" - ] + } }, "profile_settings": { "type": "object", diff --git a/lib/nix_config.nix b/lib/nix_config.nix new file mode 100644 index 00000000..852f7376 --- /dev/null +++ b/lib/nix_config.nix @@ -0,0 +1,30 @@ +with import {}; +with lib; + settings: let + mkValueString = v: + if v == null + then "" + else if isInt v + then toString v + else if isBool v + then boolToString v + else if isFloat v + then floatToString v + else if isList v + then toString v + else if isDerivation v + then toString v + else if builtins.isPath v + then toString v + else if isString v + then v + else if isCoercibleToString v + then toString v + else ""; + + mkKeyValue = k: v: "${escape ["="] k} = ${mkValueString v}"; + + mkKeyValuePairs = attrs: concatStringsSep "\n" (mapAttrsToList mkKeyValue attrs); + in '' + ${mkKeyValuePairs settings} + '' diff --git a/src/bin/activate.rs b/src/bin/activate.rs index d0cfbe17..14687bdb 100644 --- a/src/bin/activate.rs +++ b/src/bin/activate.rs @@ -6,7 +6,7 @@ use signal_hook::{consts::signal::SIGHUP, iterator::Signals}; -use clap::Clap; +use clap::Parser; use tokio::fs; use tokio::process::Command; @@ -24,7 +24,7 @@ use thiserror::Error; use log::{debug, error, info, warn}; /// Remote activation utility for deploy-rs -#[derive(Clap, Debug)] +#[derive(Parser, Debug)] #[clap(version = "1.0", author = "Serokell ")] struct Opts { /// Print debug logs to output @@ -38,7 +38,7 @@ struct Opts { subcmd: SubCommand, } -#[derive(Clap, Debug)] +#[derive(Parser, Debug)] enum SubCommand { Activate(ActivateOpts), Wait(WaitOpts), @@ -46,7 +46,7 @@ enum SubCommand { } /// Activate a profile -#[derive(Clap, Debug)] +#[derive(Parser, Debug)] struct ActivateOpts { /// The closure to activate closure: String, @@ -75,7 +75,7 @@ struct ActivateOpts { } /// Activate a profile -#[derive(Clap, Debug)] +#[derive(Parser, Debug)] struct WaitOpts { /// The closure to wait for closure: String, @@ -86,7 +86,7 @@ struct WaitOpts { } /// Activate a profile -#[derive(Clap, Debug)] +#[derive(Parser, Debug)] struct RevokeOpts { /// The profile path to revoke profile_path: String, @@ -253,7 +253,7 @@ pub async fn activation_confirmation( let (deleted, done) = mpsc::channel(1); let mut watcher: RecommendedWatcher = - Watcher::new_immediate(move |res: Result| { + Watcher::new(move |res: Result| { let send_result = match res { Ok(e) if e.kind == notify::EventKind::Remove(notify::event::RemoveKind::File) => { debug!("Got worthy removal event, sending on channel"); @@ -271,7 +271,7 @@ pub async fn activation_confirmation( } })?; - watcher.watch(&lock_path, RecursiveMode::NonRecursive)?; + watcher.watch(Path::new(&lock_path), RecursiveMode::NonRecursive)?; if let Err(err) = danger_zone(done, confirm_timeout).await { error!("Error waiting for confirmation event: {}", err); @@ -303,7 +303,7 @@ pub async fn wait(temp_path: String, closure: String) -> Result<(), WaitError> { // TODO: fix wasteful clone let lock_path = lock_path.clone(); - Watcher::new_immediate(move |res: Result| { + Watcher::new(move |res: Result| { let send_result = match res { Ok(e) if e.kind == notify::EventKind::Create(notify::event::CreateKind::File) => { match &e.paths[..] { @@ -321,11 +321,11 @@ pub async fn wait(temp_path: String, closure: String) -> Result<(), WaitError> { })? }; - watcher.watch(&temp_path, RecursiveMode::NonRecursive)?; + watcher.watch(Path::new(&temp_path), RecursiveMode::NonRecursive)?; // Avoid a potential race condition by checking for existence after watcher creation if fs::metadata(&lock_path).await.is_ok() { - watcher.unwatch(&temp_path)?; + watcher.unwatch(Path::new(&temp_path))?; return Ok(()); } diff --git a/src/cli.rs b/src/cli.rs index 61890e43..135048fe 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,89 +6,41 @@ use std::collections::HashMap; use std::io::{stdin, stdout, Write}; -use clap::{ArgMatches, Clap, FromArgMatches}; +use clap::Parser; use crate as deploy; -use self::deploy::{DeployFlake, ParseFlakeError}; -use futures_util::stream::{StreamExt, TryStreamExt}; +use self::deploy::{data, flake, settings}; use log::{debug, error, info, warn}; use serde::Serialize; +use std::env; use std::process::Stdio; use thiserror::Error; use tokio::process::Command; +use std::path::{Path, PathBuf}; + /// Simple Rust rewrite of a simple Nix Flake deployment tool -#[derive(Clap, Debug, Clone)] +#[derive(Parser, Debug, Clone, Default)] #[clap(version = "1.0", author = "Serokell ")] pub struct Opts { /// The flake to deploy #[clap(group = "deploy")] - target: Option, + pub target: Option, /// A list of flakes to deploy alternatively #[clap(long, group = "deploy")] - targets: Option>, - /// Check signatures when using `nix copy` - #[clap(short, long)] - checksigs: bool, - /// Use the interactive prompt before deployment - #[clap(short, long)] - interactive: bool, - /// Extra arguments to be passed to nix build - extra_build_args: Vec, - - /// Print debug logs to output - #[clap(short, long)] - debug_logs: bool, - /// Directory to print logs to (including the background activation process) - #[clap(long)] - log_dir: Option, - - /// Keep the build outputs of each built profile - #[clap(short, long)] - keep_result: bool, - /// Location to keep outputs from built profiles in - #[clap(short, long)] - result_path: Option, + pub targets: Option>, - /// Skip the automatic pre-build checks - #[clap(short, long)] - skip_checks: bool, - - /// Override the SSH user with the given value - #[clap(long)] - ssh_user: Option, - /// Override the profile user with the given value - #[clap(long)] - profile_user: Option, - /// Override the SSH options used - #[clap(long)] - ssh_opts: Option, - /// Override if the connecting to the target node should be considered fast - #[clap(long)] - fast_connection: Option, - /// Override if a rollback should be attempted if activation fails - #[clap(long)] - auto_rollback: Option, /// Override hostname used for the node #[clap(long)] - hostname: Option, - /// Make activation wait for confirmation, or roll back after a period of time - #[clap(long)] - magic_rollback: Option, - /// How long activation should wait for confirmation (if using magic-rollback) - #[clap(long)] - confirm_timeout: Option, - /// Where to store temporary files (only used by magic-rollback) - #[clap(long)] - temp_path: Option, - /// Show what will be activated on the machines - #[clap(long)] - dry_activate: bool, - /// Revoke all previously succeeded deploys when deploying multiple profiles - #[clap(long)] - rollback_succeeded: Option, + pub hostname: Option, + + #[clap(flatten)] + pub flags: data::Flags, + + #[clap(flatten)] + pub generic_settings: settings::GenericSettings, } /// Returns if the available Nix installation supports flakes @@ -96,9 +48,7 @@ async fn test_flake_support() -> Result { debug!("Checking for flake support"); Ok(Command::new("nix") - .arg("eval") - .arg("--expr") - .arg("builtins.getFlake") + .args(vec!["eval", "--expr", "builtins.getFlake"]) // This will error on some machines "intentionally", and we don't really need that printing .stdout(Stdio::null()) .stderr(Stdio::null()) @@ -107,189 +57,27 @@ async fn test_flake_support() -> Result { .success()) } -#[derive(Error, Debug)] -pub enum CheckDeploymentError { - #[error("Failed to execute Nix checking command: {0}")] - NixCheck(#[from] std::io::Error), - #[error("Nix checking command resulted in a bad exit code: {0:?}")] - NixCheckExit(Option), -} - -async fn check_deployment( - supports_flakes: bool, - repo: &str, - extra_build_args: &[String], -) -> Result<(), CheckDeploymentError> { - info!("Running checks for flake in {}", repo); - - let mut check_command = match supports_flakes { - true => Command::new("nix"), - false => Command::new("nix-build"), - }; - - if supports_flakes { - check_command.arg("flake").arg("check").arg(repo); - } else { - check_command.arg("-E") - .arg("--no-out-link") - .arg(format!("let r = import {}/.; x = (if builtins.isFunction r then (r {{}}) else r); in if x ? checks then x.checks.${{builtins.currentSystem}} else {{}}", repo)); - } - - for extra_arg in extra_build_args { - check_command.arg(extra_arg); - } - - let check_status = check_command.status().await?; - - match check_status.code() { - Some(0) => (), - a => return Err(CheckDeploymentError::NixCheckExit(a)), - }; - - Ok(()) -} - -#[derive(Error, Debug)] -pub enum GetDeploymentDataError { - #[error("Failed to execute nix eval command: {0}")] - NixEval(std::io::Error), - #[error("Failed to read output from evaluation: {0}")] - NixEvalOut(std::io::Error), - #[error("Evaluation resulted in a bad exit code: {0:?}")] - NixEvalExit(Option), - #[error("Error converting evaluation output to utf8: {0}")] - DecodeUtf8(#[from] std::string::FromUtf8Error), - #[error("Error decoding the JSON from evaluation: {0}")] - DecodeJson(#[from] serde_json::error::Error), - #[error("Impossible happened: profile is set but node is not")] - ProfileNoNode, -} - -/// Evaluates the Nix in the given `repo` and return the processed Data from it -async fn get_deployment_data( - supports_flakes: bool, - flakes: &[deploy::DeployFlake<'_>], - extra_build_args: &[String], -) -> Result, GetDeploymentDataError> { - futures_util::stream::iter(flakes).then(|flake| async move { - - info!("Evaluating flake in {}", flake.repo); - - let mut c = if supports_flakes { - Command::new("nix") - } else { - Command::new("nix-instantiate") - }; - - if supports_flakes { - c.arg("eval") - .arg("--json") - .arg(format!("{}#deploy", flake.repo)) - // We use --apply instead of --expr so that we don't have to deal with builtins.getFlake - .arg("--apply"); - match (&flake.node, &flake.profile) { - (Some(node), Some(profile)) => { - // Ignore all nodes and all profiles but the one we're evaluating - c.arg(format!( - r#" - deploy: - (deploy // {{ - nodes = {{ - "{0}" = deploy.nodes."{0}" // {{ - profiles = {{ - inherit (deploy.nodes."{0}".profiles) "{1}"; - }}; - }}; - }}; - }}) - "#, - node, profile - )) - } - (Some(node), None) => { - // Ignore all nodes but the one we're evaluating - c.arg(format!( - r#" - deploy: - (deploy // {{ - nodes = {{ - inherit (deploy.nodes) "{}"; - }}; - }}) - "#, - node - )) - } - (None, None) => { - // We need to evaluate all profiles of all nodes anyway, so just do it strictly - c.arg("deploy: deploy") - } - (None, Some(_)) => return Err(GetDeploymentDataError::ProfileNoNode), - } - } else { - c - .arg("--strict") - .arg("--read-write-mode") - .arg("--json") - .arg("--eval") - .arg("-E") - .arg(format!("let r = import {}/.; in if builtins.isFunction r then (r {{}}).deploy else r.deploy", flake.repo)) - }; - - for extra_arg in extra_build_args { - c.arg(extra_arg); - } - - let build_child = c - .stdout(Stdio::piped()) - .spawn() - .map_err(GetDeploymentDataError::NixEval)?; - - let build_output = build_child - .wait_with_output() - .await - .map_err(GetDeploymentDataError::NixEvalOut)?; - - match build_output.status.code() { - Some(0) => (), - a => return Err(GetDeploymentDataError::NixEvalExit(a)), - }; - - let data_json = String::from_utf8(build_output.stdout)?; - - Ok(serde_json::from_str(&data_json)?) -}).try_collect().await -} - #[derive(Serialize)] struct PromptPart<'a> { user: &'a str, - ssh_user: &'a str, path: &'a str, - hostname: &'a str, + uri: &'a str, ssh_opts: &'a [String], } -fn print_deployment( - parts: &[( - &deploy::DeployFlake<'_>, - deploy::DeployData, - deploy::DeployDefs, - )], -) -> Result<(), toml::ser::Error> { +fn print_deployment(parts: &[&data::DeployData]) -> Result<(), toml::ser::Error> { let mut part_map: HashMap> = HashMap::new(); - for (_, data, defs) in parts { + for data in parts { part_map .entry(data.node_name.to_string()) .or_insert_with(HashMap::new) .insert( data.profile_name.to_string(), PromptPart { - user: &defs.profile_user, - ssh_user: &defs.ssh_user, + user: &data.profile_user, path: &data.profile.profile_settings.path, - hostname: &data.node.node_settings.hostname, + uri: &data.ssh_uri, ssh_opts: &data.merged_settings.ssh_opts, }, ); @@ -313,13 +101,7 @@ pub enum PromptDeploymentError { Cancelled, } -fn prompt_deployment( - parts: &[( - &deploy::DeployFlake<'_>, - deploy::DeployData, - deploy::DeployDefs, - )], -) -> Result<(), PromptDeploymentError> { +fn prompt_deployment(parts: &[&data::DeployData]) -> Result<(), PromptDeploymentError> { print_deployment(parts)?; info!("Are you sure you want to deploy these profiles?"); @@ -371,14 +153,14 @@ pub enum RunDeployError { DeployProfile(#[from] deploy::deploy::DeployProfileError), #[error("Failed to push profile: {0}")] PushProfile(#[from] deploy::push::PushProfileError), - #[error("No profile named `{0}` was found")] - ProfileNotFound(String), - #[error("No node named `{0}` was found")] - NodeNotFound(String), - #[error("Profile was provided without a node name")] - ProfileWithoutNode, + #[error("Failed to resolve target: {0}")] + ResolveTarget(#[from] data::ResolveTargetError), + #[error("Failed run Nix")] + Nix(#[from] std::io::Error), + #[error("Failed to parse JSON")] + JSON(#[from] serde_json::Error), #[error("Error processing deployment definitions: {0}")] - DeployDataDefs(#[from] deploy::DeployDataDefsError), + DeployData(#[from] data::DeployDataError), #[error("Failed to make printable TOML of deployment: {0}")] TomlFormat(#[from] toml::ser::Error), #[error("{0}")] @@ -387,196 +169,119 @@ pub enum RunDeployError { RevokeProfile(#[from] deploy::deploy::RevokeProfileError), } -type ToDeploy<'a> = Vec<( - &'a deploy::DeployFlake<'a>, - &'a deploy::data::Data, - (&'a str, &'a deploy::data::Node), - (&'a str, &'a deploy::data::Profile), -)>; +fn find_flake(starting_directory: &Path) -> Option { + let mut path: PathBuf = starting_directory.into(); + let file = Path::new("flake.nix"); + + loop { + path.push(file); + + if path.is_file() { + break Some(path); + } + if !(path.pop() && path.pop()) { + // remove file && remove parent + break None; + } + } +} async fn run_deploy( - deploy_flakes: Vec>, - data: Vec, + targets: Vec, + settings: Vec, supports_flakes: bool, - check_sigs: bool, - interactive: bool, - cmd_overrides: &deploy::CmdOverrides, - keep_result: bool, - result_path: Option<&str>, - extra_build_args: &[String], - debug_logs: bool, - dry_activate: bool, - log_dir: &Option, - rollback_succeeded: bool, + hostname: Option, + cmd_settings: settings::GenericSettings, + cmd_flags: data::Flags, ) -> Result<(), RunDeployError> { - let to_deploy: ToDeploy = deploy_flakes - .iter() - .zip(&data) - .map(|(deploy_flake, data)| { - let to_deploys: ToDeploy = match (&deploy_flake.node, &deploy_flake.profile) { - (Some(node_name), Some(profile_name)) => { - let node = match data.nodes.get(node_name) { - Some(x) => x, - None => return Err(RunDeployError::NodeNotFound(node_name.clone())), - }; - let profile = match node.node_settings.profiles.get(profile_name) { - Some(x) => x, - None => return Err(RunDeployError::ProfileNotFound(profile_name.clone())), - }; - - vec![( - deploy_flake, - data, - (node_name.as_str(), node), - (profile_name.as_str(), profile), - )] - } - (Some(node_name), None) => { - let node = match data.nodes.get(node_name) { - Some(x) => x, - None => return Err(RunDeployError::NodeNotFound(node_name.clone())), - }; - - let mut profiles_list: Vec<(&str, &deploy::data::Profile)> = Vec::new(); - - for profile_name in [ - node.node_settings.profiles_order.iter().collect(), - node.node_settings.profiles.keys().collect::>(), - ] - .concat() - { - let profile = match node.node_settings.profiles.get(profile_name) { - Some(x) => x, - None => { - return Err(RunDeployError::ProfileNotFound(profile_name.clone())) - } - }; - - if !profiles_list.iter().any(|(n, _)| n == profile_name) { - profiles_list.push((profile_name, profile)); - } - } + if supports_flakes { + let path = find_flake(Path::new(&env::current_dir()?)).unwrap_or_default(); + let flake = path.to_str().unwrap_or_default(); + let config_cmd = Command::new("nix") + .args(vec!["eval", "--raw", "--impure", "--expr"]) + .arg(format!( + "let flake = import {}; in if flake ? nixConfig then flake.nixConfig else {}", + flake, "{}" + )) + .arg("--apply") + .arg(include_str!("../lib/nix_config.nix")) + .output() + .await?; + if config_cmd.status.success() { + env::set_var("NIX_CONFIG", &*String::from_utf8_lossy(&config_cmd.stdout)); + } + } + let deploy_datas_ = targets + .into_iter() + .zip(&settings) + .map(|(target, root)| target.resolve(root, &cmd_settings, &cmd_flags, hostname.to_owned())) + .collect::>>, data::ResolveTargetError>>()?; + let deploy_datas: Vec<&data::DeployData<'_>> = deploy_datas_.iter().flatten().collect(); - profiles_list - .into_iter() - .map(|x| (deploy_flake, data, (node_name.as_str(), node), x)) - .collect() - } - (None, None) => { - let mut l = Vec::new(); - - for (node_name, node) in &data.nodes { - let mut profiles_list: Vec<(&str, &deploy::data::Profile)> = Vec::new(); - - for profile_name in [ - node.node_settings.profiles_order.iter().collect(), - node.node_settings.profiles.keys().collect::>(), - ] - .concat() - { - let profile = match node.node_settings.profiles.get(profile_name) { - Some(x) => x, - None => { - return Err(RunDeployError::ProfileNotFound( - profile_name.clone(), - )) - } - }; - - if !profiles_list.iter().any(|(n, _)| n == profile_name) { - profiles_list.push((profile_name, profile)); - } - } - - let ll: ToDeploy = profiles_list - .into_iter() - .map(|x| (deploy_flake, data, (node_name.as_str(), node), x)) - .collect(); - - l.extend(ll); - } + let mut parts: Vec<&data::DeployData> = Vec::new(); - l - } - (None, Some(_)) => return Err(RunDeployError::ProfileWithoutNode), - }; - Ok(to_deploys) - }) - .collect::, RunDeployError>>()? - .into_iter() - .flatten() - .collect(); - - let mut parts: Vec<( - &deploy::DeployFlake<'_>, - deploy::DeployData, - deploy::DeployDefs, - )> = Vec::new(); - - for (deploy_flake, data, (node_name, node), (profile_name, profile)) in to_deploy { - let deploy_data = deploy::make_deploy_data( - &data.generic_settings, - node, - node_name, - profile, - profile_name, - cmd_overrides, - debug_logs, - log_dir.as_deref(), - ); - - let deploy_defs = deploy_data.defs()?; - - parts.push((deploy_flake, deploy_data, deploy_defs)); + for deploy_data in deploy_datas { + parts.push(deploy_data); } - if interactive { + if cmd_flags.interactive { prompt_deployment(&parts[..])?; } else { print_deployment(&parts[..])?; } - for (deploy_flake, deploy_data, deploy_defs) in &parts { - deploy::push::push_profile(deploy::push::PushProfileData { + for deploy_data in &parts { + deploy::push::push_profile( supports_flakes, - check_sigs, - repo: deploy_flake.repo, - deploy_data, - deploy_defs, - keep_result, - result_path, - extra_build_args, - }) + deploy::push::ShowDerivationCommand::from_data(deploy_data), + deploy::push::BuildCommand::from_data(deploy_data), + deploy::push::SignCommand::from_data(deploy_data), + deploy::push::CopyCommand::from_data(deploy_data), + ) .await?; } - let mut succeeded: Vec<(&deploy::DeployData, &deploy::DeployDefs)> = vec![]; + let mut succeeded: Vec<&data::DeployData> = vec![]; // Run all deployments // In case of an error rollback any previoulsy made deployment. // Rollbacks adhere to the global seeting to auto_rollback and secondary // the profile's configuration - for (_, deploy_data, deploy_defs) in &parts { - if let Err(e) = deploy::deploy::deploy_profile(deploy_data, deploy_defs, dry_activate).await + for deploy_data in &parts { + if let Err(e) = deploy::deploy::deploy_profile( + &deploy_data.node_name, + &deploy_data.profile_name, + deploy::deploy::SshCommand::from_data(deploy_data)?, + deploy::deploy::ActivateCommand::from_data(deploy_data), + deploy::deploy::WaitCommand::from_data(deploy_data), + deploy::deploy::ConfirmCommand::from_data(deploy_data), + ) + .await { error!("{}", e); - if dry_activate { + if cmd_flags.dry_activate { info!("dry run, not rolling back"); } - info!("Revoking previous deploys"); - if rollback_succeeded && cmd_overrides.auto_rollback.unwrap_or(true) { + if cmd_flags.rollback_succeeded && cmd_settings.auto_rollback { + info!("Revoking previous deploys"); // revoking all previous deploys // (adheres to profile configuration if not set explicitely by // the command line) - for (deploy_data, deploy_defs) in &succeeded { - if deploy_data.merged_settings.auto_rollback.unwrap_or(true) { - deploy::deploy::revoke(*deploy_data, *deploy_defs).await?; + for deploy_data in &succeeded { + if deploy_data.merged_settings.auto_rollback { + deploy::deploy::revoke( + &deploy_data.node_name, + &deploy_data.profile_name, + deploy::deploy::SshCommand::from_data(deploy_data)?, + deploy::deploy::RevokeCommand::from_data(deploy_data), + ) + .await?; } } } break; } - succeeded.push((deploy_data, deploy_defs)) + succeeded.push(deploy_data) } Ok(()) @@ -591,26 +296,26 @@ pub enum RunError { #[error("Failed to test for flake support: {0}")] FlakeTest(std::io::Error), #[error("Failed to check deployment: {0}")] - CheckDeployment(#[from] CheckDeploymentError), + CheckDeployment(#[from] flake::CheckDeploymentError), #[error("Failed to evaluate deployment data: {0}")] - GetDeploymentData(#[from] GetDeploymentDataError), + GetDeploymentData(#[from] flake::GetDeploymentDataError), #[error("Error parsing flake: {0}")] - ParseFlake(#[from] deploy::ParseFlakeError), + ParseFlake(#[from] data::ParseTargetError), #[error("Error initiating logger: {0}")] Logger(#[from] flexi_logger::FlexiLoggerError), #[error("{0}")] RunDeploy(#[from] RunDeployError), } -pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { +pub async fn run(args: Option) -> Result<(), RunError> { let opts = match args { - Some(o) => ::from_arg_matches(o), + Some(o) => o, None => Opts::parse(), }; deploy::init_logger( - opts.debug_logs, - opts.log_dir.as_deref(), + opts.flags.debug_logs, + opts.flags.log_dir.as_deref(), &deploy::LoggerType::Deploy, )?; @@ -619,51 +324,33 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { .targets .unwrap_or_else(|| vec![opts.clone().target.unwrap_or_else(|| ".".to_string())]); - let deploy_flakes: Vec = deploys - .iter() - .map(|f| deploy::parse_flake(f.as_str())) - .collect::, ParseFlakeError>>()?; - - let cmd_overrides = deploy::CmdOverrides { - ssh_user: opts.ssh_user, - profile_user: opts.profile_user, - ssh_opts: opts.ssh_opts, - fast_connection: opts.fast_connection, - auto_rollback: opts.auto_rollback, - hostname: opts.hostname, - magic_rollback: opts.magic_rollback, - temp_path: opts.temp_path, - confirm_timeout: opts.confirm_timeout, - dry_activate: opts.dry_activate, - }; - let supports_flakes = test_flake_support().await.map_err(RunError::FlakeTest)?; if !supports_flakes { warn!("A Nix version without flakes support was detected, support for this is work in progress"); } - if !opts.skip_checks { - for deploy_flake in &deploy_flakes { - check_deployment(supports_flakes, deploy_flake.repo, &opts.extra_build_args).await?; + let targets: Vec = deploys + .into_iter() + .map(|f| f.parse::()) + .collect::, data::ParseTargetError>>( + )?; + + if opts.flags.do_checks { + for target in targets.iter() { + flake::check_deployment(supports_flakes, &target.repo, &opts.flags.extra_build_args) + .await?; } } - let result_path = opts.result_path.as_deref(); - let data = get_deployment_data(supports_flakes, &deploy_flakes, &opts.extra_build_args).await?; + let settings = + flake::get_deployment_data(supports_flakes, &targets, &opts.flags.extra_build_args).await?; run_deploy( - deploy_flakes, - data, + targets, + settings, supports_flakes, - opts.checksigs, - opts.interactive, - &cmd_overrides, - opts.keep_result, - result_path, - &opts.extra_build_args, - opts.debug_logs, - opts.dry_activate, - &opts.log_dir, - opts.rollback_succeeded.unwrap_or(true), + opts.hostname, + opts.generic_settings, + opts.flags, ) .await?; diff --git a/src/data.rs b/src/data.rs index 6fe7f75f..8b66a623 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,73 +1,483 @@ // SPDX-FileCopyrightText: 2020 Serokell +// SPDX-FileCopyrightText: 2021 Yannik Sander // // SPDX-License-Identifier: MPL-2.0 +use clap::Parser; +use linked_hash_set::LinkedHashSet; use merge::Merge; -use serde::Deserialize; -use std::collections::HashMap; - -#[derive(Deserialize, Debug, Clone, Merge)] -pub struct GenericSettings { - #[serde(rename(deserialize = "sshUser"))] - pub ssh_user: Option, - pub user: Option, - #[serde( - skip_serializing_if = "Vec::is_empty", - default, - rename(deserialize = "sshOpts") - )] - #[merge(strategy = merge::vec::append)] - pub ssh_opts: Vec, - #[serde(rename(deserialize = "fastConnection"))] - pub fast_connection: Option, - #[serde(rename(deserialize = "autoRollback"))] - pub auto_rollback: Option, - #[serde(rename(deserialize = "confirmTimeout"))] - pub confirm_timeout: Option, - #[serde(rename(deserialize = "tempPath"))] - pub temp_path: Option, - #[serde(rename(deserialize = "magicRollback"))] - pub magic_rollback: Option, +use rnix::{types::*, SyntaxKind::*}; +use std::net::{SocketAddr, ToSocketAddrs}; +use thiserror::Error; + +use crate::settings; + +#[derive(PartialEq, Debug)] +pub struct Target { + pub repo: String, + pub node: Option, + pub profile: Option, + pub ip: Option, } -#[derive(Deserialize, Debug, Clone)] -pub struct NodeSettings { - pub hostname: String, - pub profiles: HashMap, - #[serde( - skip_serializing_if = "Vec::is_empty", - default, - rename(deserialize = "profilesOrder") - )] - pub profiles_order: Vec, +#[derive(Error, Debug)] +pub enum ParseTargetError { + #[error("The given path was too long, did you mean to put something in quotes?")] + PathTooLong, + #[error("Unrecognized node or token encountered")] + Unrecognized, } -#[derive(Deserialize, Debug, Clone)] -pub struct ProfileSettings { - pub path: String, - #[serde(rename(deserialize = "profilePath"))] - pub profile_path: Option, +#[derive(Error, Debug)] +pub enum ResolveTargetError { + #[error("No node named `{0}` was found in repo `{1}`")] + NodeNotFound(String, String), + #[error("No profile named `{0}` was on node `{1}` found in repo `{2}`")] + ProfileNotFound(String, String, String), + #[error("Profile was provided without a node name for repo `{0}`")] + ProfileWithoutNode(String), + #[error("Deployment data invalid: {0}")] + InvalidDeployDataError(#[from] DeployDataError), + #[error("IP suffix on flake root target '{0}'. You can't deploy all the flake's targets to the same node, dude.")] + IpOnFlakeRoot(String), } -#[derive(Deserialize, Debug, Clone)] -pub struct Profile { - #[serde(flatten)] - pub profile_settings: ProfileSettings, - #[serde(flatten)] - pub generic_settings: GenericSettings, +impl<'a> Target { + pub fn resolve( + self, + r: &'a settings::Root, + cs: &'a settings::GenericSettings, + cf: &'a Flags, + hostname: Option, + ) -> Result>, ResolveTargetError> { + match self { + Target { + repo, + node: Some(node), + profile, + ip, + } => { + let node_ = match r.nodes.get(&node) { + Some(x) => x, + None => return Err(ResolveTargetError::NodeNotFound(node.to_owned(), repo)), + }; + if let Some(profile) = profile { + let profile_ = match node_.node_settings.profiles.get(&profile) { + Some(x) => x, + None => { + return Err(ResolveTargetError::ProfileNotFound( + profile.to_owned(), + node.to_owned(), + repo, + )) + } + }; + Ok({ + let hostname_: Option = if ip.is_some() { ip } else { hostname }; + let d = DeployData::new( + repo, + node.to_owned(), + profile.to_owned(), + &r.generic_settings, + cs, + cf, + node_, + profile_, + hostname_, + )?; + vec![d] + }) + } else { + let ordered_profile_names: LinkedHashSet = + node_.node_settings.profiles_order.iter().cloned().collect(); + let profile_names: LinkedHashSet = + node_.node_settings.profiles.keys().cloned().collect(); + let prioritized_profile_names: LinkedHashSet<&String> = + ordered_profile_names.union(&profile_names).collect(); + Ok(prioritized_profile_names + .iter() + .map(|p| { + Target { + repo: repo.to_owned(), + node: Some(node.to_owned()), + profile: Some(p.to_string()), + ip: ip.to_owned(), + } + .resolve(r, cs, cf, hostname.to_owned()) + }) + .collect::>>, ResolveTargetError>>()? + .into_iter() + .flatten() + .collect::>>()) + } + } + Target { + repo, + node: None, + profile: _, + ip: Some(_), + } => Err(ResolveTargetError::IpOnFlakeRoot(repo)), + Target { + repo, + node: None, + profile: None, + ip: _, + } => { + if let Some(_hostname) = hostname { + todo!() // create issue to discuss: + // if allowed, it would be really awkward + // to override the hostname for a series of nodes at once + } + Ok(r.nodes + .iter() + .map(|(n, _)| { + Target { + repo: repo.to_owned(), + node: Some(n.to_string()), + profile: None, + ip: self.ip.to_owned(), + } + .resolve(r, cs, cf, hostname.to_owned()) + }) + .collect::>>, ResolveTargetError>>()? + .into_iter() + .flatten() + .collect::>>()) + } + Target { + repo, + node: None, + profile: Some(_), + ip: _, + } => Err(ResolveTargetError::ProfileWithoutNode(repo)), + } + } } -#[derive(Deserialize, Debug, Clone)] -pub struct Node { - #[serde(flatten)] - pub generic_settings: GenericSettings, - #[serde(flatten)] - pub node_settings: NodeSettings, +impl std::str::FromStr for Target { + type Err = ParseTargetError; + + fn from_str(s: &str) -> Result { + let target_fragment_start = s.find('#'); + let (repo, maybe_target_full) = match target_fragment_start { + Some(i) => (s[..i].to_string(), Some(&s[i + 1..])), + None => (s.to_string(), None), + }; + + let mut maybe_target: Option<&str> = None; + + let mut ip: Option = None; + + if let Some(t) = maybe_target_full { + let ip_fragment_start = t.find('@'); + if let Some(i) = ip_fragment_start { + maybe_target = Some(&t[..i]); + ip = Some(t[i + 1..].to_string()); + } else { + maybe_target = maybe_target_full; + }; + }; + + let mut node: Option = None; + let mut profile: Option = None; + + if let Some(target) = maybe_target { + let ast = rnix::parse(target); + + let first_child = match ast.root().node().first_child() { + Some(x) => x, + None => { + return Ok(Target { + repo, + node: None, + profile: None, + ip, // NB: error if not none; catched on target resolve + }); + } + }; + + let mut node_over = false; + + for entry in first_child.children_with_tokens() { + let x = match (entry.kind(), node_over) { + (TOKEN_DOT, false) => { + node_over = true; + None + } + (TOKEN_DOT, true) => { + return Err(ParseTargetError::PathTooLong); + } + (NODE_IDENT, _) => Some(entry.into_node().unwrap().text().to_string()), + (TOKEN_IDENT, _) => Some(entry.into_token().unwrap().text().to_string()), + (NODE_STRING, _) => { + let c = entry + .into_node() + .unwrap() + .children_with_tokens() + .nth(1) + .unwrap(); + + Some(c.into_token().unwrap().text().to_string()) + } + _ => return Err(ParseTargetError::Unrecognized), + }; + + if !node_over { + node = x; + } else { + profile = x; + } + } + } + + Ok(Target { + repo, + node, + profile, + ip, + }) + } } -#[derive(Deserialize, Debug, Clone)] -pub struct Data { - #[serde(flatten)] - pub generic_settings: GenericSettings, - pub nodes: HashMap, +#[test] +fn test_deploy_target_from_str() { + assert_eq!( + "../examples/system".parse::().unwrap(), + Target { + repo: "../examples/system".to_string(), + node: None, + profile: None, + ip: None, + } + ); + + assert_eq!( + "../examples/system#".parse::().unwrap(), + Target { + repo: "../examples/system".to_string(), + node: None, + profile: None, + ip: None, + } + ); + + assert_eq!( + "../examples/system#computer.\"something.nix\"@localhost:22" + .parse::() + .unwrap(), + Target { + repo: "../examples/system".to_string(), + node: Some("computer".to_string()), + profile: Some("something.nix".to_string()), + ip: Some("localhost:22".to_string()), + } + ); + + assert_eq!( + "../examples/system#\"example.com\".system" + .parse::() + .unwrap(), + Target { + repo: "../examples/system".to_string(), + node: Some("example.com".to_string()), + profile: Some("system".to_string()), + ip: None, + } + ); + + assert_eq!( + "../examples/system#example".parse::().unwrap(), + Target { + repo: "../examples/system".to_string(), + node: Some("example".to_string()), + profile: None, + ip: None, + } + ); + + assert_eq!( + "../examples/system#example.system" + .parse::() + .unwrap(), + Target { + repo: "../examples/system".to_string(), + node: Some("example".to_string()), + profile: Some("system".to_string()), + ip: None, + } + ); + + assert_eq!( + "../examples/system".parse::().unwrap(), + Target { + repo: "../examples/system".to_string(), + node: None, + profile: None, + ip: None, + } + ); +} + +#[derive(Debug, Clone)] +pub struct DeployData<'a> { + pub repo: String, + pub node_name: String, + pub profile_name: String, + + pub flags: &'a Flags, + pub node: &'a settings::Node, + pub profile: &'a settings::Profile, + pub merged_settings: settings::GenericSettings, + + // TODO: can be used instead of ssh_uri to iterate + // over potentially a series of sockets to deploy + // to + // pub sockets: Vec, + pub ssh_user: String, + pub ssh_uri: String, + pub temp_path: String, + pub profile_path: String, + pub profile_user: String, + pub sudo: Option, +} + +#[derive(Error, Debug)] +pub enum DeployDataError { + #[error("Neither `user` nor `sshUser` are set for profile {0} of node {1}")] + NoProfileUser(String, String), + #[error("Value `hostname` is not define for node {0}")] + NoHost(String), + #[error("Cannot creato a socket for '{0}' from '{1}': {2}")] + InvalidSockent(String, String, String), +} + +#[derive(Parser, Debug, Clone, Default)] +pub struct Flags { + /// Check signatures when using `nix copy` + #[clap(short, long)] + pub checksigs: bool, + /// Use the interactive prompt before deployment + #[clap(short, long)] + pub interactive: bool, + /// Extra arguments to be passed to nix build + #[clap(long)] + pub extra_build_args: Vec, + + /// Print debug logs to output + #[clap(short, long)] + pub debug_logs: bool, + /// Directory to print logs to (including the background activation process) + #[clap(long)] + pub log_dir: Option, + + /// Keep the build outputs of each built profile + #[clap(short, long)] + pub keep_result: bool, + /// Location to keep outputs from built profiles in + #[clap(short, long)] + pub result_path: Option, + + /// Do the automatic pre-build checks + #[clap(short = 's', long, env = "DEPLOY_SKIP_CHECKS")] + pub do_checks: bool, + /// Make activation wait for confirmation, or roll back after a period of time + /// Show what will be activated on the machines + #[clap(long)] + pub dry_activate: bool, + /// Revoke all previously succeeded deploys when deploying multiple profiles + #[clap(long)] + pub rollback_succeeded: bool, +} + +#[allow(clippy::too_many_arguments)] +impl<'a> DeployData<'a> { + fn new( + repo: String, + node_name: String, + profile_name: String, + top_settings: &'a settings::GenericSettings, + cmd_settings: &'a settings::GenericSettings, + flags: &'a Flags, + node: &'a settings::Node, + profile: &'a settings::Profile, + hostname: Option, + ) -> Result, DeployDataError> { + let mut merged_settings = cmd_settings.clone(); + merged_settings.merge(profile.generic_settings.clone()); + merged_settings.merge(node.generic_settings.clone()); + merged_settings.merge(top_settings.clone()); + + // if let Some(ref ssh_opts) = cmd_overrides.ssh_opts { + // merged_settings.ssh_opts = ssh_opts.split(' ').map(|x| x.to_owned()).collect(); + // } + let temp_path = match merged_settings.temp_path { + Some(ref x) => x.to_owned(), + None => "/tmp".to_string(), + }; + let profile_user = if let Some(ref x) = merged_settings.user { + x.to_owned() + } else if let Some(ref x) = merged_settings.ssh_user { + x.to_owned() + } else { + return Err(DeployDataError::NoProfileUser(profile_name, node_name)); + }; + let profile_path = match profile.profile_settings.profile_path { + None => format!( + "/nix/var/nix/profiles/{}", + match &profile_user[..] { + #[allow(clippy::redundant_clone)] + "root" => profile_name.to_owned(), + _ => format!("per-user/{}/{}", profile_user, profile_name), + } + ), + Some(ref x) => x.to_owned(), + }; + let ssh_user = match merged_settings.ssh_user { + Some(ref u) => u.to_owned(), + None => whoami::username(), + }; + let sudo = match merged_settings.user { + Some(ref user) if user != &ssh_user => Some(format!("sudo -u {}", user)), + _ => None, + }; + let hostname = match hostname { + Some(x) => x, + None => { + if let Some(x) = &node.node_settings.hostname { + x.to_string() + } else { + return Err(DeployDataError::NoHost(node_name)); + } + } + }; + let maybe_iter = &mut hostname[..].to_socket_addrs(); + let sockets: Vec = match maybe_iter { + Ok(x) => x.into_iter().collect(), + Err(err) => { + return Err(DeployDataError::InvalidSockent( + repo, + hostname, + err.to_string(), + )) + } + }; + let ssh_uri = format!("ssh://{}@{}", &ssh_user, sockets.first().unwrap()); + + Ok(DeployData { + repo, + node_name, + profile_name, + flags, + node, + profile, + merged_settings, + // sockets, + ssh_user, + ssh_uri, + temp_path, + profile_path, + profile_user, + sudo, + }) + } } diff --git a/src/deploy.rs b/src/deploy.rs index f8fc2f90..cd72b9fe 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -4,19 +4,42 @@ // // SPDX-License-Identifier: MPL-2.0 -use log::{debug, info}; -use std::borrow::Cow; +use log::{debug, error, info}; use thiserror::Error; use tokio::process::Command; -use crate::DeployDataDefsError; +use crate::data; -struct ActivateCommandData<'a> { - sudo: &'a Option, +pub struct SshCommand<'a> { + ssh_uri: &'a str, + opts: &'a Vec, +} + +impl<'a> SshCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Result { + let opts = d.merged_settings.ssh_opts.as_ref(); + Ok(SshCommand { + ssh_uri: d.ssh_uri.as_ref(), + opts, + }) + } + + fn build(&self) -> Command { + let mut cmd = Command::new("ssh"); + cmd.arg(self.ssh_uri); + cmd.args(self.opts.iter()); + + debug!("Built command: SshCommand -> {:?}", cmd); + cmd + } +} + +pub struct ActivateCommand<'a> { + sudo: Option<&'a str>, profile_path: &'a str, + temp_path: &'a str, closure: &'a str, auto_rollback: bool, - temp_path: &'a str, confirm_timeout: u16, magic_rollback: bool, debug_logs: bool, @@ -24,49 +47,64 @@ struct ActivateCommandData<'a> { dry_activate: bool, } -fn build_activate_command(data: &ActivateCommandData) -> String { - let mut self_activate_command = format!("{}/activate-rs", data.closure); - - if data.debug_logs { - self_activate_command = format!("{} --debug-logs", self_activate_command); +impl<'a> ActivateCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + ActivateCommand { + sudo: d.sudo.as_deref(), + profile_path: &d.profile_path, + temp_path: &d.temp_path, + closure: &d.profile.profile_settings.path, + auto_rollback: d.merged_settings.auto_rollback, + confirm_timeout: d.merged_settings.confirm_timeout.unwrap_or(30), + magic_rollback: d.merged_settings.magic_rollback, + debug_logs: d.flags.debug_logs, + log_dir: d.flags.log_dir.as_deref(), + dry_activate: d.flags.dry_activate, + } } - if let Some(log_dir) = data.log_dir { - self_activate_command = format!("{} --log-dir {}", self_activate_command, log_dir); - } + fn build(self) -> String { + let mut cmd = format!("{}/activate-rs", self.closure); - self_activate_command = format!( - "{} activate '{}' '{}' --temp-path '{}'", - self_activate_command, data.closure, data.profile_path, data.temp_path - ); + if self.debug_logs { + cmd = format!("{} --debug-logs", cmd); + } - self_activate_command = format!( - "{} --confirm-timeout {}", - self_activate_command, data.confirm_timeout - ); + if let Some(log_dir) = self.log_dir { + cmd = format!("{} --log-dir {}", cmd, log_dir); + } - if data.magic_rollback { - self_activate_command = format!("{} --magic-rollback", self_activate_command); - } + cmd = format!( + "{} activate '{}' '{}' --temp-path '{}'", + cmd, self.closure, self.profile_path, self.temp_path + ); - if data.auto_rollback { - self_activate_command = format!("{} --auto-rollback", self_activate_command); - } + cmd = format!("{} --confirm-timeout {}", cmd, self.confirm_timeout); - if data.dry_activate { - self_activate_command = format!("{} --dry-activate", self_activate_command); - } + if self.magic_rollback { + cmd = format!("{} --magic-rollback", cmd); + } - if let Some(sudo_cmd) = &data.sudo { - self_activate_command = format!("{} {}", sudo_cmd, self_activate_command); - } + if self.auto_rollback { + cmd = format!("{} --auto-rollback", cmd); + } - self_activate_command + if self.dry_activate { + cmd = format!("{} --dry-activate", cmd); + } + + if let Some(sudo_cmd) = &self.sudo { + cmd = format!("{} {}", sudo_cmd, cmd); + } + + debug!("Built command: ActivateCommand -> {}", cmd); + cmd + } } #[test] fn test_activation_command_builder() { - let sudo = Some("sudo -u test".to_string()); + let sudo = Some("sudo -u test"); let profile_path = "/blah/profiles/test"; let closure = "/nix/store/blah/etc"; let auto_rollback = true; @@ -78,8 +116,8 @@ fn test_activation_command_builder() { let log_dir = Some("/tmp/something.txt"); assert_eq!( - build_activate_command(&ActivateCommandData { - sudo: &sudo, + ActivateCommand { + sudo, profile_path, closure, auto_rollback, @@ -89,113 +127,167 @@ fn test_activation_command_builder() { debug_logs, log_dir, dry_activate - }), + }.build(), "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt activate '/nix/store/blah/etc' '/blah/profiles/test' --temp-path '/tmp' --confirm-timeout 30 --magic-rollback --auto-rollback" .to_string(), ); } -struct WaitCommandData<'a> { - sudo: &'a Option, +pub struct WaitCommand<'a> { + sudo: Option<&'a str>, closure: &'a str, temp_path: &'a str, debug_logs: bool, log_dir: Option<&'a str>, } -fn build_wait_command(data: &WaitCommandData) -> String { - let mut self_activate_command = format!("{}/activate-rs", data.closure); - - if data.debug_logs { - self_activate_command = format!("{} --debug-logs", self_activate_command); +impl<'a> WaitCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + WaitCommand { + sudo: d.sudo.as_deref(), + temp_path: &d.temp_path, + closure: &d.profile.profile_settings.path, + debug_logs: d.flags.debug_logs, + log_dir: d.flags.log_dir.as_deref(), + } } - if let Some(log_dir) = data.log_dir { - self_activate_command = format!("{} --log-dir {}", self_activate_command, log_dir); - } + fn build(self) -> String { + let mut cmd = format!("{}/activate-rs", self.closure); - self_activate_command = format!( - "{} wait '{}' --temp-path '{}'", - self_activate_command, data.closure, data.temp_path, - ); + if self.debug_logs { + cmd = format!("{} --debug-logs", cmd); + } - if let Some(sudo_cmd) = &data.sudo { - self_activate_command = format!("{} {}", sudo_cmd, self_activate_command); - } + if let Some(log_dir) = self.log_dir { + cmd = format!("{} --log-dir {}", cmd, log_dir); + } + + cmd = format!( + "{} wait '{}' --temp-path '{}'", + cmd, self.closure, self.temp_path, + ); + + if let Some(sudo_cmd) = &self.sudo { + cmd = format!("{} {}", sudo_cmd, cmd); + } - self_activate_command + debug!("Built command: WaitCommand -> {}", cmd); + cmd + } } #[test] fn test_wait_command_builder() { - let sudo = Some("sudo -u test".to_string()); + let sudo = Some("sudo -u test"); let closure = "/nix/store/blah/etc"; let temp_path = "/tmp"; let debug_logs = true; let log_dir = Some("/tmp/something.txt"); assert_eq!( - build_wait_command(&WaitCommandData { - sudo: &sudo, + WaitCommand { + sudo, closure, temp_path, debug_logs, log_dir - }), + }.build(), "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt wait '/nix/store/blah/etc' --temp-path '/tmp'" .to_string(), ); } -struct RevokeCommandData<'a> { - sudo: &'a Option, +pub struct RevokeCommand<'a> { + sudo: Option<&'a str>, closure: &'a str, profile_path: &'a str, debug_logs: bool, log_dir: Option<&'a str>, } -fn build_revoke_command(data: &RevokeCommandData) -> String { - let mut self_activate_command = format!("{}/activate-rs", data.closure); - - if data.debug_logs { - self_activate_command = format!("{} --debug-logs", self_activate_command); +impl<'a> RevokeCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + RevokeCommand { + sudo: d.sudo.as_deref(), + profile_path: &d.profile_path, + closure: &d.profile.profile_settings.path, + debug_logs: d.flags.debug_logs, + log_dir: d.flags.log_dir.as_deref(), + } } - if let Some(log_dir) = data.log_dir { - self_activate_command = format!("{} --log-dir {}", self_activate_command, log_dir); - } + fn build(self) -> String { + let mut cmd = format!("{}/activate-rs", self.closure); - self_activate_command = format!("{} revoke '{}'", self_activate_command, data.profile_path); + if self.debug_logs { + cmd = format!("{} --debug-logs", cmd); + } - if let Some(sudo_cmd) = &data.sudo { - self_activate_command = format!("{} {}", sudo_cmd, self_activate_command); - } + if let Some(log_dir) = self.log_dir { + cmd = format!("{} --log-dir {}", cmd, log_dir); + } + + cmd = format!("{} revoke '{}'", cmd, self.profile_path); - self_activate_command + if let Some(sudo_cmd) = &self.sudo { + cmd = format!("{} {}", sudo_cmd, cmd); + } + + debug!("Built command: RevokeCommand -> {}", cmd); + cmd + } } #[test] fn test_revoke_command_builder() { - let sudo = Some("sudo -u test".to_string()); + let sudo = Some("sudo -u test"); let closure = "/nix/store/blah/etc"; let profile_path = "/nix/var/nix/per-user/user/profile"; let debug_logs = true; let log_dir = Some("/tmp/something.txt"); assert_eq!( - build_revoke_command(&RevokeCommandData { - sudo: &sudo, + RevokeCommand { + sudo, closure, profile_path, debug_logs, log_dir - }), + }.build(), "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt revoke '/nix/var/nix/per-user/user/profile'" .to_string(), ); } +pub struct ConfirmCommand<'a> { + sudo: Option<&'a str>, + temp_path: &'a str, + closure: &'a str, +} + +impl<'a> ConfirmCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + ConfirmCommand { + sudo: d.sudo.as_deref(), + temp_path: &d.temp_path, + closure: &d.profile.profile_settings.path, + } + } + + fn build(self) -> String { + let lock_path = super::make_lock_path(self.temp_path, self.closure); + + let mut cmd = format!("rm {}", lock_path); + if let Some(sudo_cmd) = &self.sudo { + cmd = format!("{} {}", sudo_cmd, cmd); + } + + debug!("Built command: ConfirmCommand -> {}", cmd); + cmd + } +} + #[derive(Error, Debug)] pub enum ConfirmProfileError { #[error("Failed to run confirmation command over SSH (the server should roll back): {0}")] @@ -207,39 +299,24 @@ pub enum ConfirmProfileError { } pub async fn confirm_profile( - deploy_data: &super::DeployData<'_>, - deploy_defs: &super::DeployDefs, - temp_path: Cow<'_, str>, - ssh_addr: &str, + ssh: SshCommand<'_>, + confirm: ConfirmCommand<'_>, ) -> Result<(), ConfirmProfileError> { - let mut ssh_confirm_command = Command::new("ssh"); - ssh_confirm_command.arg(ssh_addr); - - for ssh_opt in &deploy_data.merged_settings.ssh_opts { - ssh_confirm_command.arg(ssh_opt); - } - - let lock_path = super::make_lock_path(&temp_path, &deploy_data.profile.profile_settings.path); + debug!("Entering confirm_profile function ..."); - let mut confirm_command = format!("rm {}", lock_path); - if let Some(sudo_cmd) = &deploy_defs.sudo { - confirm_command = format!("{} {}", sudo_cmd, confirm_command); - } + let mut ssh_confirm_cmd = ssh.build(); - debug!( - "Attempting to run command to confirm deployment: {}", - confirm_command - ); + let confirm_cmd = confirm.build(); - let ssh_confirm_exit_status = ssh_confirm_command - .arg(confirm_command) - .status() + let ssh_confirm_cmd_handle = ssh_confirm_cmd + .arg(confirm_cmd) + .output() .await .map_err(ConfirmProfileError::SSHConfirm)?; - match ssh_confirm_exit_status.code() { + match ssh_confirm_cmd_handle.status.code() { Some(0) => (), - a => return Err(ConfirmProfileError::SSHConfirmExit(a)), + a => error!("{}", ConfirmProfileError::SSHConfirmExit(a)), }; info!("Deployment confirmed."); @@ -267,103 +344,63 @@ pub enum DeployProfileError { } pub async fn deploy_profile( - deploy_data: &super::DeployData<'_>, - deploy_defs: &super::DeployDefs, - dry_activate: bool, + node_name: &str, + profile_name: &str, + ssh: SshCommand<'_>, + activate: ActivateCommand<'_>, + wait: WaitCommand<'_>, + confirm: ConfirmCommand<'_>, ) -> Result<(), DeployProfileError> { - if !dry_activate { + debug!("Entering deploy_profile function ..."); + + if !activate.dry_activate { info!( "Activating profile `{}` for node `{}`", - deploy_data.profile_name, deploy_data.node_name + profile_name, node_name ); } + let dry_activate = &activate.dry_activate.clone(); + let magic_rollback = &activate.magic_rollback.clone(); - let temp_path: Cow = match &deploy_data.merged_settings.temp_path { - Some(x) => x.into(), - None => "/tmp".into(), - }; - - let confirm_timeout = deploy_data.merged_settings.confirm_timeout.unwrap_or(30); + let activate_cmd = activate.build(); - let magic_rollback = deploy_data.merged_settings.magic_rollback.unwrap_or(true); + let mut ssh_activate_cmd = ssh.build(); - let auto_rollback = deploy_data.merged_settings.auto_rollback.unwrap_or(true); - - let self_activate_command = build_activate_command(&ActivateCommandData { - sudo: &deploy_defs.sudo, - profile_path: &deploy_defs.profile_path, - closure: &deploy_data.profile.profile_settings.path, - auto_rollback, - temp_path: &temp_path, - confirm_timeout, - magic_rollback, - debug_logs: deploy_data.debug_logs, - log_dir: deploy_data.log_dir, - dry_activate, - }); - - debug!("Constructed activation command: {}", self_activate_command); - - let hostname = match deploy_data.cmd_overrides.hostname { - Some(ref x) => x, - None => &deploy_data.node.node_settings.hostname, - }; - - let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname); - - let mut ssh_activate_command = Command::new("ssh"); - ssh_activate_command.arg(&ssh_addr); - - for ssh_opt in &deploy_data.merged_settings.ssh_opts { - ssh_activate_command.arg(&ssh_opt); - } - - if !magic_rollback || dry_activate { - let ssh_activate_exit_status = ssh_activate_command - .arg(self_activate_command) - .status() - .await - .map_err(DeployProfileError::SSHActivate)?; - - match ssh_activate_exit_status.code() { + if !*magic_rollback || *dry_activate { + let ssh_activate_cmd_handle = ssh_activate_cmd + .arg(activate_cmd) + .spawn() + .map_err(DeployProfileError::SSHActivate)? + .wait() + .await; + + match ssh_activate_cmd_handle + .map_err(DeployProfileError::SSHActivate)? + .code() + { Some(0) => (), - a => return Err(DeployProfileError::SSHActivateExit(a)), + a => error!("{}", DeployProfileError::SSHActivateExit(a)), }; - if dry_activate { + if *dry_activate { info!("Completed dry-activate!"); } else { info!("Success activating, done!"); } } else { - let self_wait_command = build_wait_command(&WaitCommandData { - sudo: &deploy_defs.sudo, - closure: &deploy_data.profile.profile_settings.path, - temp_path: &temp_path, - debug_logs: deploy_data.debug_logs, - log_dir: deploy_data.log_dir, - }); - - debug!("Constructed wait command: {}", self_wait_command); - - let ssh_activate = ssh_activate_command - .arg(self_activate_command) + let ssh_activate = ssh_activate_cmd + .arg(activate_cmd) .spawn() .map_err(DeployProfileError::SSHSpawnActivate)?; info!("Creating activation waiter"); - - let mut ssh_wait_command = Command::new("ssh"); - ssh_wait_command.arg(&ssh_addr); - - for ssh_opt in &deploy_data.merged_settings.ssh_opts { - ssh_wait_command.arg(ssh_opt); - } + let wait_cmd = wait.build(); + let mut ssh_wait_cmd = ssh.build(); let (send_activate, recv_activate) = tokio::sync::oneshot::channel(); let (send_activated, recv_activated) = tokio::sync::oneshot::channel(); - tokio::spawn(async move { + let thread = tokio::spawn(async move { let o = ssh_activate.wait_with_output().await; let maybe_err = match o { @@ -381,11 +418,11 @@ pub async fn deploy_profile( send_activated.send(()).unwrap(); }); tokio::select! { - x = ssh_wait_command.arg(self_wait_command).status() => { + x = ssh_wait_cmd.arg(wait_cmd).output() => { debug!("Wait command ended"); - match x.map_err(DeployProfileError::SSHWait)?.code() { + match x.map_err(DeployProfileError::SSHWait)?.status.code() { Some(0) => (), - a => return Err(DeployProfileError::SSHWaitExit(a)), + a => error!("{}",DeployProfileError::SSHWaitExit(a)), }; }, x = recv_activate => { @@ -396,9 +433,13 @@ pub async fn deploy_profile( info!("Success activating, attempting to confirm activation"); - let c = confirm_profile(deploy_data, deploy_defs, temp_path, &ssh_addr).await; + let c = confirm_profile(ssh, confirm).await; recv_activated.await.unwrap(); c?; + + thread + .await + .map_err(|x| DeployProfileError::SSHActivate(x.into()))?; } Ok(()) @@ -413,44 +454,30 @@ pub enum RevokeProfileError { SSHRevoke(std::io::Error), #[error("Revoking over SSH resulted in a bad exit code: {0:?}")] SSHRevokeExit(Option), - - #[error("Deployment data invalid: {0}")] - InvalidDeployDataDefs(#[from] DeployDataDefsError), } pub async fn revoke( - deploy_data: &crate::DeployData<'_>, - deploy_defs: &crate::DeployDefs, + node_name: &str, + profile_name: &str, + ssh: SshCommand<'_>, + revoke: RevokeCommand<'_>, ) -> Result<(), RevokeProfileError> { - let self_revoke_command = build_revoke_command(&RevokeCommandData { - sudo: &deploy_defs.sudo, - closure: &deploy_data.profile.profile_settings.path, - profile_path: &deploy_data.get_profile_path()?, - debug_logs: deploy_data.debug_logs, - log_dir: deploy_data.log_dir, - }); - - debug!("Constructed revoke command: {}", self_revoke_command); - - let hostname = match deploy_data.cmd_overrides.hostname { - Some(ref x) => x, - None => &deploy_data.node.node_settings.hostname, - }; + debug!("Entering revoke function ..."); - let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname); + info!( + "Revoking profile `{}` for node `{}`", + profile_name, node_name + ); - let mut ssh_activate_command = Command::new("ssh"); - ssh_activate_command.arg(&ssh_addr); + let revoke_cmd = revoke.build(); - for ssh_opt in &deploy_data.merged_settings.ssh_opts { - ssh_activate_command.arg(&ssh_opt); - } + let mut ssh_revoke_cmd = ssh.build(); - let ssh_revoke = ssh_activate_command - .arg(self_revoke_command) + let ssh_revoke_cmd = ssh_revoke_cmd + .arg(revoke_cmd) .spawn() .map_err(RevokeProfileError::SSHSpawnRevoke)?; - let result = ssh_revoke.wait_with_output().await; + let result = ssh_revoke_cmd.wait_with_output().await; match result { Err(x) => Err(RevokeProfileError::SSHRevoke(x)), diff --git a/src/flake.rs b/src/flake.rs new file mode 100644 index 00000000..691e5e4e --- /dev/null +++ b/src/flake.rs @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: 2020 Serokell +// SPDX-FileCopyrightText: 2021 Yannik Sander +// +// SPDX-License-Identifier: MPL-2.0 + +use crate as deploy; + +use self::deploy::{data, settings}; +use futures_util::stream::{StreamExt, TryStreamExt}; +use log::{error, info}; +use std::process::Stdio; +use thiserror::Error; +use tokio::process::Command; + +#[derive(Error, Debug)] +pub enum CheckDeploymentError { + #[error("Failed to execute Nix checking command: {0}")] + NixCheck(#[from] std::io::Error), + #[error("Nix checking command resulted in a bad exit code: {0:?}")] + NixCheckExit(Option), +} + +pub async fn check_deployment( + supports_flakes: bool, + repo: &str, + extra_build_args: &[String], +) -> Result<(), CheckDeploymentError> { + info!("Running checks for flake in {}", repo); + + let mut check_command = match supports_flakes { + true => Command::new("nix"), + false => Command::new("nix-build"), + }; + + if supports_flakes { + check_command.arg("flake").arg("check").arg(repo); + } else { + check_command.arg("-E") + .arg("--no-out-link") + .arg(format!("let r = import {}/.; x = (if builtins.isFunction r then (r {{}}) else r); in if x ? checks then x.checks.${{builtins.currentSystem}} else {{}}", repo)); + }; + + for extra_arg in extra_build_args { + check_command.arg(extra_arg); + } + + let check_status = check_command.status().await?; + + match check_status.code() { + Some(0) => (), + a => return Err(CheckDeploymentError::NixCheckExit(a)), + }; + + Ok(()) +} + +#[derive(Error, Debug)] +pub enum GetDeploymentDataError { + #[error("Failed to execute nix eval command: {0}")] + NixEval(std::io::Error), + #[error("Failed to read output from evaluation: {0}")] + NixEvalOut(std::io::Error), + #[error("Evaluation resulted in a bad exit code: {0:?}")] + NixEvalExit(Option), + #[error("Error converting evaluation output to utf8: {0}")] + DecodeUtf8(#[from] std::string::FromUtf8Error), + #[error("Error decoding the JSON from evaluation: {0}")] + DecodeJson(#[from] serde_json::error::Error), + #[error("Impossible happened: profile is set but node is not")] + ProfileNoNode, +} + +/// Evaluates the Nix in the given `repo` and return the processed Data from it +pub async fn get_deployment_data( + supports_flakes: bool, + flakes: &[data::Target], + extra_build_args: &[String], +) -> Result, GetDeploymentDataError> { + futures_util::stream::iter(flakes).then(|flake| async move { + + info!("Evaluating flake in {}", flake.repo); + + let mut c = if supports_flakes { + Command::new("nix") + } else { + Command::new("nix-instantiate") + }; + + if supports_flakes { + c.arg("eval") + .arg("--json") + .arg(format!("{}#deploy", flake.repo)) + // We use --apply instead of --expr so that we don't have to deal with builtins.getFlake + .arg("--apply"); + match (&flake.node, &flake.profile) { + (Some(node), Some(profile)) => { + // Ignore all nodes and all profiles but the one we're evaluating + c.arg(format!( + r#" + deploy: + (deploy // {{ + nodes = {{ + "{0}" = deploy.nodes."{0}" // {{ + profiles = {{ + inherit (deploy.nodes."{0}".profiles) "{1}"; + }}; + }}; + }}; + }}) + "#, + node, profile + )) + } + (Some(node), None) => { + // Ignore all nodes but the one we're evaluating + c.arg(format!( + r#" + deploy: + (deploy // {{ + nodes = {{ + inherit (deploy.nodes) "{}"; + }}; + }}) + "#, + node + )) + } + (None, None) => { + // We need to evaluate all profiles of all nodes anyway, so just do it strictly + c.arg("deploy: deploy") + } + (None, Some(_)) => return Err(GetDeploymentDataError::ProfileNoNode), + } + } else { + c + .arg("--strict") + .arg("--read-write-mode") + .arg("--json") + .arg("--eval") + .arg("-E") + .arg(format!("let r = import {}/.; in if builtins.isFunction r then (r {{}}).deploy else r.deploy", flake.repo)) + }; + + for extra_arg in extra_build_args { + c.arg(extra_arg); + } + + let build_child = c + .stdout(Stdio::piped()) + .spawn() + .map_err(GetDeploymentDataError::NixEval)?; + + let build_output = build_child + .wait_with_output() + .await + .map_err(GetDeploymentDataError::NixEvalOut)?; + + match build_output.status.code() { + Some(0) => (), + a => return Err(GetDeploymentDataError::NixEvalExit(a)), + }; + + let data_json = String::from_utf8(build_output.stdout)?; + + Ok(serde_json::from_str(&data_json)?) +}).try_collect().await +} diff --git a/src/lib.rs b/src/lib.rs index 981ec1ed..b943546c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,12 +4,6 @@ // // SPDX-License-Identifier: MPL-2.0 -use rnix::{types::*, SyntaxKind::*}; - -use merge::Merge; - -use thiserror::Error; - use flexi_logger::*; pub fn make_lock_path(temp_path: &str, closure: &str) -> String { @@ -145,296 +139,9 @@ pub fn init_logger( Ok(()) } +pub mod cli; pub mod data; pub mod deploy; +pub mod flake; pub mod push; -pub mod cli; - -#[derive(Debug)] -pub struct CmdOverrides { - pub ssh_user: Option, - pub profile_user: Option, - pub ssh_opts: Option, - pub fast_connection: Option, - pub auto_rollback: Option, - pub hostname: Option, - pub magic_rollback: Option, - pub temp_path: Option, - pub confirm_timeout: Option, - pub dry_activate: bool, -} - -#[derive(PartialEq, Debug)] -pub struct DeployFlake<'a> { - pub repo: &'a str, - pub node: Option, - pub profile: Option, -} - -#[derive(Error, Debug)] -pub enum ParseFlakeError { - #[error("The given path was too long, did you mean to put something in quotes?")] - PathTooLong, - #[error("Unrecognized node or token encountered")] - Unrecognized, -} -pub fn parse_flake(flake: &str) -> Result { - let flake_fragment_start = flake.find('#'); - let (repo, maybe_fragment) = match flake_fragment_start { - Some(s) => (&flake[..s], Some(&flake[s + 1..])), - None => (flake, None), - }; - - let mut node: Option = None; - let mut profile: Option = None; - - if let Some(fragment) = maybe_fragment { - let ast = rnix::parse(fragment); - - let first_child = match ast.root().node().first_child() { - Some(x) => x, - None => { - return Ok(DeployFlake { - repo, - node: None, - profile: None, - }) - } - }; - - let mut node_over = false; - - for entry in first_child.children_with_tokens() { - let x: Option = match (entry.kind(), node_over) { - (TOKEN_DOT, false) => { - node_over = true; - None - } - (TOKEN_DOT, true) => { - return Err(ParseFlakeError::PathTooLong); - } - (NODE_IDENT, _) => Some(entry.into_node().unwrap().text().to_string()), - (TOKEN_IDENT, _) => Some(entry.into_token().unwrap().text().to_string()), - (NODE_STRING, _) => { - let c = entry - .into_node() - .unwrap() - .children_with_tokens() - .nth(1) - .unwrap(); - - Some(c.into_token().unwrap().text().to_string()) - } - _ => return Err(ParseFlakeError::Unrecognized), - }; - - if !node_over { - node = x; - } else { - profile = x; - } - } - } - - Ok(DeployFlake { - repo, - node, - profile, - }) -} - -#[test] -fn test_parse_flake() { - assert_eq!( - parse_flake("../deploy/examples/system").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: None, - profile: None, - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system#").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: None, - profile: None, - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system#computer.\"something.nix\"").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: Some("computer".to_string()), - profile: Some("something.nix".to_string()), - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system#\"example.com\".system").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: Some("example.com".to_string()), - profile: Some("system".to_string()), - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system#example").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: Some("example".to_string()), - profile: None - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system#example.system").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: Some("example".to_string()), - profile: Some("system".to_string()) - } - ); - - assert_eq!( - parse_flake("../deploy/examples/system").unwrap(), - DeployFlake { - repo: "../deploy/examples/system", - node: None, - profile: None, - } - ); -} - -#[derive(Debug, Clone)] -pub struct DeployData<'a> { - pub node_name: &'a str, - pub node: &'a data::Node, - pub profile_name: &'a str, - pub profile: &'a data::Profile, - - pub cmd_overrides: &'a CmdOverrides, - - pub merged_settings: data::GenericSettings, - - pub debug_logs: bool, - pub log_dir: Option<&'a str>, -} - -#[derive(Debug)] -pub struct DeployDefs { - pub ssh_user: String, - pub profile_user: String, - pub profile_path: String, - pub sudo: Option, -} - -#[derive(Error, Debug)] -pub enum DeployDataDefsError { - #[error("Neither `user` nor `sshUser` are set for profile {0} of node {1}")] - NoProfileUser(String, String), -} - -impl<'a> DeployData<'a> { - pub fn defs(&'a self) -> Result { - let ssh_user = match self.merged_settings.ssh_user { - Some(ref u) => u.clone(), - None => whoami::username(), - }; - - let profile_user = self.get_profile_user()?; - - let profile_path = self.get_profile_path()?; - - let sudo: Option = match self.merged_settings.user { - Some(ref user) if user != &ssh_user => Some(format!("sudo -u {}", user)), - _ => None, - }; - - Ok(DeployDefs { - ssh_user, - profile_user, - profile_path, - sudo, - }) - } - - fn get_profile_path(&'a self) -> Result { - let profile_user = self.get_profile_user()?; - let profile_path = match self.profile.profile_settings.profile_path { - None => match &profile_user[..] { - "root" => format!("/nix/var/nix/profiles/{}", self.profile_name), - _ => format!( - "/nix/var/nix/profiles/per-user/{}/{}", - profile_user, self.profile_name - ), - }, - Some(ref x) => x.clone(), - }; - Ok(profile_path) - } - - fn get_profile_user(&'a self) -> Result { - let profile_user = match self.merged_settings.user { - Some(ref x) => x.clone(), - None => match self.merged_settings.ssh_user { - Some(ref x) => x.clone(), - None => { - return Err(DeployDataDefsError::NoProfileUser( - self.profile_name.to_owned(), - self.node_name.to_owned(), - )) - } - }, - }; - Ok(profile_user) - } -} - -pub fn make_deploy_data<'a, 's>( - top_settings: &'s data::GenericSettings, - node: &'a data::Node, - node_name: &'a str, - profile: &'a data::Profile, - profile_name: &'a str, - cmd_overrides: &'a CmdOverrides, - debug_logs: bool, - log_dir: Option<&'a str>, -) -> DeployData<'a> { - let mut merged_settings = profile.generic_settings.clone(); - merged_settings.merge(node.generic_settings.clone()); - merged_settings.merge(top_settings.clone()); - - if cmd_overrides.ssh_user.is_some() { - merged_settings.ssh_user = cmd_overrides.ssh_user.clone(); - } - if cmd_overrides.profile_user.is_some() { - merged_settings.user = cmd_overrides.profile_user.clone(); - } - if let Some(ref ssh_opts) = cmd_overrides.ssh_opts { - merged_settings.ssh_opts = ssh_opts.split(' ').map(|x| x.to_owned()).collect(); - } - if let Some(fast_connection) = cmd_overrides.fast_connection { - merged_settings.fast_connection = Some(fast_connection); - } - if let Some(auto_rollback) = cmd_overrides.auto_rollback { - merged_settings.auto_rollback = Some(auto_rollback); - } - if let Some(magic_rollback) = cmd_overrides.magic_rollback { - merged_settings.magic_rollback = Some(magic_rollback); - } - - DeployData { - node_name, - node, - profile_name, - profile, - cmd_overrides, - merged_settings, - debug_logs, - log_dir, - } -} +pub mod settings; diff --git a/src/push.rs b/src/push.rs index 69eba0db..5e5c7e3b 100644 --- a/src/push.rs +++ b/src/push.rs @@ -2,13 +2,14 @@ // // SPDX-License-Identifier: MPL-2.0 -use log::{debug, info}; +use log::{debug, error, info}; use std::collections::HashMap; use std::path::Path; -use std::process::Stdio; use thiserror::Error; use tokio::process::Command; +use crate::data; + #[derive(Error, Debug)] pub enum PushProfileError { #[error("Failed to run Nix show-derivation command: {0}")] @@ -41,40 +42,182 @@ pub enum PushProfileError { Copy(std::io::Error), #[error("Nix copy command resulted in a bad exit code: {0:?}")] CopyExit(Option), + + #[error("Deployment data invalid: {0}")] + DeployData(#[from] data::DeployDataError), } -pub struct PushProfileData<'a> { - pub supports_flakes: bool, - pub check_sigs: bool, - pub repo: &'a str, - pub deploy_data: &'a super::DeployData<'a>, - pub deploy_defs: &'a super::DeployDefs, - pub keep_result: bool, - pub result_path: Option<&'a str>, - pub extra_build_args: &'a [String], +pub struct ShowDerivationCommand<'a> { + closure: &'a str, } -pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileError> { - debug!( - "Finding the deriver of store path for {}", - &data.deploy_data.profile.profile_settings.path - ); +impl<'a> ShowDerivationCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + ShowDerivationCommand { + closure: d.profile.profile_settings.path.as_str(), + } + } + + fn build(self) -> Command { + // `nix-store --query --deriver` doesn't work on invalid paths, so we parse output of show-derivation :( + let mut cmd = Command::new("nix"); + + cmd.arg("show-derivation").arg(&self.closure); + //cmd.what_is_this; + + debug!("Built command: ShowDerivationCommand -> {:?}", cmd); + cmd + } +} - // `nix-store --query --deriver` doesn't work on invalid paths, so we parse output of show-derivation :( - let mut show_derivation_command = Command::new("nix"); +pub struct SignCommand<'a> { + closure: &'a str, +} + +impl<'a> SignCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + SignCommand { + closure: d.profile.profile_settings.path.as_str(), + } + } + + fn build(self, local_key: String) -> Command { + let mut cmd = Command::new("nix"); - show_derivation_command - .arg("show-derivation") - .arg(&data.deploy_data.profile.profile_settings.path); + cmd.arg("sign-paths") + .arg("-r") + .arg("-k") + .arg(local_key) + .arg(&self.closure); + //cmd.what_is_this; + + debug!("Built command: SignCommand -> {:?}", cmd); + cmd + } +} + +pub struct CopyCommand<'a> { + closure: &'a str, + fast_connection: bool, + check_sigs: &'a bool, + hostname: &'a str, + nix_ssh_opts: String, +} + +impl<'a> CopyCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + // ssh_uri: ssh://host:port + let (uri, port) = d.ssh_uri.as_str().rsplit_once(":").unwrap(); + CopyCommand { + closure: d.profile.profile_settings.path.as_str(), + fast_connection: d.merged_settings.fast_connection, + check_sigs: &d.flags.checksigs, + hostname: uri, + nix_ssh_opts: format!( + "{} -p {}", + d.merged_settings + .ssh_opts + .iter() + .fold("".to_string(), |s, o| format!("{} {}", s, o)), + port, + ), + } + } + + fn build(self) -> Command { + let mut cmd = Command::new("nix"); + + cmd.arg("-L").arg("copy"); + + if !self.fast_connection { + cmd.arg("--substitute-on-destination"); + } + + if !self.check_sigs { + cmd.arg("--no-check-sigs"); + } + cmd.arg("--to") + .arg(self.hostname) + .arg(self.closure) + .env("NIX_SSHOPTS", self.nix_ssh_opts); + //cmd.what_is_this; + + debug!("Built command: CopyCommand -> {:?}", cmd); + cmd + } +} + +pub struct BuildCommand<'a> { + node_name: &'a str, + profile_name: &'a str, + keep_result: &'a bool, + result_path: &'a str, + extra_build_args: &'a Vec, +} + +impl<'a> BuildCommand<'a> { + pub fn from_data(d: &'a data::DeployData) -> Self { + BuildCommand { + node_name: d.node_name.as_str(), + profile_name: d.profile_name.as_str(), + keep_result: &d.flags.keep_result, + result_path: d.flags.result_path.as_deref().unwrap_or("./.deploy-gc"), + extra_build_args: &d.flags.extra_build_args, + } + } - let show_derivation_output = show_derivation_command + fn build(self, derivation_name: &str, supports_flakes: bool) -> Command { + let mut cmd = if supports_flakes { + Command::new("nix") + } else { + Command::new("nix-build") + }; + + if supports_flakes { + cmd.arg("-L").arg("build").arg(derivation_name) + } else { + cmd.arg(derivation_name) + }; + + match (self.keep_result, supports_flakes) { + (true, _) => cmd.arg("--out-link").arg(format!( + "{}/{}/{}", + self.result_path, self.node_name, self.profile_name + )), + (false, false) => cmd.arg("--no-out-link"), + (false, true) => cmd.arg("--no-link"), + }; + cmd.args(self.extra_build_args.iter()); + // cmd.what_is_this; + + debug!("Built command: BuildCommand -> {:?}", cmd); + cmd + } +} + +pub async fn push_profile( + supports_flakes: bool, + show_derivation: ShowDerivationCommand<'_>, + build: BuildCommand<'_>, + sign: SignCommand<'_>, + copy: CopyCommand<'_>, +) -> Result<(), PushProfileError> { + debug!("Entering push_profil function ..."); + + let node_name = build.node_name; + let profile_name = build.profile_name; + let closure = show_derivation.closure; + + let mut show_derivation_cmd = show_derivation.build(); + + let show_derivation_output = show_derivation_cmd .output() .await .map_err(PushProfileError::ShowDerivation)?; match show_derivation_output.status.code() { Some(0) => (), - a => return Err(PushProfileError::ShowDerivationExit(a)), + a => error!("{}", PushProfileError::ShowDerivationExit(a)), }; let derivation_info: HashMap<&str, serde_json::value::Value> = serde_json::from_str( @@ -90,139 +233,58 @@ pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileEr info!( "Building profile `{}` for node `{}`", - data.deploy_data.profile_name, data.deploy_data.node_name + profile_name, node_name ); - let mut build_command = if data.supports_flakes { - Command::new("nix") - } else { - Command::new("nix-build") - }; - - if data.supports_flakes { - build_command.arg("build").arg(derivation_name) - } else { - build_command.arg(derivation_name) - }; - - match (data.keep_result, data.supports_flakes) { - (true, _) => { - let result_path = data.result_path.unwrap_or("./.deploy-gc"); - - build_command.arg("--out-link").arg(format!( - "{}/{}/{}", - result_path, data.deploy_data.node_name, data.deploy_data.profile_name - )) - } - (false, false) => build_command.arg("--no-out-link"), - (false, true) => build_command.arg("--no-link"), - }; - - for extra_arg in data.extra_build_args { - build_command.arg(extra_arg); - } + let mut build_cmd = build.build(*derivation_name, supports_flakes); - let build_exit_status = build_command - // Logging should be in stderr, this just stops the store path from printing for no reason - .stdout(Stdio::null()) - .status() - .await - .map_err(PushProfileError::Build)?; + let build_cmd_handle = build_cmd + .spawn() + .map_err(PushProfileError::Build)? + .wait() + .await; - match build_exit_status.code() { + match build_cmd_handle.map_err(PushProfileError::Build)?.code() { Some(0) => (), - a => return Err(PushProfileError::BuildExit(a)), + a => error!("{}", PushProfileError::BuildExit(a)), }; - if !Path::new( - format!( - "{}/deploy-rs-activate", - data.deploy_data.profile.profile_settings.path - ) - .as_str(), - ) - .exists() - { - return Err(PushProfileError::DeployRsActivateDoesntExist); + if !Path::new(format!("{}/deploy-rs-activate", closure).as_str()).exists() { + error!("{}", PushProfileError::DeployRsActivateDoesntExist); } - if !Path::new( - format!( - "{}/activate-rs", - data.deploy_data.profile.profile_settings.path - ) - .as_str(), - ) - .exists() - { - return Err(PushProfileError::ActivateRsDoesntExist); + if !Path::new(format!("{}/activate-rs", closure).as_str()).exists() { + error!("{}", PushProfileError::ActivateRsDoesntExist); } if let Ok(local_key) = std::env::var("LOCAL_KEY") { info!( "Signing key present! Signing profile `{}` for node `{}`", - data.deploy_data.profile_name, data.deploy_data.node_name + profile_name, node_name ); - let sign_exit_status = Command::new("nix") - .arg("sign-paths") - .arg("-r") - .arg("-k") - .arg(local_key) - .arg(&data.deploy_data.profile.profile_settings.path) - .status() - .await - .map_err(PushProfileError::Sign)?; + let mut sign_cmd = sign.build(local_key); + let sign_cmd_handle = sign_cmd.output().await.map_err(PushProfileError::Sign)?; - match sign_exit_status.code() { + match sign_cmd_handle.status.code() { Some(0) => (), - a => return Err(PushProfileError::SignExit(a)), + a => error!("{}", PushProfileError::SignExit(a)), }; } - info!( - "Copying profile `{}` to node `{}`", - data.deploy_data.profile_name, data.deploy_data.node_name - ); + info!("Copying profile `{}` to node `{}`", profile_name, node_name); - let mut copy_command = Command::new("nix"); - copy_command.arg("copy"); + let mut copy_cmd = copy.build(); - if data.deploy_data.merged_settings.fast_connection != Some(true) { - copy_command.arg("--substitute-on-destination"); - } - - if !data.check_sigs { - copy_command.arg("--no-check-sigs"); - } - - let ssh_opts_str = data - .deploy_data - .merged_settings - .ssh_opts - // This should provide some extra safety, but it also breaks for some reason, oh well - // .iter() - // .map(|x| format!("'{}'", x)) - // .collect::>() - .join(" "); - - let hostname = match data.deploy_data.cmd_overrides.hostname { - Some(ref x) => x, - None => &data.deploy_data.node.node_settings.hostname, - }; - - let copy_exit_status = copy_command - .arg("--to") - .arg(format!("ssh://{}@{}", data.deploy_defs.ssh_user, hostname)) - .arg(&data.deploy_data.profile.profile_settings.path) - .env("NIX_SSHOPTS", ssh_opts_str) - .status() - .await - .map_err(PushProfileError::Copy)?; + let copy_exit_cmd_handle = copy_cmd + .spawn() + .map_err(PushProfileError::Copy)? + .wait() + .await; - match copy_exit_status.code() { + match copy_exit_cmd_handle.map_err(PushProfileError::Copy)?.code() { Some(0) => (), - a => return Err(PushProfileError::CopyExit(a)), + a => error!("{}", PushProfileError::CopyExit(a)), }; Ok(()) diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 00000000..9312161b --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2020 Serokell +// +// SPDX-License-Identifier: MPL-2.0 + +use clap::Parser; +use envmnt::{self, ExpandOptions, ExpansionType}; +use merge::Merge; +use serde::{Deserialize, Deserializer}; +use std::collections::HashMap; + +#[derive(Parser, Deserialize, Debug, Clone, Merge, Default)] +pub struct GenericSettings { + /// Override the SSH user with the given value + #[clap(long)] + #[serde(rename(deserialize = "sshUser"))] + pub ssh_user: Option, + /// Override the profile user with the given value + #[clap(long = "profile-user")] + pub user: Option, + /// Override the SSH options used + #[clap(long, multiple_occurrences(true))] + #[serde( + skip_serializing_if = "Vec::is_empty", + default, + rename(deserialize = "sshOpts"), + deserialize_with = "GenericSettings::de_ssh_opts" + )] + #[merge(strategy = merge::vec::append)] + pub ssh_opts: Vec, + /// Override if the connecting to the target node should be considered fast + #[clap(long)] + #[serde(rename(deserialize = "fastConnection"), default)] + #[merge(strategy = merge::bool::overwrite_false)] + pub fast_connection: bool, + /// Attempt rollback if activation fails + #[clap(long)] + #[serde(rename(deserialize = "noAutoRollback"), default)] + #[merge(strategy = merge::bool::overwrite_true)] + pub auto_rollback: bool, + /// How long activation should wait for confirmation (if using magic-rollback) + #[clap(long)] + #[serde(rename(deserialize = "confirmTimeout"))] + pub confirm_timeout: Option, + /// Where to store temporary files (only used by magic-rollback) + #[clap(long)] + #[serde(rename(deserialize = "tempPath"))] + pub temp_path: Option, + /// Do a magic rollback (see documentation) + #[clap(long)] + #[serde(rename(deserialize = "noMagicRollback"), default)] + #[merge(strategy = merge::bool::overwrite_true)] + pub magic_rollback: bool, +} + +impl GenericSettings { + fn de_ssh_opts<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let buf: Vec = Vec::deserialize(deserializer)?; + + let mut options = ExpandOptions::new(); + options.expansion_type = Some(ExpansionType::UnixBrackets); + + Ok(buf + .into_iter() + .map(|opt| envmnt::expand(&opt, Some(options))) + .collect()) + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct NodeSettings { + pub hostname: Option, + pub profiles: HashMap, + #[serde( + skip_serializing_if = "Vec::is_empty", + default, + rename(deserialize = "profilesOrder") + )] + pub profiles_order: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ProfileSettings { + pub path: String, + #[serde(rename(deserialize = "profilePath"))] + pub profile_path: Option, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Profile { + #[serde(flatten)] + pub profile_settings: ProfileSettings, + #[serde(flatten)] + pub generic_settings: GenericSettings, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Node { + #[serde(flatten)] + pub generic_settings: GenericSettings, + #[serde(flatten)] + pub node_settings: NodeSettings, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Root { + #[serde(flatten)] + pub generic_settings: GenericSettings, + pub nodes: HashMap, +}