Initial commit

This commit is contained in:
Zhaofeng Li 2020-12-15 20:21:26 -08:00
commit e092ba5bb1
14 changed files with 1869 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

760
Cargo.lock generated Normal file
View file

@ -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"

21
Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "colmena"
version = "0.1.0"
authors = ["Zhaofeng Li <hello@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"] }

21
LICENSE Normal file
View file

@ -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.

10
README.md Normal file
View file

@ -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.

89
src/command/apply.rs Normal file
View file

@ -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::<usize>() {
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<String> = 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::<usize>().unwrap();
let max_parallelism = match max_parallelism {
0 => None,
_ => Some(max_parallelism),
};
let mut task_list: Vec<DeploymentTask> = 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;
}

47
src/command/build.rs Normal file
View file

@ -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<String> = 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!");
}

2
src/command/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod build;
pub mod apply;

118
src/deployment.rs Normal file
View file

@ -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<DeploymentTask<'static>>, max_parallelism: Option<usize>, 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<Mutex<Vec<DeploymentResult>>> = 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);
}
}
}

131
src/eval.nix Normal file
View file

@ -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 = <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., <nixpkgs>)
- A Nixpkgs lambda (e.g., import <nixpkgs>)
- 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;
}

31
src/main.rs Normal file
View file

@ -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<dyn std::error::Error>>{
let matches = App::new("Colmena")
.version("0.1.0")
.author("Zhaofeng Li <hello@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(())
}

583
src/nix.rs Normal file
View file

@ -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<String>,
}
#[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<ProgressStyle>,
}
#[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<Self> {
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<String>,
push_successful: Option<bool>,
activate_output: Option<String>,
activate_successful: Option<bool>,
}
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<T> = Result<T, NixError>;
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<String>;
async fn capture_json<T>(&mut self) -> NixResult<T> where T: DeserializeOwned;
async fn capture_store_path(&mut self) -> NixResult<StorePath>;
}
#[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<String> {
// 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<T>(&mut self) -> NixResult<T> 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<StorePath> {
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<Vec<PathBuf>> {
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<Self> 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<P: AsRef<Path>>(hive: P) -> NixResult<Self> {
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<Self> {
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<Vec<String>> {
self.nix_instantiate("attrNames hive.nodes").eval()
.capture_json().await
}
/// Retrieve deployment info for all nodes
pub async fn deployment_info(&self) -> NixResult<HashMap<String, DeploymentInfo>> {
// 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<String>) -> NixResult<HashMap<String, PathBuf>> {
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<HashMap<String, PathBuf>> {
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<HashMap<String, PathBuf>> {
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<String, PathBuf> = 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<DeploymentResult> {
match self.goal {
DeploymentGoal::Push => {
self.push().await
}
_ => {
self.push_and_activate().await
}
}
}
async fn push(&mut self) -> NixResult<DeploymentResult> {
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<DeploymentResult> {
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<String>)> {
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()
}
}

15
src/progress.rs Normal file
View file

@ -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),
)
}

40
src/util.rs Normal file
View file

@ -0,0 +1,40 @@
use clap::{Arg, App};
use glob::Pattern as GlobPattern;
pub fn filter_nodes(nodes: &Vec<String>, filter: &str) -> Vec<String> {
let filters: Vec<GlobPattern> = 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))
}