commit e092ba5bb18939f2b30832c33fab55a968555056 Author: Zhaofeng Li Date: Tue Dec 15 20:21:26 2020 -0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2fa7a4b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,760 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "async-trait" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bytes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0dcbc35f504eb6fc275a6d20e4ebcda18cf50d40ba6fabff8c711fa16cb3b16" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "colmena" +version = "0.1.0" +dependencies = [ + "async-trait", + "clap", + "console", + "futures", + "glob", + "indicatif", + "log", + "serde", + "serde_json", + "snafu", + "tempfile", + "tokio", +] + +[[package]] +name = "console" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50aab2529019abfabfa93f1e6c41ef392f91fbf179b347a7e96abb524884a08" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "unicode-width", + "winapi", + "winapi-util", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "futures" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3b0c040a1fe6529d30b3c5944b280c7f0dcb2930d2c3062bca967b602583d0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" + +[[package]] +name = "futures-executor" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4caa2b2b68b880003057c1dd49f1ed937e38f22fcf6c212188a121f08cf40a65" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611834ce18aaa1bd13c4b374f5d653e1027cf99b6b502584ff8c9a64413b30bb" + +[[package]] +name = "futures-macro" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d" + +[[package]] +name = "futures-task" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d" +dependencies = [ + "once_cell", +] + +[[package]] +name = "futures-util" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "hermit-abi" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +dependencies = [ + "libc", +] + +[[package]] +name = "indicatif" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7baab56125e25686df467fe470785512329883aab42696d661247aca2a2896e4" +dependencies = [ + "console", + "lazy_static", + "number_prefix", + "regex", +] + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" + +[[package]] +name = "lock_api" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "mio" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33bc887064ef1fd66020c9adfc45bb9f33d75a42096c81e7c56c65b75dd1a8b" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" +dependencies = [ + "socket2", + "winapi", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a" + +[[package]] +name = "once_cell" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c6d9b8427445284a09c55be860a15855ab580a417ccad9da88f5a06787ced0" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[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", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[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.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "regex" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smallvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75" + +[[package]] +name = "snafu" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "syn" +version = "1.0.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "terminal_size" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd2d183bd3fac5f5fe38ddbeb4dc9aec4a39a9d7d59e7491d900302da01cbe1" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "tokio" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "720ba21c25078711bf456d607987d95bce90f7c3bea5abe1db587862e7a1e87c" +dependencies = [ + "autocfg", + "bytes", + "futures-core", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d30fdbb5dc2d8f91049691aa1a9d4d4ae422a21c334ce8936e5886d30c5c45" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9bc231b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "colmena" +version = "0.1.0" +authors = ["Zhaofeng Li "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = "0.1.42" +clap = "2.33.3" +console = "0.13.0" +futures = "0.3.8" +glob = "0.3.0" +indicatif = "0.15.0" +log = "0.4.11" +serde = { version = "1.0.118", features = ["derive"] } +serde_json = "1.0" +snafu = "0.6.10" +tempfile = "3.1.0" +tokio = { version = "0.3.6", features = ["full"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f3ca6b7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Zhaofeng Li + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..368a10b --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Colmena + +Colmena is a simple, stateless NixOS deployment tool modeled after NixOps and Morph, written in Rust. +It's a thin wrapper over Nix commands like `nix-instantiate` and `nix-copy-closure`, and supports parallel deployment. + +Colmena is still an early prototype. + +## Licensing + +Colmena is available under the MIT License. diff --git a/src/command/apply.rs b/src/command/apply.rs new file mode 100644 index 0000000..f9d1756 --- /dev/null +++ b/src/command/apply.rs @@ -0,0 +1,89 @@ +use clap::{Arg, App, SubCommand, ArgMatches}; + +use crate::nix::{Hive, DeploymentTask, DeploymentGoal}; +use crate::deployment::deploy; +use crate::util; + +pub fn subcommand() -> App<'static, 'static> { + let command = SubCommand::with_name("apply") + .about("Apply the configuration") + .arg(Arg::with_name("goal") + .help("Deployment goal") + .long_help("Same as the targets for switch-to-configuration.\n\"push\" means only copying the closures to remote nodes.") + .default_value("switch") + .index(1) + .possible_values(&["push", "switch", "boot", "test", "dry-activate"])) + .arg(Arg::with_name("parallel") + .short("p") + .long("parallel") + .help("Parallelism limit") + .long_help("Set to 0 to disable parallemism limit.") + .default_value("10") + .takes_value(true) + .validator(|s| { + match s.parse::() { + Ok(_) => Ok(()), + Err(_) => Err(String::from("The value must be a valid number")), + } + })) + .arg(Arg::with_name("verbose") + .short("v") + .long("verbose") + .help("Be verbose") + .long_help("Deactivates the progress spinner and prints every line of output.") + .takes_value(false)); + + util::register_common_args(command) +} + +pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { + let hive = Hive::from_config_arg(local_args).unwrap(); + + println!("Enumerating nodes..."); + let deployment_info = hive.deployment_info().await.unwrap(); + let all_nodes: Vec = deployment_info.keys().cloned().collect(); + + let selected_nodes = match local_args.value_of("on") { + Some(filter) => { + util::filter_nodes(&all_nodes, filter) + } + None => all_nodes.clone(), + }; + + if selected_nodes.len() == 0 { + println!("No hosts matched. Exiting..."); + return; + } + + if selected_nodes.len() == all_nodes.len() { + println!("Building all node configurations..."); + } else { + println!("Selected {} out of {} hosts. Building node configurations...", selected_nodes.len(), deployment_info.len()); + } + + // Some ugly argument mangling :/ + let profiles = hive.build_selected(selected_nodes).await.unwrap(); + let goal = DeploymentGoal::from_str(local_args.value_of("goal").unwrap()).unwrap(); + let verbose = local_args.is_present("verbose"); + + let max_parallelism = local_args.value_of("parallel").unwrap().parse::().unwrap(); + let max_parallelism = match max_parallelism { + 0 => None, + _ => Some(max_parallelism), + }; + + let mut task_list: Vec = Vec::new(); + for (name, profile) in profiles.iter() { + let task = DeploymentTask::new( + name.clone(), + deployment_info.get(name).unwrap().clone(), + profile.clone(), + goal, + ); + task_list.push(task); + } + + println!("Applying configurations..."); + + deploy(task_list, max_parallelism, !verbose).await; +} diff --git a/src/command/build.rs b/src/command/build.rs new file mode 100644 index 0000000..b75b499 --- /dev/null +++ b/src/command/build.rs @@ -0,0 +1,47 @@ +use clap::{Arg, App, SubCommand, ArgMatches}; + +use crate::nix::Hive; +use crate::util; + +pub fn subcommand() -> App<'static, 'static> { + let command = SubCommand::with_name("build") + .about("Build the configuration") + .arg(Arg::with_name("verbose") + .short("v") + .long("verbose") + .help("Be verbose") + .long_help("Deactivates the progress spinner and prints every line of output.") + .takes_value(false)); + + util::register_common_args(command) +} + +pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) { + let hive = Hive::from_config_arg(local_args).unwrap(); + + println!("Enumerating nodes..."); + let deployment_info = hive.deployment_info().await.unwrap(); + let all_nodes: Vec = deployment_info.keys().cloned().collect(); + + let selected_nodes = match local_args.value_of("on") { + Some(filter) => { + util::filter_nodes(&all_nodes, filter) + } + None => all_nodes.clone(), + }; + + if selected_nodes.len() == 0 { + println!("No hosts matched. Exiting..."); + return; + } + + if selected_nodes.len() == all_nodes.len() { + println!("Building all node configurations..."); + } else { + println!("Selected {} out of {} hosts. Building node configurations...", selected_nodes.len(), deployment_info.len()); + } + + hive.build_selected(selected_nodes).await.unwrap(); + + println!("Success!"); +} diff --git a/src/command/mod.rs b/src/command/mod.rs new file mode 100644 index 0000000..4c05a5b --- /dev/null +++ b/src/command/mod.rs @@ -0,0 +1,2 @@ +pub mod build; +pub mod apply; diff --git a/src/deployment.rs b/src/deployment.rs new file mode 100644 index 0000000..878e432 --- /dev/null +++ b/src/deployment.rs @@ -0,0 +1,118 @@ +use std::cmp::min; +use std::sync::{Arc, Mutex}; + +use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget}; +use futures::future::join_all; + +use crate::nix::{DeploymentTask, DeploymentResult}; +use crate::progress::get_spinner_styles; + +/// User-facing deploy routine +pub async fn deploy(tasks: Vec>, max_parallelism: Option, progress_bar: bool) { + let parallelism = match max_parallelism { + Some(limit) => { + min(limit, tasks.len()) + } + None => { + tasks.len() + } + }; + + let node_name_alignment = tasks.iter().map(|task| task.name().len()).max().unwrap(); + + let multi = Arc::new(MultiProgress::new()); + let root_bar = Arc::new(multi.add(ProgressBar::new(tasks.len() as u64))); + multi.set_draw_target(ProgressDrawTarget::stderr_nohz()); + + { + let (spinner_style, _) = get_spinner_styles(node_name_alignment); + root_bar.set_message("Running..."); + root_bar.set_style(spinner_style); + root_bar.inc(0); + } + + let tasks = Arc::new(Mutex::new(tasks)); + let result_list: Arc>> = Arc::new(Mutex::new(Vec::new())); + + let mut futures = Vec::new(); + + for _ in 0..parallelism { + let tasks = tasks.clone(); + let result_list = result_list.clone(); + let multi = multi.clone(); + let (spinner_style, failing_spinner_style) = get_spinner_styles(node_name_alignment); + + let root_bar = root_bar.clone(); + + let future = tokio::spawn(async move { + // Perform tasks until there's none + loop { + let (task, remaining) = { + let mut tasks = tasks.lock().unwrap(); + let task = tasks.pop(); + let remaining = tasks.len(); + (task, remaining) + }; + + if task.is_none() { + // We are donzo! + return; + } + + let mut task = task.unwrap(); + + let bar = multi.add(ProgressBar::new(100)); + bar.set_style(spinner_style.clone()); + bar.set_prefix(task.name()); + bar.set_message("Starting..."); + bar.inc(0); + + if progress_bar { + task.set_progress_bar(&bar); + task.set_failing_spinner_style(failing_spinner_style.clone()); + } + + match task.execute().await { + Ok(result) => { + if !result.success() { + bar.abandon_with_message("Failed") + } else { + bar.finish_with_message(task.goal().success_str().unwrap()); + } + bar.inc(0); + let mut result_list = result_list.lock().unwrap(); + result_list.push(result); + }, + Err(e) => { + println!("An error occurred while pushing to {}: {:?}", task.name(), e); + bar.set_style(failing_spinner_style.clone()); + bar.abandon_with_message("Internal error"); + }, + } + + root_bar.inc(1); + + if remaining == 0 { + root_bar.finish_with_message("Finished"); + } + } + }); + + futures.push(future); + } + + if progress_bar { + futures.push(tokio::task::spawn_blocking(move || { + multi.join().unwrap(); + })); + } + + join_all(futures).await; + + let result_list = result_list.lock().unwrap(); + for result in result_list.iter() { + if !result.success() { + println!("{}", result); + } + } +} diff --git a/src/eval.nix b/src/eval.nix new file mode 100644 index 0000000..4ee9f19 --- /dev/null +++ b/src/eval.nix @@ -0,0 +1,131 @@ +{ rawHive }: +with builtins; +let + defaultHive = { + # Will be set in defaultHiveMeta + network = {}; + + # Like in NixOps, there is a special host named `defaults` + # containing configurations that will be applied to all + # hosts. + defaults = {}; + }; + + defaultHiveMeta = { + name = "hive"; + description = "A Colmena Hive"; + + # Can be a path, a lambda, or an initialized Nixpkgs attrset + nixpkgs = ; + }; + + # Colmena-specific options + # + # Largely compatible with NixOps/Morph. + deploymentOptions = { name, lib, ... }: + let + types = lib.types; + in { + options = { + deployment = { + targetHost = lib.mkOption { + description = '' + The target SSH node for deployment. + + If not specified, the node's attribute name will be used. + ''; + type = types.str; + default = name; + }; + targetUser = lib.mkOption { + description = '' + The user to use to log into the remote node. + ''; + type = types.str; + default = "root"; + }; + tags = lib.mkOption { + description = '' + A list of tags for the node. + + Can be used to select a group of nodes for deployment. + ''; + type = types.listOf types.str; + default = []; + }; + }; + }; + }; + + hiveMeta = { + network = defaultHiveMeta // (if rawHive ? network then rawHive.network else {}); + }; + hive = defaultHive // rawHive // hiveMeta; + + pkgs = let + pkgConf = hive.network.nixpkgs; + in if typeOf pkgConf == "path" then + import pkgConf {} + else if typeOf pkgConf == "lambda" then + pkgConf {} + else if typeOf pkgConf == "set" then + pkgConf + else throw '' + network.nixpkgs must be one of: + + - A path to Nixpkgs (e.g., ) + - A Nixpkgs lambda (e.g., import ) + - A Nixpkgs attribute set + ''; + + lib = pkgs.lib; + reservedNames = [ "defaults" "network" "meta" ]; + + evalNode = name: config: let + evalConfig = import (pkgs.path + "/nixos/lib/eval-config.nix"); + in evalConfig { + system = currentSystem; + modules = [ + deploymentOptions + hive.defaults + config + ] ++ (import (pkgs.path + "/nixos/modules/module-list.nix")); + specialArgs = { + inherit name nodes; + modulesPath = pkgs.path + "/nixos/modules"; + }; + }; + + nodeNames = filter (name: ! elem name reservedNames) (attrNames hive); + + # Exported attributes + # + # Functions are intended to be called with `nix-instantiate --eval --json` + + nodes = listToAttrs (map (name: { + inherit name; + value = evalNode name hive.${name}; + }) nodeNames); + + deploymentInfoJson = toJSON (lib.attrsets.mapAttrs (name: eval: eval.config.deployment) nodes); + + toplevel = lib.attrsets.mapAttrs (name: eval: eval.config.system.build.toplevel) nodes; + + buildAll = buildSelected { + names = nodeNames; + }; + buildSelected = { names ? null }: let + # Change in the order of the names should not cause a derivation to be created + selected = lib.attrsets.filterAttrs (name: _: elem name names) toplevel; + in derivation rec { + name = "colmena-${hive.network.name}"; + system = currentSystem; + json = toJSON (lib.attrsets.mapAttrs (k: v: toString v) selected); + builder = pkgs.writeScript "${name}.sh" '' + #!/bin/sh + echo "$json" > $out + ''; + }; +in { + inherit nodes deploymentInfoJson toplevel buildAll buildSelected; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6ebe0f9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,31 @@ +use clap::{App, AppSettings}; + +mod nix; +mod command; +mod progress; +mod deployment; +mod util; + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> Result<(), Box>{ + let matches = App::new("Colmena") + .version("0.1.0") + .author("Zhaofeng Li ") + .about("NixOS deployment tool") + .global_setting(AppSettings::ColoredHelp) + .setting(AppSettings::ArgRequiredElseHelp) + .subcommand(command::apply::subcommand()) + .subcommand(command::build::subcommand()) + .get_matches(); + + if let Some(sub_matches) = matches.subcommand_matches("build") { + command::build::run(&matches, &sub_matches).await; + return Ok(()); + } + if let Some(sub_matches) = matches.subcommand_matches("apply") { + command::apply::run(&matches, &sub_matches).await; + return Ok(()); + } + + Ok(()) +} diff --git a/src/nix.rs b/src/nix.rs new file mode 100644 index 0000000..2f86aad --- /dev/null +++ b/src/nix.rs @@ -0,0 +1,583 @@ +//! A Colmena Hive. + +use std::path::{Path, PathBuf}; +use std::convert::AsRef; +use std::io::Write; +use std::process::{ExitStatus, Stdio}; +use std::collections::HashMap; +use std::fs; +use std::fmt; + +use console::style; +use async_trait::async_trait; +use clap::ArgMatches; +use indicatif::{ProgressBar, ProgressStyle}; +use serde::de::DeserializeOwned; +use serde::{Serialize, Deserialize}; +use snafu::Snafu; +use tempfile::{NamedTempFile, TempPath}; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Command; + +const HIVE_EVAL: &'static [u8] = include_bytes!("eval.nix"); + +#[derive(Debug, Clone, Deserialize)] +pub struct DeploymentInfo { + #[serde(rename = "targetHost")] + target_host: String, + + #[serde(rename = "targetUser")] + target_user: String, + tags: Vec, +} + +#[derive(Debug)] +pub struct DeploymentTask<'task> { + /// Name of the target. + name: String, + + /// The target to deploy to. + target: DeploymentInfo, + + /// Nix store path to the system profile to deploy. + profile: PathBuf, + + /// The goal of this deployment. + goal: DeploymentGoal, + + /// A ProgressBar to show the deployment progress to the user. + progress: Option<&'task ProgressBar>, + + /// The ProgressStyle to set when the deployment is failing. + failing_spinner_style: Option, +} + +#[derive(Debug, Copy, Clone)] +pub enum DeploymentGoal { + /// Push the closures only. + Push, + + /// Make the configuration the boot default and activate now. + Switch, + + /// Make the configuration the boot default. + Boot, + + /// Activate the configuration, but don't make it the boot default. + Test, + + /// Show what would be done if this configuration were activated. + DryActivate, +} + +impl DeploymentGoal { + pub fn from_str(s: &str) -> Option { + match s { + "push" => Some(Self::Push), + "switch" => Some(Self::Switch), + "boot" => Some(Self::Boot), + "test" => Some(Self::Test), + "dry-activate" => Some(Self::DryActivate), + _ => None, + } + } + + pub fn as_str(&self) -> Option<&'static str> { + use DeploymentGoal::*; + match self { + Push => None, + Switch => Some("switch"), + Boot => Some("boot"), + Test => Some("test"), + DryActivate => Some("dry-activate"), + } + } + + pub fn success_str(&self) -> Option<&'static str> { + use DeploymentGoal::*; + match self { + Push => Some("Pushed"), + Switch => Some("Activation successful"), + Boot => Some("Will be activated next boot"), + Test => Some("Activation successful (test)"), + DryActivate => Some("Dry activation successful"), + } + } +} + +/// Results of a DeploymentTask to show to the user. +pub struct DeploymentResult { + name: String, + push_output: Option, + push_successful: Option, + activate_output: Option, + activate_successful: Option, +} + +impl DeploymentResult { + fn new(name: String) -> Self { + Self { + name, + push_output: None, + push_successful: None, + activate_output: None, + activate_successful: None, + } + } + + /// Whether the deployment was successful overall. + pub fn success(&self) -> bool { + if let Some(push_successful) = self.push_successful { + if !push_successful { + return false; + } + } + + if let Some(activate_successful) = self.activate_successful { + if !activate_successful { + return false; + } + } + + true + } + + fn dump_log(f: &mut fmt::Formatter<'_>, output: Option<&String>) -> fmt::Result { + if let Some(output) = output { + writeln!(f, "Last 10 lines of log:")?; + writeln!(f, "~~~~~~~~~~")?; + let lines: Vec<&str> = output.split("\n").collect(); + + let start = if lines.len() < 10 { + 0 + } else { + lines.len() - 10 + }; + + for i in start..lines.len() { + writeln!(f, "{}", lines[i])?; + } + writeln!(f, "~~~~~~~~~~")?; + } + + writeln!(f) + } +} + +impl fmt::Display for DeploymentResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(push_successful) = self.push_successful { + if push_successful { + writeln!(f, "Deployment on node {} succeeded.", self.name)?; + } else { + write!(f, "Deployment on node {} failed. ", self.name)?; + Self::dump_log(f, self.push_output.as_ref())?; + } + } + if let Some(activate_successful) = self.activate_successful { + if activate_successful { + writeln!(f, "Activation on node {} succeeded.", self.name)?; + } else { + write!(f, "Activation on node {} failed.", self.name)?; + Self::dump_log(f, self.activate_output.as_ref())?; + } + } + Ok(()) + } +} + +#[derive(Debug, Snafu)] +pub enum NixError { + #[snafu(display("I/O Error: {}", error))] + IoError { error: std::io::Error }, + + #[snafu(display("Nix returned invalid response: {}", output))] + BadOutput { output: String }, + + #[snafu(display("Nix exited with error code: {}", exit_code))] + NixFailure { exit_code: i32 }, + + #[snafu(display("Nix was interrupted"))] + NixKilled, + + #[snafu(display("Nix Error: {}", message))] + Unknown { message: String }, +} + +pub type NixResult = Result; + +pub struct Hive { + hive: PathBuf, + eval_nix: TempPath, +} + +struct NixInstantiate<'hive> { + eval_nix: &'hive Path, + hive: &'hive Path, + expression: String, +} + +impl<'hive> NixInstantiate<'hive> { + fn new(eval_nix: &'hive Path, hive: &'hive Path, expression: String) -> Self { + Self { + eval_nix, + expression, + hive, + } + } + + fn instantiate(self) -> Command { + // FIXME: unwrap + // Technically filenames can be arbitrary byte strings (OsStr), + // but Nix may not like it... + + let mut command = Command::new("nix-instantiate"); + command + .arg("-E") + .arg(format!( + "with builtins; let eval = import {}; hive = eval {{ rawHive = import {}; }}; in {}", + self.eval_nix.to_str().unwrap(), + self.hive.to_str().unwrap(), + self.expression, + )); + command + } + + fn eval(self) -> Command { + let mut command = self.instantiate(); + command.arg("--eval").arg("--json"); + command + } +} + +#[async_trait] +trait NixCommand { + async fn passthrough(&mut self) -> NixResult<()>; + async fn capture_output(&mut self) -> NixResult; + async fn capture_json(&mut self) -> NixResult where T: DeserializeOwned; + async fn capture_store_path(&mut self) -> NixResult; +} + +#[async_trait] +impl NixCommand for Command { + /// Runs the command with stdout and stderr passed through to the user. + async fn passthrough(&mut self) -> NixResult<()> { + let exit = self + .spawn() + .map_err(map_io_error)? + .wait() + .await + .map_err(map_io_error)?; + + if exit.success() { + Ok(()) + } else { + Err(match exit.code() { + Some(exit_code) => NixError::NixFailure { exit_code }, + None => NixError::NixKilled, + }) + } + } + + /// Captures output as a String. + async fn capture_output(&mut self) -> NixResult { + // We want the user to see the raw errors + let output = self + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .map_err(map_io_error)? + .wait_with_output() + .await + .map_err(map_io_error)?; + + if output.status.success() { + // FIXME: unwrap + Ok(String::from_utf8(output.stdout).unwrap()) + } else { + Err(match output.status.code() { + Some(exit_code) => NixError::NixFailure { exit_code }, + None => NixError::NixKilled, + }) + } + } + + /// Captures deserialized output from JSON. + async fn capture_json(&mut self) -> NixResult where T: DeserializeOwned { + let output = self.capture_output().await?; + serde_json::from_str(&output).map_err(|_| NixError::BadOutput { + output: output.clone() + }) + } + + /// Captures a single store path. + async fn capture_store_path(&mut self) -> NixResult { + let output = self.capture_output().await?; + Ok(StorePath(output.trim_end().into())) + } +} + +/// A Nix store path. +#[derive(Debug, Serialize, Deserialize)] +struct StorePath(PathBuf); + +impl StorePath { + /// Builds the store path. + pub async fn realise(&self) -> NixResult> { + Command::new("nix-store") + .arg("--realise") + .arg(&self.0) + .capture_output() + .await + .map(|paths| { + paths.lines().map(|p| p.into()).collect() + }) + } +} + +/// A serialized Nix expression. +/// +/// Very hacky and involves an Import From Derivation, so should be +/// avoided as much as possible. But I suppose it's more robust than attempting +/// to generate Nix expressions directly or escaping a JSON string to strip +/// off Nix interpolation. +struct SerializedNixExpresssion { + json_file: TempPath, +} + +impl SerializedNixExpresssion { + pub fn new<'de, T>(data: T) -> NixResult where T: Serialize { + let mut tmp = NamedTempFile::new().map_err(map_io_error)?; + let json = serde_json::to_vec(&data).expect("Could not serialize data"); + tmp.write_all(&json).map_err(map_io_error)?; + + Ok(Self { + json_file: tmp.into_temp_path(), + }) + } + + pub fn expression(&self) -> String { + format!("(builtins.fromJSON (builtins.readFile {}))", self.json_file.to_str().unwrap()) + } +} + +impl Hive { + pub fn new>(hive: P) -> NixResult { + let mut eval_nix = NamedTempFile::new().map_err(map_io_error)?; + eval_nix.write_all(HIVE_EVAL).map_err(map_io_error)?; + + Ok(Self { + hive: hive.as_ref().to_owned(), + eval_nix: eval_nix.into_temp_path(), + }) + } + + pub fn from_config_arg(args: &ArgMatches<'_>) -> NixResult { + let path = args.value_of("config").expect("The config arg should exist").to_owned(); + let path = canonicalize_path(path); + + Self::new(path) + } + + /// Retrieve a list of nodes in the hive + pub async fn nodes(&self) -> NixResult> { + self.nix_instantiate("attrNames hive.nodes").eval() + .capture_json().await + } + + /// Retrieve deployment info for all nodes + pub async fn deployment_info(&self) -> NixResult> { + // FIXME: Really ugly :( + let s: String = self.nix_instantiate("hive.deploymentInfoJson").eval() + .capture_json().await?; + + Ok(serde_json::from_str(&s).unwrap()) + } + + /// Builds selected nodes + pub async fn build_selected(&self, nodes: Vec) -> NixResult> { + let nodes_expr = SerializedNixExpresssion::new(&nodes)?; + let expr = format!("hive.buildSelected {{ names = {}; }}", nodes_expr.expression()); + + self.build_common(&expr).await + } + + /// Builds all node configurations + pub async fn build_all(&self) -> NixResult> { + self.build_common("hive.buildAll").await + } + + /// Builds node configurations + /// + /// Expects the resulting store path to point to a JSON file containing + /// a map of node name -> store path. + async fn build_common(&self, expression: &str) -> NixResult> { + let build: StorePath = self.nix_instantiate(expression).instantiate() + .capture_store_path().await?; + + let realization = build.realise().await?; + assert!(realization.len() == 1); + + let json = fs::read_to_string(&realization[0]).map_err(map_io_error)?; + let result_map: HashMap = serde_json::from_str(&json) + .expect("Bad result from our own build routine"); + + Ok(result_map) + } + + fn nix_instantiate(&self, expression: &str) -> NixInstantiate { + NixInstantiate::new(&self.eval_nix, &self.hive, expression.to_owned()) + } +} + +impl<'task> DeploymentTask<'task> { + pub fn new(name: String, target: DeploymentInfo, profile: PathBuf, goal: DeploymentGoal) -> Self { + Self { + name, + target, + profile, + goal, + progress: None, + failing_spinner_style: None, + } + } + + pub fn name(&self) -> &str { &self.name } + pub fn goal(&self) -> DeploymentGoal { self.goal } + + /// Set the progress bar used during deployment. + pub fn set_progress_bar(&mut self, progress: &'task ProgressBar) { + self.progress = Some(progress); + } + + /// Set a spinner style to switch to when the deployment is failing. + pub fn set_failing_spinner_style(&mut self, style: ProgressStyle) { + self.failing_spinner_style = Some(style); + } + + pub async fn execute(&mut self) -> NixResult { + match self.goal { + DeploymentGoal::Push => { + self.push().await + } + _ => { + self.push_and_activate().await + } + } + } + + async fn push(&mut self) -> NixResult { + let mut result = DeploymentResult::new(self.name.clone()); + + // Issue of interest: + // https://github.com/NixOS/nix/issues?q=ipv6 + let target = format!("{}@{}", self.target.target_user, self.target.target_host); + let mut command = Command::new("nix-copy-closure"); + command + .arg("--to") + .arg("--gzip") + .arg("--include-outputs") + .arg("--use-substitutes") + .arg(&target) + .arg(&self.profile); + + let (exit, output) = self.run_command(&mut command, false).await?; + + if let Some(progress) = self.progress.as_mut() { + if !exit.success() { + if self.failing_spinner_style.is_some() { + let style = self.failing_spinner_style.as_ref().unwrap().clone(); + progress.set_style(style); + } + } + } + + result.push_successful = Some(exit.success()); + result.push_output = output; + + Ok(result) + } + + async fn push_and_activate(&mut self) -> NixResult { + let mut result = self.push().await?; + + if !result.success() { + // Don't go any further + return Ok(result); + } + + let target = format!("{}@{}", self.target.target_user, self.target.target_host); + let activation_command = format!("{}/bin/switch-to-configuration", self.profile.to_str().unwrap()); + let mut command = Command::new("ssh"); + command + .arg("-o") + .arg("StrictHostKeyChecking=accept-new") + .arg(&target) + .arg("--") + .arg(activation_command) + .arg(self.goal.as_str().unwrap()); + + let (exit, output) = self.run_command(&mut command, true).await?; + + if let Some(progress) = self.progress.as_mut() { + if !exit.success() { + if self.failing_spinner_style.is_some() { + let style = self.failing_spinner_style.as_ref().unwrap().clone(); + progress.set_style(style); + } + } + } + + result.activate_successful = Some(exit.success()); + result.activate_output = output; + + Ok(result) + } + + async fn run_command(&mut self, command: &mut Command, capture_stdout: bool) -> NixResult<(ExitStatus, Option)> { + command.stdin(Stdio::null()); + command.stderr(Stdio::piped()); + + if capture_stdout { + command.stdout(Stdio::piped()); + } + + let mut child = command.spawn().map_err(map_io_error)?; + + let mut stderr = BufReader::new(child.stderr.as_mut().unwrap()); + let mut output = String::new(); + + loop { + let mut line = String::new(); + let len = stderr.read_line(&mut line).await.unwrap(); + + if len == 0 { + break; + } + + let trimmed = line.trim_end(); + if let Some(progress) = self.progress.as_mut() { + progress.set_message(trimmed); + progress.inc(0); + } else { + println!("{} | {}", style(&self.name).cyan(), trimmed); + } + output += &line; + } + let exit = child.wait().await.map_err(map_io_error)?; + Ok((exit, Some(output))) + } +} + +fn map_io_error(error: std::io::Error) -> NixError { + NixError::IoError { error } +} + +fn canonicalize_path(path: String) -> PathBuf { + if !path.starts_with("/") { + format!("./{}", path).into() + } else { + path.into() + } +} + diff --git a/src/progress.rs b/src/progress.rs new file mode 100644 index 0000000..8688b71 --- /dev/null +++ b/src/progress.rs @@ -0,0 +1,15 @@ +use indicatif::ProgressStyle; + +pub fn get_spinner_styles(node_name_alignment: usize) -> (ProgressStyle, ProgressStyle) { + let template = format!("{{prefix:>{}.bold.dim}} {{spinner}} {{elapsed}} {{wide_msg}}", node_name_alignment); + + ( + ProgressStyle::default_spinner() + .tick_chars("🕛🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚✅") + .template(&template), + + ProgressStyle::default_spinner() + .tick_chars("❌❌") + .template(&template), + ) +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..bc14501 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,40 @@ +use clap::{Arg, App}; +use glob::Pattern as GlobPattern; + +pub fn filter_nodes(nodes: &Vec, filter: &str) -> Vec { + let filters: Vec = filter.split(",").map(|pattern| GlobPattern::new(pattern).unwrap()).collect(); + + if filters.len() > 0 { + nodes.iter().filter(|name| { + for filter in filters.iter() { + if filter.matches(name) { + return true; + } + } + + false + }).cloned().collect() + } else { + nodes.to_owned() + } +} + +pub fn register_common_args<'a, 'b>(command: App<'a, 'b>) -> App<'a, 'b> { + command + .arg(Arg::with_name("config") + .short("f") + .long("config") + .help("Path to a Hive expression") + .default_value("hive.nix") + .required(true)) + .arg(Arg::with_name("on") + .long("on") + .help("Select a list of machines") + .long_help(r#"The list is comma-separated and globs are supported. +Valid examples: + +- host1,host2,host3 +- edge-* +- edge-*,core-*"#) + .takes_value(true)) +}