difftreelog
feat system build
in: trunk
24 files changed
.gitignorediffbeforeafterboth--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+target
+result
+example
Cargo.lockdiffbeforeafterboth--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,709 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "aho-corasick"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b476ce7103678b0c6d3d395dbbae31d48ff910bd28be979ba5d48c6351131d0d"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7"
+
+[[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 = "base-x"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
+name = "bumpalo"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "clap"
+version = "3.0.0-beta.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "indexmap",
+ "lazy_static",
+ "os_str_bytes",
+ "strsim",
+ "termcolor",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.0.0-beta.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "const_fn"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab"
+
+[[package]]
+name = "discard"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
+
+[[package]]
+name = "env_logger"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54532e3223c5af90a6a757c90b5c5521564b07e5e7a958681bcd2afad421cdcd"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "fleet"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "env_logger",
+ "lockfile",
+ "log",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "time",
+ "toml",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
+
+[[package]]
+name = "heck"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "humantime"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c1ad908cc71012b7bea4d0c53ba96a8cba9962f048fa68d143376143d863b7a"
+
+[[package]]
+name = "indexmap"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[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.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743"
+
+[[package]]
+name = "lockfile"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e9b01c4735c76fec1c390661ac8794722f0af0b5eb742500308f94b2caae40f"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "log"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
+
+[[package]]
+name = "os_str_bytes"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ac6fe3538f701e339953a3ebbe4f39941aababa8a3f6964635b24ab526daeac"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
+[[package]]
+name = "proc-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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8963b85b8ce3074fecffde43b4b0dded83ce2f367dc8d363afc56679f3ee820b"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+ "thread_local",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cab7a364d15cde1e505267766a2d3c4e22a843e1a601f0fa7564c0f82ced11c"
+
+[[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 = "rustc_version"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+
+[[package]]
+name = "serde"
+version = "1.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.59"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
+
+[[package]]
+name = "standback"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4e0831040d2cf2bdfd51b844be71885783d489898a192f254ae25d57cce725c"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "stdweb"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
+dependencies = [
+ "discard",
+ "rustc_version",
+ "stdweb-derive",
+ "stdweb-internal-macros",
+ "stdweb-internal-runtime",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "stdweb-derive"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_derive",
+ "syn",
+]
+
+[[package]]
+name = "stdweb-internal-macros"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
+dependencies = [
+ "base-x",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha1",
+ "syn",
+]
+
+[[package]]
+name = "stdweb-internal-runtime"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e03e57e4fcbfe7749842d53e24ccb9aa12b7252dbe5e91d2acad31834c8b8fdd"
+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",
+ "libc",
+ "rand",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "time"
+version = "0.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55b7151c9065e80917fbf285d9a5d1432f60db41d170ccafc749a136b41a93af"
+dependencies = [
+ "const_fn",
+ "libc",
+ "serde",
+ "standback",
+ "stdweb",
+ "time-macros",
+ "version_check",
+ "winapi",
+]
+
+[[package]]
+name = "time-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1"
+dependencies = [
+ "proc-macro-hack",
+ "time-macros-impl",
+]
+
+[[package]]
+name = "time-macros-impl"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa"
+dependencies = [
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "standback",
+ "syn",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
+
+[[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 = "version_check"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307"
+
+[[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"
Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "fleet"
+description = "NixOS configuration management"
+version = "0.1.0"
+authors = ["Yaroslav Bolyukin <iam@lach.pw>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.34"
+clap = { version = "3.0.0-beta.2", features = ["derive", "suggestions", "color"] }
+log = "0.4.11"
+env_logger = "0.8.1"
+
+serde = { version = "1.0.117", features = ["derive"] }
+serde_json = "1.0.59"
+
+time = { version = "0.2.22", features = ["serde"] }
+
+lockfile = "0.2.2"
+toml = "0.5"
+tempfile = "3.1.0"
README.mddiffbeforeafterboth--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+# fleet
+
+Early prototype stage
+
+## Advantages over existing configuration systems (NixOps/Morph)
+
+- Modules can configure multiple hosts at once (I.e for wireguard/kubernetes installation)
+- Secrets can be securely stored in Git (No one except target hosts can decrypt them)
flake.lockdiffbeforeafterbothno changes
flake.nixdiffbeforeafterboth--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,10 @@
+{
+ description = "NixOS configuration management";
+
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs";
+ };
+ outputs = { self, nixpkgs }: with nixpkgs.lib; rec {
+ lib = import ./lib;
+ };
+}
lib/default.nixdiffbeforeafterboth--- /dev/null
+++ b/lib/default.nix
@@ -0,0 +1,45 @@
+{
+ fleetConfiguration = { common ? { modules = []; }, hosts, nixpkgs }@args:
+ rec {
+ root = nixpkgs.lib.evalModules {
+ modules = [
+ (
+ { ... }: {
+ config = {
+ inherit hosts;
+ # Secret data is available only via fleet build-systems
+ secrets = if builtins?getEnv then
+ let
+ stringData = builtins.getEnv "SECRET_DATA";
+ in
+ if stringData != "" then (builtins.fromJSON stringData) else {}
+ else {};
+ };
+
+ }
+ )
+ ] ++ common.modules ++ import ./modules/modules.nix {
+ pkgs = nixpkgs;
+ lib = nixpkgs.lib;
+ };
+
+ specialArgs = {
+ fleet = import ./lib/fleetLib.nix {
+ inherit nixpkgs hosts;
+ };
+ };
+ };
+ configuredHosts = root.config.hosts;
+ configuredSecrets = root.config.secrets;
+ configuredSystems = listToAttrs (
+ map (
+ name: {
+ inherit name; value = nixpkgs.lib.nixosSystem {
+ system = configuredHosts.${name}.system;
+ modules = configuredHosts.${name}.modules;
+ };
+ }
+ ) (builtins.attrNames hosts)
+ ); #nixpkgs.lib.nixosSystem {}
+ };
+}
lib/fleetLib.nixdiffbeforeafterboth--- /dev/null
+++ b/lib/fleetLib.nix
@@ -0,0 +1,52 @@
+# Shared functions for fleet configuration, available as `fleet` module argument
+{ nixpkgs, hosts }: with nixpkgs.lib; rec {
+ mkSecret = let
+ system = builtins.currentSystem;
+ pkgs = import nixpkgs { inherit system; };
+ keys = builtins.getEnv "RAGE_KEYS";
+ encryptCmd = "rage ${keys} -a";
+ impuritySource = builtins.getEnv "IMPURITY_SOURCE";
+ in
+ f: let
+ data = f { inherit pkgs encryptCmd; };
+ in
+ builtins.derivation {
+ inherit system;
+ name = "secret";
+
+ builder = "${pkgs.bash}/bin/bash";
+ args = [
+ (
+ pkgs.writeTextFile {
+ name = "./build-${impuritySource}.sh";
+ text = data.script;
+ executable = true;
+ }
+ )
+ ];
+
+ PATH = "${pkgs.coreutils}/bin:${pkgs.rage}/bin${builtins.concatStringsSep "" (builtins.map (n: ":${n}/bin") data.utils)}";
+ };
+ # Modules can't register hosts because of infinite recursion
+ hostNames = attrNames hosts;
+ hostsToAttrs = f: listToAttrs (
+ map (name: { inherit name; value = f name; }) hostNames
+ );
+ hostsCartesian = remove null (
+ unique (
+ crossLists (
+ a: b: if a == b then
+ null
+ else
+ hostsPair a b
+ ) [ hostNames hostNames ]
+ )
+ );
+ hostsPair = this: other: let
+ sorted = sort (a: b: a < b) [ this other ];
+ in
+ {
+ a = elemAt sorted 0;
+ b = elemAt sorted 1;
+ };
+}
modules/modules.nixdiffbeforeafterboth--- /dev/null
+++ b/modules/modules.nix
@@ -0,0 +1,8 @@
+{ pkgs
+, lib
+, check ? true
+}:
+with lib; [
+ ./networking/wireguard
+ ./root.nix
+]
modules/networking/wireguard/default.nixdiffbeforeafterboth--- /dev/null
+++ b/modules/networking/wireguard/default.nix
@@ -0,0 +1,101 @@
+{ config, lib, nixpkgs, fleet, ... }: with lib; with fleet; let
+ cfg = config.networking.wireguard;
+ genWgKey = { owners }: {
+ inherit owners;
+ generator = mkSecret (
+ { pkgs, encryptCmd }: {
+ utils = [ pkgs.wireguard-tools ];
+ script = ''
+ key=$(wg genkey)
+ pub=$(echo $key | wg pubkey)
+
+ mkdir -p $out
+ echo $key | ${encryptCmd} >$out/key
+ echo $pub >$out/pub_key
+ '';
+ }
+ );
+ };
+ genWgPsk = { owners }: {
+ inherit owners;
+ generator = mkSecret (
+ { pkgs, encryptCmd }: {
+ utils = [ pkgs.wireguard-tools ];
+ script = ''
+ key=$(wg genpsk)
+
+ mkdir -p $out
+ echo $key | ${encryptCmd} >$out/key
+ '';
+ }
+ );
+ };
+
+ hostKeys = listToAttrs (
+ map (
+ hostName: {
+ name = "wg-key-${hostName}";
+ value = genWgKey {
+ owners = [ hostName ];
+ };
+ }
+ )
+ hostNames
+ );
+ psks = listToAttrs (
+ map (
+ { a, b }: {
+ name = "wg-psk-${a}-${b}";
+ value = genWgPsk {
+ owners = [ a b ];
+ };
+ }
+ )
+ hostsCartesian
+ );
+in
+{
+ options.networking.wireguard = with types; {
+ enable = mkEnableOption "wireguard";
+ interface = mkOption {
+ type = str;
+ description = "Interface name for wireguard network";
+ default = "fleet";
+ };
+ port = mkOption {
+ type = int;
+ description = "Port, on which wireguard interface should listen";
+ default = 51871;
+ };
+ allowedIPs = mkOption {
+ type = attrsOf (listOf str);
+ description = "Per host allowed ips";
+ };
+ };
+ config = mkIf cfg.enable {
+ secrets =
+ (hostKeys // psks);
+ hosts = hostsToAttrs (
+ hostName: {
+ modules = [
+ {
+ networking.wireguard.enable = true;
+ networking.wireguard.interfaces.fleetwg = {
+ privateKeyFile = "/run/secrets/wg-key-${hostName}";
+ peers = map (
+ peer: let
+ pair = hostsPair hostName peer;
+ in
+ {
+ publicKey = config.secrets."wg-key-${peer}".data.key;
+ presharedKey = "/run/secrets/wg-psk-${pair.a}-${pair.b}";
+ allowedIPs = cfg.allowedIPs.${peer};
+ }
+ ) hostNames;
+ };
+ }
+ ];
+ }
+ );
+ };
+}
modules/networking/wireguard/wgbuilder.shdiffbeforeafterboth--- /dev/null
+++ b/modules/networking/wireguard/wgbuilder.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+key=$($WG genkey)
+pub=$(echo $key | $WG pubkey)
+
+$COREUTILS/bin/mkdir -p $out
+echo $key | $RAGE $recipients >$out/key
+echo $pub >$out/pub_key
modules/root.nixdiffbeforeafterboth--- /dev/null
+++ b/modules/root.nix
@@ -0,0 +1,68 @@
+{ lib, ... }: with lib;
+let
+ secret = with types; {
+ options = {
+ owners = mkOption {
+ type = listOf str;
+ description = ''
+ List of hosts to encrypt secret for
+
+ Secrets would be decrypted and stored to /run/secrets/$\{name} on owners
+ '';
+ };
+ generator = mkOption {
+ type = types.package;
+ description = "Derivation to execute for secret generation";
+ };
+ expireIn = mkOption {
+ type = nullOr int;
+ description = "Time in hours, in which this secret should be regenerated";
+ default = null;
+ };
+ data = mkOption {
+ type = attrsOf anything;
+ description = "Generated secret data, do not set it yourself";
+ default = {};
+ };
+ };
+ };
+ host = with types; {
+ options = {
+ modules = mkOption {
+ type = listOf anything;
+ description = "List of nixos modules";
+ default = [];
+ };
+ network = mkOption {
+ type = submodule {
+ options = {
+ fleetIp = {
+ type = str;
+ description = "Ip which is available to all hosts in fleet";
+ };
+ };
+ };
+ description = "Network definition of host";
+ };
+ system = mkOption {
+ type = str;
+ description = "Type of system";
+ };
+ };
+ };
+in
+{
+ options = with types; {
+ hosts = mkOption {
+ type = attrsOf (submodule host);
+ default = {};
+ description = "Configurations of individual hosts";
+ };
+ secrets = mkOption {
+ type = attrsOf (submodule secret);
+ default = {};
+ description = "Secrets";
+ };
+ };
+ config = {};
+}
rustfmt.tomldiffbeforeafterboth--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1 @@
+hard_tabs = true
src/cmds/build_systems.rsdiffbeforeafterboth--- /dev/null
+++ b/src/cmds/build_systems.rs
@@ -0,0 +1,32 @@
+use crate::{
+ db::{keys::list_hosts, secret::SecretDb, Db, DbData},
+ nix::{NixBuild, NixCopy, HOSTS_ATTRIBUTE, SYSTEMS_ATTRIBUTE},
+};
+use anyhow::Result;
+use clap::Clap;
+use log::info;
+
+#[derive(Clap)]
+pub struct BuildSystems {}
+
+impl BuildSystems {
+ pub fn run(self) -> Result<()> {
+ let db = Db::new(".fleet")?;
+ let hosts = list_hosts()?;
+ let data = SecretDb::open(&db)?.generate_nix_data()?;
+
+ for host in hosts.iter() {
+ info!("Building host {}", host);
+ let path = NixBuild::new(format!(
+ "{}.{}.config.system.build.toplevel",
+ SYSTEMS_ATTRIBUTE, host,
+ ))
+ .env("SECRET_DATA".into(), data.clone())
+ .run()?;
+ info!("{:?}", path.path());
+ NixCopy::new(path.path().to_owned()).to(format!("ssh://root@{}", host))?;
+ std::thread::sleep_ms(9999999)
+ }
+ Ok(())
+ }
+}
src/cmds/fetch_keys.rsdiffbeforeafterboth--- /dev/null
+++ b/src/cmds/fetch_keys.rs
@@ -0,0 +1,44 @@
+use crate::db::{
+ keys::{list_hosts, KeyDb},
+ Db, DbData,
+};
+use anyhow::Result;
+use clap::Clap;
+use log::info;
+
+#[derive(Clap)]
+pub struct FetchKeys {
+ /// Fetch if already exists the following hosts
+ #[clap(short = 'f', long)]
+ force_hosts: Vec<String>,
+ /// If true - remove orphaned keys
+ #[clap(long)]
+ cleanup: bool,
+}
+
+impl FetchKeys {
+ pub fn run(self) -> Result<()> {
+ let db = Db::new(".fleet")?;
+ let hosts = list_hosts()?;
+ let mut keys = KeyDb::open(&db)?;
+ for host in hosts.iter() {
+ let force = self.force_hosts.contains(&host);
+ keys.ensure_key_loaded(host, force)?;
+ }
+ let orphans: Vec<_> = hosts.iter().filter(|h| !keys.has_key(h)).cloned().collect();
+ if !orphans.is_empty() {
+ if self.cleanup {
+ info!("Removed orphan host keys:");
+ } else {
+ info!("Orphan host keys found, run with --cleanup to remove them from db:");
+ }
+ for key in orphans {
+ info!("- {}", key);
+ if self.cleanup {
+ keys.remove_key(&key)
+ }
+ }
+ }
+ Ok(())
+ }
+}
src/cmds/generate_secrets.rsdiffbeforeafterboth--- /dev/null
+++ b/src/cmds/generate_secrets.rs
@@ -0,0 +1,51 @@
+use std::collections::HashSet;
+
+use anyhow::Result;
+use clap::Clap;
+use log::info;
+
+use crate::db::{
+ keys::KeyDb,
+ secret::{list_secrets, SecretDb},
+ Db, DbData,
+};
+
+#[derive(Clap)]
+pub struct GenerateSecrets {
+ /// If set - remove orphaned secrets
+ #[clap(long)]
+ cleanup: bool,
+}
+
+impl GenerateSecrets {
+ pub fn run(self) -> Result<()> {
+ let db = Db::new(".fleet")?;
+ let mut secrets = SecretDb::open(&db)?;
+
+ let defined_secrets = list_secrets()?;
+ for (secret, data) in defined_secrets.iter() {
+ let keys = KeyDb::open(&db)?;
+ secrets.ensure_generated(&keys, &secret, &data)?;
+ }
+ let key_names = defined_secrets
+ .keys()
+ .filter(|s| !secrets.has_secret(s))
+ .cloned()
+ .collect::<HashSet<_>>();
+ if !key_names.is_empty() {
+ if self.cleanup {
+ info!("Removed orphan secrets:");
+ } else {
+ info!("Orphan secrets found, run with --cleanup to remove them from db:");
+ }
+ for key in key_names {
+ info!("- {}", key);
+ if self.cleanup {
+ secrets.remove_secret(&key)
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
src/cmds/mod.rsdiffbeforeafterboth--- /dev/null
+++ b/src/cmds/mod.rs
@@ -0,0 +1,3 @@
+pub mod build_systems;
+pub mod fetch_keys;
+pub mod generate_secrets;
src/command.rsdiffbeforeafterboth--- /dev/null
+++ b/src/command.rs
@@ -0,0 +1,34 @@
+use std::{
+ ffi::OsStr,
+ process::{Command, Stdio},
+};
+
+use anyhow::{Context, Result};
+use serde::Deserialize;
+
+pub struct CommandOutput(pub Vec<u8>);
+impl CommandOutput {
+ pub fn into_json<'d, T: Deserialize<'d>>(&'d self) -> Result<T> {
+ let str = self.as_str().ok();
+ Ok(serde_json::from_slice(&self.0).with_context(|| format!("{:?}", str))?)
+ }
+ pub fn as_str(&self) -> Result<&str> {
+ Ok(std::str::from_utf8(&self.0)?)
+ }
+}
+
+pub fn ssh_command<I, S>(host: impl AsRef<OsStr>, command: I) -> Result<CommandOutput>
+where
+ I: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+{
+ let out = Command::new("ssh")
+ .stderr(Stdio::inherit())
+ .arg(host)
+ .args(command)
+ .output()?;
+ if !out.status.success() {
+ anyhow::bail!("command failed");
+ }
+ Ok(CommandOutput(out.stdout))
+}
src/db/db.rsdiffbeforeafterboth--- /dev/null
+++ b/src/db/db.rs
@@ -0,0 +1,122 @@
+//! Small .toml based readable data store
+
+use anyhow::{Context, Result};
+use serde::{de::DeserializeOwned, Serialize};
+use std::{
+ cell::Cell,
+ collections::HashSet,
+ io::Write,
+ ops::{Deref, DerefMut},
+ path::Path,
+ path::PathBuf,
+ sync::{Arc, Mutex},
+};
+
+struct DbInternal {
+ root: PathBuf,
+ locked_paths: HashSet<PathBuf>,
+ _lockfile: lockfile::Lockfile,
+}
+
+pub trait DbData: DeserializeOwned + Serialize + Default {
+ const DB_NAME: &'static str;
+
+ fn open(db: &Db) -> Result<DbFile<Self>> {
+ db.db::<Self>()
+ }
+}
+
+#[derive(Clone)]
+pub struct Db(Arc<Mutex<DbInternal>>);
+impl Db {
+ pub fn new(root: impl AsRef<Path>) -> Result<Self> {
+ let root: &Path = root.as_ref();
+ std::fs::create_dir_all(&root).context("db root")?;
+ let mut lockfile = root.to_owned();
+ lockfile.push(".lock");
+ let lockfile = lockfile::Lockfile::create(lockfile).context("db lock")?;
+ Ok(Db(Arc::new(Mutex::new(DbInternal {
+ root: root.to_owned(),
+ locked_paths: HashSet::new(),
+ _lockfile: lockfile,
+ }))))
+ }
+
+ pub fn db<T: DbData>(&self) -> Result<DbFile<T>> {
+ let name = T::DB_NAME;
+ assert!(!name.contains("/") && !name.contains("\\"));
+ let mut db = self.0.lock().unwrap();
+ let mut data_path = db.root.clone();
+ data_path.push(format!("{}.toml", name));
+
+ if !db.locked_paths.insert(data_path.clone()) {
+ anyhow::bail!("file is already open");
+ }
+
+ let data = if data_path.exists() {
+ let raw_data = std::fs::read(&data_path).context("reading file")?;
+ toml::from_slice(&raw_data).context("parsing file")?
+ } else {
+ T::default()
+ };
+
+ Ok(DbFile {
+ db: self.clone(),
+ root: db.root.clone(),
+ path: data_path,
+ data,
+ dirty: Cell::new(false),
+ })
+ }
+}
+
+pub struct DbFile<T: DbData> {
+ db: Db,
+ root: PathBuf,
+ path: PathBuf,
+ data: T,
+ dirty: Cell<bool>,
+}
+
+impl<T: DbData> Deref for DbFile<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.data
+ }
+}
+
+impl<T: DbData> DerefMut for DbFile<T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.dirty.set(true);
+ &mut self.data
+ }
+}
+
+impl<T: DbData> DbFile<T> {
+ pub fn write(&self) -> Result<()> {
+ if !self.dirty.get() {
+ return Ok(());
+ }
+ let mut temp = tempfile::Builder::new()
+ .prefix("~")
+ .suffix(".toml")
+ .tempfile_in(&self.root)?;
+ let mut out = String::new();
+ let mut serializer = toml::Serializer::new(&mut out);
+ serializer.pretty_array(true).pretty_string(true);
+ self.data.serialize(&mut serializer)?;
+ temp.write_all(&out.as_bytes())?;
+ temp.persist(&self.path)?;
+ self.dirty.set(false);
+ Ok(())
+ }
+}
+
+impl<T: DbData> Drop for DbFile<T> {
+ fn drop(&mut self) {
+ let mut db = self.db.0.lock().unwrap();
+ self.write().unwrap();
+ db.locked_paths.remove(&self.path);
+ }
+}
src/db/keys.rsdiffbeforeafterboth--- /dev/null
+++ b/src/db/keys.rs
@@ -0,0 +1,62 @@
+use std::collections::BTreeMap;
+
+use anyhow::Result;
+use log::*;
+
+use crate::{
+ command::ssh_command,
+ nix::{NixEval, HOSTS_ATTRIBUTE},
+};
+
+use serde::{Deserialize, Serialize};
+
+use super::db::DbData;
+
+pub fn list_hosts() -> Result<Vec<String>> {
+ Ok(NixEval::new(HOSTS_ATTRIBUTE.into())
+ .apply("builtins.attrNames".into())
+ .run_json()?)
+}
+
+#[derive(Serialize, Deserialize, Default)]
+pub struct KeyDb {
+ host_keys: BTreeMap<String, String>,
+}
+impl DbData for KeyDb {
+ const DB_NAME: &'static str = "keys";
+}
+
+impl KeyDb {
+ pub fn fetch_key(&mut self, host: &str) -> Result<()> {
+ info!("Fetching key for {}", host);
+ let key = ssh_command(host, &["cat", "/etc/ssh/ssh_host_ed25519_key.pub"])?
+ .as_str()?
+ .trim()
+ .to_owned();
+ self.host_keys.insert(host.to_owned(), key);
+ Ok(())
+ }
+
+ pub fn ensure_key_loaded(&mut self, host: &str, force: bool) -> Result<()> {
+ if !self.host_keys.contains_key(host) || force {
+ self.fetch_key(host)?;
+ }
+ Ok(())
+ }
+
+ pub fn get_host_key(&self, host: &str) -> Result<String> {
+ Ok(self
+ .host_keys
+ .get(host)
+ .ok_or_else(|| anyhow::anyhow!("no host key for {}", host))?
+ .to_owned())
+ }
+
+ pub fn has_key(&self, key: &str) -> bool {
+ self.host_keys.contains_key(key)
+ }
+
+ pub fn remove_key(&mut self, host: &str) {
+ self.host_keys.remove(host);
+ }
+}
src/db/mod.rsdiffbeforeafterboth--- /dev/null
+++ b/src/db/mod.rs
@@ -0,0 +1,5 @@
+mod db;
+pub mod keys;
+pub mod secret;
+
+pub use db::*;
src/db/secret.rsdiffbeforeafterboth--- /dev/null
+++ b/src/db/secret.rs
@@ -0,0 +1,211 @@
+use crate::nix::{NixBuild, NixEval, SECRETS_ATTRIBUTE};
+use anyhow::{bail, Result};
+use log::info;
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use std::{
+ collections::{BTreeMap, BTreeSet, HashMap},
+ time::Instant,
+ time::SystemTime,
+};
+use time::{Duration, PrimitiveDateTime};
+
+use super::{db::DbData, keys::KeyDb};
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct SecretListData {
+ pub owners: BTreeSet<String>,
+ #[serde(rename = "expireIn")]
+ renew_in: Option<u64>,
+}
+pub fn list_secrets() -> Result<HashMap<String, SecretListData>> {
+ NixEval::new(format!("{}", SECRETS_ATTRIBUTE))
+ .apply(
+ r#"
+ s: (builtins.mapAttrs (n: {owners, expireIn, ...}: {
+ inherit owners expireIn;
+ }) s)
+ "#
+ .into(),
+ )
+ .run_json()
+}
+
+struct ReadableDate(PrimitiveDateTime);
+impl Serialize for ReadableDate {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(&self.0.to_string())
+ }
+}
+impl<'de> Deserialize<'de> for ReadableDate {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ Ok(Self(
+ PrimitiveDateTime::parse(String::deserialize(deserializer)?, "%F %T").unwrap(),
+ ))
+ }
+}
+impl From<PrimitiveDateTime> for ReadableDate {
+ fn from(d: PrimitiveDateTime) -> Self {
+ Self(d)
+ }
+}
+impl From<ReadableDate> for PrimitiveDateTime {
+ fn from(d: ReadableDate) -> Self {
+ d.0
+ }
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+struct SecretData {
+ created_at: ReadableDate,
+ renew_at: Option<ReadableDate>,
+ owners: BTreeSet<String>,
+
+ public_data: BTreeMap<String, String>,
+ private_files: BTreeMap<String, String>,
+}
+impl SecretData {
+ fn should_renew(&self) -> bool {
+ if let Some(renew_at) = &self.renew_at {
+ let now: PrimitiveDateTime = SystemTime::now().into();
+ renew_at.0 <= now
+ } else {
+ false
+ }
+ }
+ fn is_valid(&self, data: &SecretListData) -> bool {
+ self.owners == data.owners
+ }
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+struct NixDataValue {
+ data: BTreeMap<String, String>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+struct NixData {
+ secrets: BTreeMap<String, NixDataValue>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Default)]
+pub struct SecretDb {
+ secrets: BTreeMap<String, SecretData>,
+}
+impl DbData for SecretDb {
+ const DB_NAME: &'static str = "secrets";
+}
+
+impl SecretDb {
+ // Secrets are generated on machine running fleet command
+ pub fn generate_secret(
+ &mut self,
+ keys: &KeyDb,
+ secret: &str,
+ data: &SecretListData,
+ ) -> Result<()> {
+ let mut rage_keys = String::new();
+ for (i, owner) in data.owners.iter().enumerate() {
+ if i != 0 {
+ rage_keys.push(' ');
+ }
+ rage_keys.push_str("--recipient \"");
+ rage_keys.push_str(&keys.get_host_key(&owner)?);
+ rage_keys.push('"')
+ }
+ let created_at: PrimitiveDateTime = SystemTime::now().into();
+ let renew_at = data
+ .renew_in
+ .map(|hours| created_at + Duration::hours(hours as i64));
+ let built = NixBuild::new(format!("{}.{}.generator", SECRETS_ATTRIBUTE, secret))
+ .env("RAGE_KEYS".into(), rage_keys)
+ .env("IMPURITY_SOURCE".into(), format!("{:?}", Instant::now()))
+ .run()?;
+ let path = built.path().to_owned();
+ let mut secret_data = SecretData {
+ created_at: created_at.into(),
+ renew_at: renew_at.map(|v| v.into()),
+ owners: data.owners.clone(),
+ public_data: BTreeMap::new(),
+ private_files: BTreeMap::new(),
+ };
+ for file in std::fs::read_dir(path)? {
+ let entry = file?;
+ if !entry.file_type()?.is_file() {
+ bail!("Secret generator should produce files, not directories");
+ }
+ let name = entry.file_name();
+ let name = name
+ .to_str()
+ .ok_or(anyhow::anyhow!("file name should be utf-8"))?;
+ let value = String::from_utf8(std::fs::read(entry.path())?)?;
+ if let Some(name) = name.strip_prefix("pub_") {
+ secret_data.public_data.insert(name.into(), value);
+ } else {
+ secret_data.private_files.insert(name.into(), value);
+ }
+ }
+ self.secrets.insert(secret.into(), secret_data);
+ Ok(())
+ }
+ pub fn need_to_generate(&self, secret: &str, data: &SecretListData) -> Result<bool> {
+ let secret = self.secrets.get(secret);
+ if secret.is_none() {
+ return Ok(true);
+ }
+ let secret = secret.unwrap();
+
+ if secret.should_renew() {
+ return Ok(true);
+ }
+
+ if !secret.is_valid(&data) {
+ return Ok(true);
+ }
+
+ Ok(false)
+ }
+ pub fn ensure_generated(
+ &mut self,
+ keys: &KeyDb,
+ secret: &str,
+ data: &SecretListData,
+ ) -> Result<()> {
+ if self.need_to_generate(secret, data)? {
+ info!("Generating secret {}", secret);
+ self.generate_secret(keys, secret, data)?;
+ }
+
+ Ok(())
+ }
+ pub fn generate_nix_data(&self) -> Result<String> {
+ let mut out = BTreeMap::new();
+ for (host, secrets) in &self.secrets {
+ out.insert(
+ host.to_owned(),
+ NixDataValue {
+ data: secrets
+ .public_data
+ .clone()
+ .iter()
+ .map(|(k, v)| (k.to_owned(), v.trim().to_owned()))
+ .collect(),
+ },
+ );
+ }
+ Ok(serde_json::to_string(&out)?)
+ }
+
+ pub fn has_secret(&self, secret: &str) -> bool {
+ self.secrets.contains_key(secret)
+ }
+
+ pub fn remove_secret(&mut self, secret: &str) {
+ self.secrets.remove(secret);
+ }
+}
src/main.rsdiffbeforeafterboth--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,34 @@
+pub mod command;
+
+pub mod cmds;
+pub mod db;
+pub mod nix;
+
+use anyhow::Result;
+use clap::Clap;
+use cmds::{build_systems::BuildSystems, fetch_keys::FetchKeys, generate_secrets::GenerateSecrets};
+
+#[derive(Clap)]
+#[clap(version = "1.0", author = "CertainLach <iam@lach.pw>")]
+enum Opts {
+ /// Fetch encryption (ssh) public keys from remote hosts
+ FetchKeys(FetchKeys),
+ /// Force generation of missing secrets
+ GenerateSecrets(GenerateSecrets),
+ /// Prepare systems for deployments
+ BuildSystems(BuildSystems),
+}
+
+fn main() -> Result<()> {
+ env_logger::Builder::new()
+ .filter_level(log::LevelFilter::Info)
+ .init();
+ let opts = Opts::parse();
+
+ match opts {
+ Opts::FetchKeys(c) => c.run()?,
+ Opts::BuildSystems(c) => c.run()?,
+ Opts::GenerateSecrets(c) => c.run()?,
+ };
+ Ok(())
+}
src/nix.rsdiffbeforeafterboth--- /dev/null
+++ b/src/nix.rs
@@ -0,0 +1,172 @@
+use std::{
+ collections::HashMap,
+ ffi::OsStr,
+ path::PathBuf,
+ process::{Command, Stdio},
+};
+
+use anyhow::Result;
+use serde::de::DeserializeOwned;
+
+use crate::command::CommandOutput;
+
+pub const HOSTS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredHosts";
+pub const SECRETS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredSecrets";
+pub const SYSTEMS_ATTRIBUTE: &str = ".#fleetConfigurations.default.configuredSystems";
+
+pub struct NixCopy {
+ closure: PathBuf,
+}
+impl NixCopy {
+ pub fn new(closure: PathBuf) -> Self {
+ Self { closure }
+ }
+ fn run_internal(&self, f: impl Fn(&mut Command)) -> Result<CommandOutput> {
+ let mut cmd = Command::new("nix");
+ cmd.stderr(Stdio::inherit())
+ .arg("copy")
+ .arg("--substitute-on-destination")
+ .arg(&self.closure);
+ f(&mut cmd);
+
+ let out = cmd.output()?;
+ if !out.status.success() {
+ anyhow::bail!("nix copy failed");
+ }
+ Ok(CommandOutput(out.stdout))
+ }
+ pub fn from(&self, from: impl AsRef<OsStr>) -> Result<()> {
+ let from = from.as_ref();
+ self.run_internal(|cmd| {
+ cmd.arg("--from").arg(from);
+ })?;
+ Ok(())
+ }
+ pub fn to(&self, to: impl AsRef<OsStr>) -> Result<()> {
+ let to = to.as_ref();
+ self.run_internal(|cmd| {
+ cmd.arg("--to").arg(to);
+ })?;
+ Ok(())
+ }
+}
+
+pub struct NixBuild {
+ attribute: String,
+ impure: bool,
+ env: HashMap<String, String>,
+}
+
+impl NixBuild {
+ pub fn new(attribute: String) -> Self {
+ Self {
+ attribute,
+ impure: false,
+ env: HashMap::new(),
+ }
+ }
+ pub fn env(&mut self, name: String, value: String) -> &mut Self {
+ self.impure = true;
+ self.env.insert(name, value);
+ self
+ }
+ pub fn run(&self) -> Result<tempfile::TempDir> {
+ let dir = tempfile::tempdir()?;
+ std::fs::remove_dir(dir.path())?;
+ let mut cmd = Command::new("nix");
+ cmd.stderr(Stdio::inherit())
+ .arg("build")
+ .arg(&self.attribute)
+ .arg("--no-link")
+ .arg("--out-link")
+ .arg(dir.path());
+ if self.impure {
+ cmd.arg("--impure");
+ }
+ if !self.env.is_empty() {
+ cmd.envs(&self.env);
+ }
+
+ let out = cmd.output()?;
+ if !out.status.success() {
+ anyhow::bail!("nix eval failed");
+ }
+ Ok(dir)
+ }
+}
+
+#[derive(Default)]
+pub struct NixEval {
+ attribute: String,
+ impure: bool,
+ apply: Option<String>,
+ env: HashMap<String, String>,
+}
+
+impl NixEval {
+ pub fn new(attribute: String) -> Self {
+ Self {
+ attribute,
+ ..Default::default()
+ }
+ }
+ pub fn impure(&mut self) -> &mut Self {
+ self.impure = true;
+ self
+ }
+ /// This is the only and impure way to pass something to flake
+ /// - https://github.com/NixOS/nix/issues/3949
+ /// - https://github.com/NixOS/nixpkgs/issues/101101
+ pub fn env(&mut self, name: String, value: String) -> &mut Self {
+ self.impure = true;
+ self.env.insert(name, value);
+ self
+ }
+ pub fn apply(&mut self, apply: String) -> &mut Self {
+ self.apply = Some(apply);
+ self
+ }
+ fn run_internal(&self, f: impl Fn(&mut Command)) -> Result<CommandOutput> {
+ let mut cmd = Command::new("nix");
+ cmd.stderr(Stdio::inherit())
+ .arg("eval")
+ .arg("--show-trace")
+ .arg(&self.attribute);
+ if let Some(apply) = &self.apply {
+ cmd.arg("--apply").arg(apply);
+ };
+ if self.impure {
+ cmd.arg("--impure");
+ }
+ if !self.env.is_empty() {
+ cmd.envs(&self.env);
+ }
+ f(&mut cmd);
+
+ let out = cmd.output()?;
+ if !out.status.success() {
+ anyhow::bail!("nix eval failed");
+ }
+ Ok(CommandOutput(out.stdout))
+ }
+ pub fn run(&self) -> Result<String> {
+ Ok(self.run_internal(|_cmd| {})?.as_str()?.to_owned())
+ }
+ pub fn run_json<T: DeserializeOwned>(&self) -> Result<T> {
+ Ok(serde_json::from_slice(
+ &self
+ .run_internal(|cmd| {
+ cmd.arg("--json");
+ })?
+ .0,
+ )?)
+ }
+ pub fn run_raw(&self) -> Result<String> {
+ Ok(self
+ .run_internal(|cmd| {
+ cmd.arg("--raw");
+ })?
+ .as_str()?
+ .to_owned())
+ }
+}