diff --git a/appmgr/.gitignore b/appmgr/.gitignore index 8587b9d3c..7fb7fc14c 100644 --- a/appmgr/.gitignore +++ b/appmgr/.gitignore @@ -1,3 +1,4 @@ /target **/*.rs.bk -.DS_Store \ No newline at end of file +.DS_Store +.vscode diff --git a/appmgr/Cargo.lock b/appmgr/Cargo.lock index 669782442..8535438d1 100644 --- a/appmgr/Cargo.lock +++ b/appmgr/Cargo.lock @@ -1,25 +1,21 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] -name = "addr2line" -version = "0.14.1" +name = "ahash" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98" dependencies = [ - "gimli", + "getrandom 0.2.3", + "once_cell", + "version_check", ] -[[package]] -name = "adler" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" - [[package]] name = "aho-corasick" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] @@ -30,57 +26,20 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] name = "anyhow" -version = "1.0.38" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" +checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" [[package]] -name = "appmgr" -version = "0.2.14" -dependencies = [ - "async-trait", - "avahi-sys", - "base32", - "clap", - "ctrlc", - "ed25519-dalek", - "emver", - "failure", - "file-lock", - "futures", - "git-version", - "http", - "itertools 0.9.0", - "lazy_static", - "libc", - "linear-map", - "log", - "nix 0.19.1", - "openssl", - "pest", - "pest_derive", - "prettytable-rs", - "rand 0.7.3", - "regex", - "reqwest", - "rpassword", - "rust-argon2", - "scopeguard", - "serde", - "serde_cbor", - "serde_json", - "serde_yaml", - "simple-logging", - "tokio 0.3.7", - "tokio-compat-02", - "tokio-tar", - "yajrc", -] +name = "array_tool" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f8cb5d814eb646a863c4f24978cff2880c4be96ad8cde2c0f0678732902e271" [[package]] name = "arrayref" @@ -96,15 +55,24 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "async-trait" -version = "0.1.42" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" +checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "atoi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" +dependencies = [ + "num-traits", +] + [[package]] name = "atty" version = "0.2.14" @@ -113,7 +81,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -131,20 +99,6 @@ dependencies = [ "libc", ] -[[package]] -name = "backtrace" -version = "0.3.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" -dependencies = [ - "addr2line", - "cfg-if 1.0.0", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - [[package]] name = "base32" version = "0.4.0" @@ -189,9 +143,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bitvec" -version = "0.19.4" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" dependencies = [ "funty", "radium", @@ -210,41 +164,62 @@ dependencies = [ "constant_time_eq", ] -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array 0.12.3", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.4", + "generic-array", ] [[package]] -name = "block-padding" -version = "0.1.5" +name = "bollard" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +checksum = "699194c00f3a2effd3358d47f880646818e3d483190b17ebcdf598c654fb77e9" dependencies = [ - "byte-tools", + "base64", + "bollard-stubs", + "bytes 1.0.1", + "chrono", + "ct-logs", + "dirs-next", + "futures-core", + "futures-util", + "hex", + "http", + "hyper", + "hyper-unix-connector", + "log", + "pin-project", + "serde", + "serde_derive", + "serde_json", + "serde_urlencoded", + "thiserror", + "tokio 1.9.0", + "tokio-util", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2f2e73fffe9455141e170fb9c1feb0ac521ec7e7dcd47a7cab72a658490fb8" +dependencies = [ + "chrono", + "serde", + "serde_with", ] [[package]] name = "bstr" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" +checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" dependencies = [ "lazy_static", "memchr", @@ -253,22 +228,22 @@ dependencies = [ ] [[package]] -name = "bumpalo" -version = "3.6.0" +name = "build_const" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099e596ef14349721d9016f6b80dd3419ea1bf289ab9b44df8e4dfd3a005d5d9" +checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7" [[package]] -name = "byte-tools" -version = "0.3.1" +name = "bumpalo" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" [[package]] name = "byteorder" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" @@ -276,12 +251,6 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" -[[package]] -name = "bytes" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0dcbc35f504eb6fc275a6d20e4ebcda18cf50d40ba6fabff8c711fa16cb3b16" - [[package]] name = "bytes" version = "1.0.1" @@ -290,9 +259,9 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "cc" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" [[package]] name = "cexpr" @@ -316,10 +285,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "clang-sys" -version = "1.1.0" +name = "chrono" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cb92721cb37482245ed88428f72253ce422b3b4ee169c70a0642521bb5db4cc" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "serde", + "time", + "winapi", +] + +[[package]] +name = "clang-sys" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "853eda514c284c2287f4bf20ae614f8781f40a81d32ecda6e91449304dfe077c" dependencies = [ "glob", "libc", @@ -335,7 +318,7 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", @@ -364,27 +347,96 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" [[package]] -name = "cpuid-bool" -version = "0.1.2" +name = "cpufeatures" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +dependencies = [ + "build_const", +] + +[[package]] +name = "crossbeam" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b10ddc024425c88c2ad148c1b0fd53f4c6d38db9697c9f1588381212fa657c9" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] [[package]] name = "crossbeam-utils" -version = "0.8.1" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" dependencies = [ - "autocfg", "cfg-if 1.0.0", "lazy_static", ] [[package]] name = "csv" -version = "1.1.5" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d58633299b24b515ac72a3f869f8b91306a3cec616a602843a383acd6f9e97" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" dependencies = [ "bstr", "csv-core", @@ -403,35 +455,60 @@ dependencies = [ ] [[package]] -name = "ctrlc" -version = "3.1.7" +name = "ct-logs" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b57a92e9749e10f25a171adcebfafe72991d45e7ec2dcb853e8f83d9dafaeb08" +checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8" dependencies = [ - "nix 0.18.0", - "winapi 0.3.9", + "sct", ] [[package]] name = "curve25519-dalek" -version = "3.0.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f627126b946c25a4638eec0ea634fc52506dea98db118aae985118ce7c3d723f" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", - "digest 0.9.0", + "digest", "rand_core 0.5.1", "subtle", "zeroize", ] [[package]] -name = "digest" -version = "0.8.1" +name = "darling" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12" dependencies = [ - "generic-array 0.12.3", + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc" +dependencies = [ + "darling_core", + "quote", + "syn", ] [[package]] @@ -440,7 +517,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.4", + "generic-array", ] [[package]] @@ -450,22 +527,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" dependencies = [ "libc", - "redox_users", - "winapi 0.3.9", + "redox_users 0.3.5", + "winapi", ] [[package]] -name = "dtoa" -version = "0.4.7" +name = "dirs-next" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.0", + "winapi", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" [[package]] name = "ed25519" -version = "1.0.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c66a534cbb46ab4ea03477eae19d5c22c01da8258030280b7bd9d8433fb6ef" +checksum = "4620d40f6d2601794401d6dd95a5cf69b6c157852539470eeda433a99b3c0efc" dependencies = [ + "serde", "signature", ] @@ -479,6 +584,7 @@ dependencies = [ "ed25519", "rand 0.7.3", "serde", + "serde_bytes", "sha2", "zeroize", ] @@ -489,11 +595,65 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "embassy" +version = "0.3.0" +dependencies = [ + "anyhow", + "async-trait", + "avahi-sys", + "base32", + "bollard", + "chrono", + "clap", + "digest", + "ed25519-dalek", + "emver", + "futures", + "git-version", + "http", + "id-pool", + "indexmap", + "itertools 0.10.1", + "jsonpath_lib", + "lazy_static", + "libc", + "log", + "nix 0.20.0", + "openssl", + "patch-db", + "pin-project", + "prettytable-rs", + "rand 0.8.4", + "regex", + "reqwest", + "rpassword", + "rpc-toolkit", + "rust-argon2", + "scopeguard", + "serde", + "serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json", + "serde_yaml", + "sha2", + "simple-logging", + "sqlx", + "thiserror", + "tokio 1.9.0", + "tokio-compat-02", + "tokio-stream", + "tokio-tar", + "tokio-util", + "toml", + "typed-builder", + "url", +] + [[package]] name = "emver" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7621f4df0519152a2ceb5f8cc0685e9fa6c8b7aec289e1afb37c66849cb71ffe" +checksum = "79b2c25504998d1069f80a8082e3c558acd7c103a2f2399306b12ed06fec5db8" dependencies = [ "either", "fp-core", @@ -530,55 +690,24 @@ dependencies = [ ] [[package]] -name = "failure" -version = "0.1.8" +name = "fd-lock-rs" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +checksum = "32a24e88458a5abfcd1cb81622511306f1aea43b900ddb9b34ff5ad8857a7685" dependencies = [ - "backtrace", - "failure_derive", -] - -[[package]] -name = "failure_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - -[[package]] -name = "file-lock" -version = "1.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16486239b3741480cef090b6f9924faf5dd5481022c6f266a51fab1a92971a2" -dependencies = [ - "gcc", - "libc", - "mktemp", - "nix 0.11.1", + "nix 0.19.1", ] [[package]] name = "filetime" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.5", - "winapi 0.3.9", + "redox_syscall 0.2.10", + "winapi", ] [[package]] @@ -604,9 +733,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ "matches", "percent-encoding", @@ -621,28 +750,6 @@ dependencies = [ "itertools 0.8.2", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - -[[package]] -name = "fuchsia-zircon" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -dependencies = [ - "bitflags", - "fuchsia-zircon-sys", -] - -[[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" - [[package]] name = "funty" version = "1.1.0" @@ -651,9 +758,9 @@ checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" [[package]] name = "futures" -version = "0.3.12" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" +checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" dependencies = [ "futures-channel", "futures-core", @@ -666,9 +773,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.12" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" +checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" dependencies = [ "futures-core", "futures-sink", @@ -676,15 +783,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.12" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" +checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" [[package]] name = "futures-executor" -version = "0.3.12" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" +checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c" dependencies = [ "futures-core", "futures-task", @@ -693,16 +800,17 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.12" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500" +checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" [[package]] name = "futures-macro" -version = "0.3.12" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd" +checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57" dependencies = [ + "autocfg", "proc-macro-hack", "proc-macro2", "quote", @@ -711,25 +819,23 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.12" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6" +checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" [[package]] name = "futures-task" -version = "0.3.12" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86" -dependencies = [ - "once_cell", -] +checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" [[package]] name = "futures-util" -version = "0.3.12" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" +checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" dependencies = [ + "autocfg", "futures-channel", "futures-core", "futures-io", @@ -737,28 +843,13 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.7", "pin-utils", "proc-macro-hack", "proc-macro-nested", "slab", ] -[[package]] -name = "gcc" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" - -[[package]] -name = "generic-array" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" -dependencies = [ - "typenum", -] - [[package]] name = "generic-array" version = "0.14.4" @@ -782,26 +873,20 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.10.0+wasi-snapshot-preview1", ] -[[package]] -name = "gimli" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" - [[package]] name = "git-version" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94918e83f1e01dedc2e361d00ce9487b14c58c7f40bab148026fa39d42cb41e2" +checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899" dependencies = [ "git-version-macro", "proc-macro-hack", @@ -809,9 +894,9 @@ dependencies = [ [[package]] name = "git-version-macro" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34a97a52fdee1870a34fa6e4b77570cba531b27d1838874fef4429a791a3d657" +checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f" dependencies = [ "proc-macro-hack", "proc-macro2", @@ -827,11 +912,11 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "h2" -version = "0.2.7" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726" dependencies = [ - "bytes 0.5.6", + "bytes 1.0.1", "fnv", "futures-core", "futures-sink", @@ -839,10 +924,9 @@ dependencies = [ "http", "indexmap", "slab", - "tokio 0.2.25", + "tokio 1.9.0", "tokio-util", "tracing", - "tracing-futures", ] [[package]] @@ -853,24 +937,51 @@ checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3" [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] -name = "http" -version = "0.2.3" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" dependencies = [ "bytes 1.0.1", "fnv", @@ -879,25 +990,26 @@ dependencies = [ [[package]] name = "http-body" -version = "0.3.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" dependencies = [ - "bytes 0.5.6", + "bytes 1.0.1", "http", + "pin-project-lite 0.2.7", ] [[package]] name = "httparse" -version = "1.3.5" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" +checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" [[package]] name = "httpdate" -version = "0.3.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" +checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" [[package]] name = "humantime" @@ -910,11 +1022,11 @@ dependencies = [ [[package]] name = "hyper" -version = "0.13.10" +version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" +checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" dependencies = [ - "bytes 0.5.6", + "bytes 1.0.1", "futures-channel", "futures-core", "futures-util", @@ -924,9 +1036,9 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project", + "pin-project-lite 0.2.7", "socket2", - "tokio 0.2.25", + "tokio 1.9.0", "tower-service", "tracing", "want", @@ -934,22 +1046,50 @@ dependencies = [ [[package]] name = "hyper-tls" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 0.5.6", + "bytes 1.0.1", "hyper", "native-tls", - "tokio 0.2.25", - "tokio-tls", + "tokio 1.9.0", + "tokio-native-tls", ] [[package]] -name = "idna" +name = "hyper-unix-connector" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ef1fd95d34b4ff007d3f0590727b5cf33572cace09b42032fc817dc8b16557" +dependencies = [ + "anyhow", + "hex", + "hyper", + "pin-project", + "tokio 1.9.0", +] + +[[package]] +name = "id-pool" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de910d521f7cc3135c4de8db1cb910e0b5ed1dc6f57c381cd07e8e661ce10094" +checksum = "5e33832e3000c359e3f67781634a5e6d58bffa8c5247c1464bd2e483f1a79cb0" +dependencies = [ + "serde", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ "matches", "unicode-bidi", @@ -958,37 +1098,29 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg", "hashbrown", + "serde", ] [[package]] name = "instant" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] - [[package]] name = "ipnet" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" [[package]] name = "itertools" @@ -1001,9 +1133,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.9.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" dependencies = [ "either", ] @@ -1016,21 +1148,43 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "js-sys" -version = "0.3.47" +version = "0.3.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65" +checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752" dependencies = [ "wasm-bindgen", ] [[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +name = "json-patch" +version = "0.2.7-alpha.0" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "json-ptr", + "serde", + "serde_json", + "treediff", +] + +[[package]] +name = "json-ptr" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "jsonpath_lib" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61352ec23883402b7d30b3313c16cbabefb8907361c4eb669d990cbb87ceee5a" +dependencies = [ + "array_tool", + "env_logger", + "log", + "serde", + "serde_json", ] [[package]] @@ -1047,9 +1201,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lexical-core" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ "arrayvec", "bitflags", @@ -1060,28 +1214,29 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.86" +version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" [[package]] name = "libloading" -version = "0.6.7" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" dependencies = [ "cfg-if 1.0.0", - "winapi 0.3.9", + "winapi", ] [[package]] -name = "linear-map" -version = "1.2.0" +name = "libsqlite3-sys" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" +checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d" dependencies = [ - "serde", - "serde_test", + "cc", + "pkg-config", + "vcpkg", ] [[package]] @@ -1092,9 +1247,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "lock_api" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" dependencies = [ "scopeguard", ] @@ -1122,9 +1277,18 @@ checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" [[package]] name = "memchr" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] [[package]] name = "mime" @@ -1132,87 +1296,26 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -[[package]] -name = "mime_guess" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "miniz_oxide" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" -dependencies = [ - "adler", - "autocfg", -] - [[package]] name = "mio" -version = "0.6.23" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" -dependencies = [ - "cfg-if 0.1.10", - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", - "libc", - "log", - "miow 0.2.2", - "net2", - "slab", - "winapi 0.2.8", -] - -[[package]] -name = "mio" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc250d6848c90d719ea2ce34546fb5df7af1d3fd189d10bf7bad80bfcebecd95" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" dependencies = [ "libc", "log", - "miow 0.3.6", + "miow", "ntapi", - "winapi 0.3.9", + "winapi", ] [[package]] name = "miow" -version = "0.2.2" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", -] - -[[package]] -name = "miow" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" -dependencies = [ - "socket2", - "winapi 0.3.9", -] - -[[package]] -name = "mktemp" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77001ceb9eed65439f3dc2a2543f9ba1417d912686bf224a7738d0966e6dcd69" -dependencies = [ - "uuid", + "winapi", ] [[package]] @@ -1233,42 +1336,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "net2" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" -dependencies = [ - "cfg-if 0.1.10", - "libc", - "winapi 0.3.9", -] - -[[package]] -name = "nix" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "becb657d662f1cd2ef38c7ad480ec6b8cf9e96b27adb543e594f9cf0f2e6065c" -dependencies = [ - "bitflags", - "cc", - "cfg-if 0.1.10", - "libc", - "void", -] - -[[package]] -name = "nix" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" -dependencies = [ - "bitflags", - "cc", - "cfg-if 0.1.10", - "libc", -] - [[package]] name = "nix" version = "0.19.1" @@ -1281,6 +1348,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", +] + [[package]] name = "nom" version = "5.1.2" @@ -1310,7 +1389,26 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" dependencies = [ - "winapi 0.3.9", + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", ] [[package]] @@ -1323,23 +1421,11 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" - [[package]] name = "once_cell" -version = "1.5.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" - -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] name = "opaque-debug" @@ -1349,29 +1435,29 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.32" +version = "0.10.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" +checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" dependencies = [ "bitflags", "cfg-if 1.0.0", "foreign-types", - "lazy_static", "libc", + "once_cell", "openssl-sys", ] [[package]] name = "openssl-probe" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.60" +version = "0.9.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6" +checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" dependencies = [ "autocfg", "cc", @@ -1400,9 +1486,49 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.2.5", + "redox_syscall 0.2.10", "smallvec", - "winapi 0.3.9", + "winapi", +] + +[[package]] +name = "patch-db" +version = "0.1.0" +dependencies = [ + "async-trait", + "fd-lock-rs", + "futures", + "indexmap", + "json-patch", + "json-ptr", + "lazy_static", + "nix 0.20.0", + "patch-db-macro", + "qutex-2", + "serde", + "serde_cbor 0.11.1", + "serde_json", + "thiserror", + "tokio 1.9.0", +] + +[[package]] +name = "patch-db-macro" +version = "0.1.0" +dependencies = [ + "patch-db-macro-internals", + "proc-macro2", + "syn", +] + +[[package]] +name = "patch-db-macro-internals" +version = "0.1.0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1417,63 +1543,20 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" -dependencies = [ - "maplit", - "pest", - "sha-1", -] - [[package]] name = "pin-project" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96fa8ebb90271c4477f144354485b8068bd8f6b78b428b01ba892ca26caf0b63" +checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758669ae3558c6f74bd2a18b41f7ac0b5a195aea6639d6a9b5e5d1ad5ba24c0b" +checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" dependencies = [ "proc-macro2", "quote", @@ -1482,15 +1565,15 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" [[package]] name = "pin-utils" @@ -1538,9 +1621,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" dependencies = [ "unicode-xid", ] @@ -1560,35 +1643,20 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "qutex-2" +version = "0.3.0" +dependencies = [ + "crossbeam", + "futures", +] + [[package]] name = "radium" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" -[[package]] -name = "rand" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" -dependencies = [ - "libc", - "rand 0.4.6", -] - -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi 0.3.9", -] - [[package]] name = "rand" version = "0.7.3" @@ -1604,14 +1672,14 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", - "rand_chacha 0.3.0", - "rand_core 0.6.2", - "rand_hc 0.3.0", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", ] [[package]] @@ -1626,29 +1694,14 @@ dependencies = [ [[package]] name = "rand_chacha" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.2", -] - -[[package]] -name = "rand_core" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "rand_core 0.4.2", + "ppv-lite86", + "rand_core 0.6.3", ] -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.5.1" @@ -1660,11 +1713,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.2", + "getrandom 0.2.3", ] [[package]] @@ -1678,20 +1731,11 @@ dependencies = [ [[package]] name = "rand_hc" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ - "rand_core 0.6.2", -] - -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", + "rand_core 0.6.3", ] [[package]] @@ -1702,9 +1746,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.5" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] @@ -1721,31 +1765,37 @@ dependencies = [ ] [[package]] -name = "regex" -version = "1.4.3" +name = "redox_users" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.3", + "redox_syscall 0.2.10", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] name = "regex-automata" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" -dependencies = [ - "byteorder", -] +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.6.22" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "remove_dir_all" @@ -1753,17 +1803,17 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] name = "reqwest" -version = "0.10.10" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" +checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" dependencies = [ "base64", - "bytes 0.5.6", + "bytes 1.0.1", "encoding_rs", "futures-core", "futures-util", @@ -1776,15 +1826,14 @@ dependencies = [ "lazy_static", "log", "mime", - "mime_guess", "native-tls", "percent-encoding", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.7", "serde", "serde_json", "serde_urlencoded", - "tokio 0.2.25", - "tokio-tls", + "tokio 1.9.0", + "tokio-native-tls", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -1792,6 +1841,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rpassword" version = "5.0.1" @@ -1799,7 +1863,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", +] + +[[package]] +name = "rpc-toolkit" +version = "0.1.0" +dependencies = [ + "clap", + "futures", + "hyper", + "lazy_static", + "reqwest", + "rpc-toolkit-macro", + "serde", + "serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json", + "thiserror", + "tokio 1.9.0", + "url", + "yajrc", +] + +[[package]] +name = "rpc-toolkit-macro" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "rpc-toolkit-macro-internals", + "syn", +] + +[[package]] +name = "rpc-toolkit-macro-internals" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1814,12 +1915,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rustc-demangle" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -1827,10 +1922,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "rustc-serialize" -version = "0.3.24" +name = "rustls" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64", + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" [[package]] name = "ryu" @@ -1845,7 +1953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ "lazy_static", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1855,10 +1963,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] -name = "security-framework" -version = "2.0.0" +name = "sct" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" dependencies = [ "bitflags", "core-foundation", @@ -1869,9 +1987,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.0.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" +checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284" dependencies = [ "core-foundation-sys", "libc", @@ -1879,13 +1997,30 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.123" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_cbor" +version = "0.11.1" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_cbor" version = "0.11.1" @@ -1898,9 +2033,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.123" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" +checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" dependencies = [ "proc-macro2", "quote", @@ -1909,24 +2044,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.62" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" +checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" dependencies = [ + "indexmap", "itoa", "ryu", "serde", ] -[[package]] -name = "serde_test" -version = "1.0.123" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38145a8510bdf71d9a8cceeb57664049538446e77f24648328bdbcf22dc7e169" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.0" @@ -1939,6 +2066,29 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad9fdbb69badc8916db738c25efd04f0a65297d26c2f8de4b62e57b8c12bc72" +dependencies = [ + "rustversion", + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1569374bd54623ec8bd592cf22ba6e03c0f177ff55fbc8c29a49e296e7adecf" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_yaml" version = "0.8.17" @@ -1951,29 +2101,17 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha2" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de" +checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" dependencies = [ - "block-buffer 0.9.0", + "block-buffer", "cfg-if 1.0.0", - "cpuid-bool", - "digest 0.9.0", - "opaque-debug 0.3.0", + "cpufeatures", + "digest", + "opaque-debug", ] [[package]] @@ -1984,18 +2122,18 @@ checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" [[package]] name = "signal-hook-registry" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" dependencies = [ "libc", ] [[package]] name = "signature" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0242b8e50dd9accdd56170e94ca1ebd223b098eb9c83539a6e367d0f36ae68" +checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335" [[package]] name = "simple-logging" @@ -2010,9 +2148,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" [[package]] name = "smallvec" @@ -2022,13 +2160,115 @@ checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" [[package]] name = "socket2" -version = "0.3.19" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" dependencies = [ - "cfg-if 1.0.0", "libc", - "winapi 0.3.9", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "sqlformat" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d86e3c77ff882a828346ba401a7ef4b8e440df804491c6064fe8295765de71c" +dependencies = [ + "lazy_static", + "maplit", + "nom 6.1.2", + "regex", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba82f79b31f30acebf19905bcd8b978f46891b9d0723f578447361a8910b6584" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f23af36748ec8ea8d49ef8499839907be41b0b1178a4e82b8cb45d29f531dc9" +dependencies = [ + "ahash", + "atoi", + "bitflags", + "byteorder", + "bytes 1.0.1", + "crc", + "crossbeam-channel", + "crossbeam-queue", + "crossbeam-utils", + "either", + "futures-channel", + "futures-core", + "futures-util", + "hashlink", + "hex", + "itoa", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "once_cell", + "parking_lot", + "percent-encoding", + "rustls", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "tokio-stream", + "url", + "webpki", + "webpki-roots", + "whoami", +] + +[[package]] +name = "sqlx-macros" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4a2349d1ffd60a03ca0de3f116ba55d7f406e55a0d84c64a5590866d94c06" +dependencies = [ + "dotenv", + "either", + "futures", + "heck", + "once_cell", + "proc-macro2", + "quote", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8199b421ecf3493ee9ef3e7bc90c904844cfb2ea7ea2f57347a93f52bfd3e057" +dependencies = [ + "once_cell", + "tokio 1.9.0", + "tokio-rustls", ] [[package]] @@ -2037,6 +2277,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.8.0" @@ -2044,16 +2294,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] -name = "subtle" -version = "2.4.0" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.60" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" dependencies = [ "proc-macro2", "quote", @@ -2062,9 +2318,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa" dependencies = [ "proc-macro2", "quote", @@ -2086,10 +2342,10 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if 1.0.0", "libc", - "rand 0.8.3", - "redox_syscall 0.2.5", + "rand 0.8.4", + "redox_syscall 0.2.10", "remove_dir_all", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2100,7 +2356,7 @@ checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" dependencies = [ "byteorder", "dirs", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2123,18 +2379,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" dependencies = [ "proc-macro2", "quote", @@ -2149,23 +2405,25 @@ checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" dependencies = [ "libc", "redox_syscall 0.1.57", - "winapi 0.3.9", + "winapi", ] [[package]] -name = "thread_local" -version = "1.1.3" +name = "time" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ - "once_cell", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", ] [[package]] name = "tinyvec" -version = "1.1.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" dependencies = [ "tinyvec_macros", ] @@ -2183,98 +2441,124 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" dependencies = [ "bytes 0.5.6", - "fnv", - "futures-core", - "iovec", - "lazy_static", - "memchr", - "mio 0.6.23", "num_cpus", - "pin-project-lite 0.1.11", + "pin-project-lite 0.1.12", "slab", ] [[package]] name = "tokio" -version = "0.3.7" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46409491c9375a693ce7032101970a54f8a2010efb77e13f70788f0d84489e39" +checksum = "4b7b349f11a7047e6d1276853e612d152f5e8a352c61917887cc2169e2366b4c" dependencies = [ "autocfg", - "bytes 0.6.0", - "futures-core", + "bytes 1.0.1", "libc", "memchr", - "mio 0.7.8", + "mio", "num_cpus", "once_cell", "parking_lot", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.7", "signal-hook-registry", - "slab", "tokio-macros", - "winapi 0.3.9", + "winapi", ] [[package]] name = "tokio-compat-02" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4cec419b8b6f06c32e74aae6d8c5e79646d038a38e5ea2b36045f2c3296e22" +checksum = "e7d4237822b7be8fff0a7a27927462fad435dcb6650f95cea9e946bf6bdc7e07" dependencies = [ "bytes 0.5.6", "once_cell", - "pin-project-lite 0.1.11", + "pin-project-lite 0.2.7", "tokio 0.2.25", - "tokio 0.3.7", + "tokio 1.9.0", + "tokio-stream", ] [[package]] name = "tokio-macros" -version = "0.3.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46dfffa59fc3c8aad216ed61bdc2c263d2b9d87a9c8ac9de0c11a813e51b6db7" +checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio 1.9.0", +] + +[[package]] +name = "tokio-rustls" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +dependencies = [ + "rustls", + "tokio 1.9.0", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f" +dependencies = [ + "futures-core", + "pin-project-lite 0.2.7", + "tokio 1.9.0", +] + [[package]] name = "tokio-tar" version = "0.3.0" -source = "git+https://github.com/dr-bonez/tokio-tar.git?rev=1ba710f3#1ba710f344ddb2a5b4d98bb96c905195c3cd9d43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50188549787c32c1c3d9c8c71ad7e003ccf2f102489c5a96e385c84760477f4" dependencies = [ "filetime", "futures-core", "libc", - "redox_syscall 0.2.5", - "tokio 0.3.7", + "redox_syscall 0.2.10", + "tokio 1.9.0", + "tokio-stream", "xattr", ] -[[package]] -name = "tokio-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" -dependencies = [ - "native-tls", - "tokio 0.2.25", -] - [[package]] name = "tokio-util" -version = "0.3.1" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" dependencies = [ - "bytes 0.5.6", + "bytes 1.0.1", "futures-core", "futures-sink", "log", - "pin-project-lite 0.1.11", - "tokio 0.2.25", + "pin-project-lite 0.2.7", + "tokio 1.9.0", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", ] [[package]] @@ -2285,33 +2569,31 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f77d3842f76ca899ff2dbcf231c5c65813dea431301d6eb686279c15c4464f12" +checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" dependencies = [ "cfg-if 1.0.0", - "log", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.7", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" dependencies = [ "lazy_static", ] [[package]] -name = "tracing-futures" -version = "0.2.5" +name = "treediff" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +checksum = "9975655adeb6c47931d6bcd4173a493a1feb510a549f9e5f41f6f1f9fc24836f" dependencies = [ - "pin-project", - "tracing", + "serde_json", ] [[package]] @@ -2321,44 +2603,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] -name = "typenum" -version = "1.12.0" +name = "typed-builder" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" - -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - -[[package]] -name = "unicase" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "345426c7406aa355b60c5007c79a2d1f5b605540072795222f17f6443e6a9c6f" dependencies = [ - "version_check", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "unicode-bidi" -version = "0.3.4" +name = "typenum" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + +[[package]] +name = "unicode-bidi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" dependencies = [ "matches", ] [[package]] name = "unicode-normalization" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + [[package]] name = "unicode-width" version = "0.1.8" @@ -2367,37 +2651,40 @@ checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" [[package]] name = "unicode-xid" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ "form_urlencoded", "idna", "matches", "percent-encoding", -] - -[[package]] -name = "uuid" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c590b5bd79ed10aad8fb75f078a59d8db445af6c743e55c4a53227fc01c13f" -dependencies = [ - "rand 0.3.23", - "rustc-serialize", + "serde", ] [[package]] name = "vcpkg" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vec_map" @@ -2407,15 +2694,9 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" - -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" [[package]] name = "want" @@ -2435,15 +2716,15 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.70" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be" +checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586" dependencies = [ "cfg-if 1.0.0", "serde", @@ -2453,9 +2734,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.70" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7" +checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f" dependencies = [ "bumpalo", "lazy_static", @@ -2468,9 +2749,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.20" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de431a2910c86679c34283a33f66f4e4abd7e0aec27b6669060148872aadf94" +checksum = "16646b21c3add8e13fdb8f20172f8a28c3dbf62f45406bcff0233188226cfe0c" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -2480,9 +2761,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.70" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c" +checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2490,9 +2771,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.70" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385" +checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f" dependencies = [ "proc-macro2", "quote", @@ -2503,20 +2784,39 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.70" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64" +checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2" [[package]] name = "web-sys" -version = "0.3.47" +version = "0.3.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3" +checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +dependencies = [ + "webpki", +] + [[package]] name = "which" version = "3.1.1" @@ -2527,10 +2827,14 @@ dependencies = [ ] [[package]] -name = "winapi" -version = "0.2.8" +name = "whoami" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +checksum = "4abacf325c958dfeaf1046931d37f2a901b6dfe0968ee965a29e94c6766b2af6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] [[package]] name = "winapi" @@ -2542,12 +2846,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -2560,7 +2858,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2575,17 +2873,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ - "winapi 0.3.9", -] - -[[package]] -name = "ws2_32-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" -dependencies = [ - "winapi 0.2.8", - "winapi-build", + "winapi", ] [[package]] @@ -2606,7 +2894,6 @@ dependencies = [ [[package]] name = "yajrc" version = "0.1.0" -source = "git+https://github.com/dr-bonez/yajrc?rev=c2952a4a21c50f7be6f8003afa37ee77deb66d56#c2952a4a21c50f7be6f8003afa37ee77deb66d56" dependencies = [ "anyhow", "serde", @@ -2625,18 +2912,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.2.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36" +checksum = "377db0846015f7ae377174787dd452e1c5f5a9050bc6f954911d01f116daa0cd" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f369ddb18862aba61aa49bf31e74d29f0f162dec753063200e1dc084345d16" +checksum = "a2c1e130bebaeab2f23886bf9acbaca14b092408c452543c857f66399cd6dab1" dependencies = [ "proc-macro2", "quote", diff --git a/appmgr/Cargo.toml b/appmgr/Cargo.toml index a10ea0b6e..a8ed9b90b 100644 --- a/appmgr/Cargo.toml +++ b/appmgr/Cargo.toml @@ -1,16 +1,28 @@ [package] authors = ["Aiden McClelland "] edition = "2018" -name = "appmgr" -version = "0.2.14" +name = "embassy" +version = "0.3.0" [lib] -name = "appmgrlib" +name = "embassy" path = "src/lib.rs" [[bin]] -name = "appmgr" -path = "src/main.rs" +name = "embassyd" +path = "src/bin/embassyd.rs" + +[[bin]] +name = "embassy-init" +path = "src/bin/embassy-init.rs" + +[[bin]] +name = "embassy-sdk" +path = "src/bin/embassy-sdk.rs" + +[[bin]] +name = "embassy-cli" +path = "src/bin/embassy-cli.rs" [features] avahi = ["avahi-sys"] @@ -19,42 +31,53 @@ portable = [] production = [] [dependencies] +anyhow = "1.0.40" async-trait = "0.1.42" avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", branch = "feature/dynamic-linking", features = [ "dynamic", ], optional = true } base32 = "0.4.0" +bollard = "0.10.1" +chrono = { version = "0.4.19", features = ["serde"] } clap = "2.33" -ctrlc = "3.1.7" -ed25519-dalek = "1.0.1" -emver = { version = "0.1.0", features = ["serde"] } -failure = "0.1.8" -file-lock = "1.1" +digest = "0.9.0" +ed25519-dalek = { version = "1.0.1", features = ["serde"] } +emver = { version = "0.1.2", features = ["serde"] } futures = "0.3.8" git-version = "0.3.4" http = "0.2.3" -itertools = "0.9.0" +id-pool = { version = "0.2.1", features = ["u16", "serde"], default-features = false } +indexmap = { version = "1.6.2", features = ["serde"] } +itertools = "0.10.0" +jsonpath_lib = "0.2.6" lazy_static = "1.4" libc = "0.2.86" -linear-map = { version = "1.2", features = ["serde_impl"] } log = "0.4.11" -nix = "0.19.1" +nix = "0.20.0" openssl = "0.10.30" -pest = "2.1" -pest_derive = "2.1" +patch-db = { path = "../../patch-db/patch-db" } +pin-project = "1.0.6" prettytable-rs = "0.8.0" -rand = "0.7.3" +rand = "0.8.3" regex = "1.4.2" -reqwest = { version = "0.10.9", features = ["stream", "json"] } +reqwest = { version = "0.11.2", features = ["stream", "json"] } rpassword = "5.0.0" +rpc-toolkit = { version = "*", path = "../../rpc-toolkit/rpc-toolkit" } rust-argon2 = "0.8.3" scopeguard = "1.1" # because avahi-sys fucks your shit up serde = { version = "1.0.118", features = ["derive", "rc"] } serde_cbor = "0.11.1" serde_json = "1.0.59" +serde_toml = { package = "toml", version = "0.5.8" } serde_yaml = "0.8.14" +sha2 = "0.9.3" simple-logging = "2.0" -tokio = { version = "0.3.5", features = ["full"] } -tokio-compat-02 = "0.1.2" -tokio-tar = { version = "0.3.0", git = "https://github.com/dr-bonez/tokio-tar.git", rev = "1ba710f3" } -yajrc = { version = "0.1.0", git = "https://github.com/dr-bonez/yajrc", rev = "c2952a4a21c50f7be6f8003afa37ee77deb66d56" } +sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite"] } +thiserror = "1.0.24" +tokio = { version = "1.5.0", features = ["full"] } +tokio-compat-02 = "0.2.0" +tokio-stream = { version = "0.1.5", features = ["io-util"] } +tokio-tar = "0.3.0" +tokio-util = { version = "0.6.6", features = ["io"] } +typed-builder = "0.9.0" +url = { version = "2.2.1", features = ["serde"] } diff --git a/appmgr/rustfmt.toml b/appmgr/rustfmt.toml new file mode 100644 index 000000000..64d94def2 --- /dev/null +++ b/appmgr/rustfmt.toml @@ -0,0 +1,2 @@ +group_imports = "StdExternalCrate" +imports_granularity = "Module" diff --git a/appmgr/src/action/docker.rs b/appmgr/src/action/docker.rs new file mode 100644 index 000000000..6051314db --- /dev/null +++ b/appmgr/src/action/docker.rs @@ -0,0 +1,234 @@ +use std::borrow::Cow; +use std::ffi::{OsStr, OsString}; +use std::net::Ipv4Addr; +use std::path::PathBuf; + +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::id::ImageId; +use crate::net::host::Hosts; +use crate::s9pk::manifest::{PackageId, SYSTEM_PACKAGE_ID}; +use crate::util::{Invoke, IoFormat, Version}; +use crate::volume::{VolumeId, Volumes}; +use crate::{Error, ResultExt}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct DockerAction { + pub image: ImageId, + #[serde(default)] + pub system: bool, + pub entrypoint: String, + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub mounts: IndexMap, + #[serde(default)] + pub io_format: Option, + #[serde(default)] + pub inject: bool, + #[serde(default)] + pub shm_size_mb: Option, // TODO: use postfix sizing? like 1k vs 1m vs 1g +} +impl DockerAction { + pub async fn create( + &self, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + ip: Ipv4Addr, + ) -> Result<(), Error> { + tokio::process::Command::new("docker") + .arg("create") + .arg("--net") + .arg("start9") + .arg("--ip") + .arg(format!("{}", ip)) + .arg("--name") + .arg(Self::container_name(pkg_id, pkg_version)) + .args(self.docker_args(pkg_id, pkg_version, volumes, false)) + .invoke(crate::ErrorKind::Docker) + .await?; + Ok(()) + } + + pub async fn execute Deserialize<'de>>( + &self, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + hosts: &Hosts, + input: Option, + allow_inject: bool, + ) -> Result, Error> { + let mut cmd = tokio::process::Command::new("docker"); + if self.inject && allow_inject { + cmd.arg("exec"); + } else { + cmd.arg("run").arg("--rm"); + cmd.args(hosts.docker_args()); + } + cmd.args(self.docker_args(pkg_id, pkg_version, volumes, allow_inject)); + let input_buf = if let (Some(input), Some(format)) = (&input, &self.io_format) { + cmd.stdin(std::process::Stdio::piped()); + Some(format.to_vec(input)?) + } else { + None + }; + let mut handle = cmd.spawn().with_kind(crate::ErrorKind::Docker)?; + if let (Some(input), Some(stdin)) = (&input_buf, &mut handle.stdin) { + use tokio::io::AsyncWriteExt; + stdin + .write_all(input) + .await + .with_kind(crate::ErrorKind::Docker)?; + } + let res = handle + .wait_with_output() + .await + .with_kind(crate::ErrorKind::Docker)?; + Ok(if res.status.success() { + Ok(if let Some(format) = &self.io_format { + match format.from_slice(&res.stdout) { + Ok(a) => a, + Err(e) => { + log::warn!( + "Failed to deserialize stdout from {}: {}, falling back to UTF-8 string.", + format, + e + ); + serde_json::from_value(String::from_utf8(res.stdout)?.into()) + .with_kind(crate::ErrorKind::Deserialization)? + } + } + } else if res.stdout.is_empty() { + serde_json::from_value(Value::Null).with_kind(crate::ErrorKind::Deserialization)? + } else { + serde_json::from_value(String::from_utf8(res.stdout)?.into()) + .with_kind(crate::ErrorKind::Deserialization)? + }) + } else { + Err(( + res.status.code().unwrap_or_default(), + String::from_utf8(res.stderr)?, + )) + }) + } + + pub async fn sandboxed Deserialize<'de>>( + &self, + pkg_id: &PackageId, + pkg_version: &Version, + input: Option, + ) -> Result, Error> { + let mut cmd = tokio::process::Command::new("docker"); + cmd.arg("run").arg("--rm"); + cmd.arg("--network=none"); + cmd.args(self.docker_args(pkg_id, pkg_version, &Volumes::default(), false)); + let input_buf = if let (Some(input), Some(format)) = (&input, &self.io_format) { + cmd.stdin(std::process::Stdio::piped()); + Some(format.to_vec(input)?) + } else { + None + }; + let mut handle = cmd.spawn().with_kind(crate::ErrorKind::Docker)?; + if let (Some(input), Some(stdin)) = (&input_buf, &mut handle.stdin) { + use tokio::io::AsyncWriteExt; + stdin + .write_all(input) + .await + .with_kind(crate::ErrorKind::Docker)?; + } + let res = handle + .wait_with_output() + .await + .with_kind(crate::ErrorKind::Docker)?; + Ok(if res.status.success() { + Ok(if let Some(format) = &self.io_format { + match format.from_slice(&res.stdout) { + Ok(a) => a, + Err(e) => { + log::warn!( + "Failed to deserialize stdout from {}: {}, falling back to UTF-8 string.", + format, + e + ); + serde_json::from_value(String::from_utf8(res.stdout)?.into()) + .with_kind(crate::ErrorKind::Deserialization)? + } + } + } else if res.stdout.is_empty() { + serde_json::from_value(Value::Null).with_kind(crate::ErrorKind::Deserialization)? + } else { + serde_json::from_value(String::from_utf8(res.stdout)?.into()) + .with_kind(crate::ErrorKind::Deserialization)? + }) + } else { + Err(( + res.status.code().unwrap_or_default(), + String::from_utf8(res.stderr)?, + )) + }) + } + + pub fn container_name(pkg_id: &PackageId, version: &Version) -> String { + format!("service_{}_{}", pkg_id, version) + } + + pub fn uncontainer_name(name: &str) -> Option<&str> { + name.strip_prefix("service_") + .and_then(|name| name.split("_").next()) + } + + fn docker_args<'a>( + &'a self, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + allow_inject: bool, + ) -> Vec> { + let mut res = Vec::with_capacity( + (2 * self.mounts.len()) // --mount + + (2 * self.shm_size_mb.is_some() as usize) // --shm-size + + 3 // --entrypoint + + self.args.len(), // [ARG...] + ); + for (volume_id, dst) in &self.mounts { + let src = if let Some(path) = volumes.get_path_for(pkg_id, volume_id) { + path + } else { + continue; + }; + res.push(OsStr::new("--mount").into()); + res.push( + OsString::from(format!( + "type=bind,src={},dst={}", + src.display(), + dst.display() + )) + .into(), + ); + } + if let Some(shm_size_mb) = self.shm_size_mb { + res.push(OsStr::new("--shm-size").into()); + res.push(OsString::from(format!("{}m", shm_size_mb)).into()); + } + if self.inject && allow_inject { + res.push(OsString::from(Self::container_name(pkg_id, pkg_version)).into()); + res.push(OsStr::new(&self.entrypoint).into()); + } else { + res.push(OsStr::new("--entrypoint").into()); + res.push(OsStr::new(&self.entrypoint).into()); + if self.system { + res.push(OsString::from(self.image.for_package(SYSTEM_PACKAGE_ID, None)).into()); + } else { + res.push(OsString::from(self.image.for_package(pkg_id, Some(pkg_version))).into()); + } + } + res.extend(self.args.iter().map(|s| OsStr::new(s).into())); + + res + } +} diff --git a/appmgr/src/action/mod.rs b/appmgr/src/action/mod.rs new file mode 100644 index 000000000..db09ae452 --- /dev/null +++ b/appmgr/src/action/mod.rs @@ -0,0 +1,160 @@ +use std::net::Ipv4Addr; +use std::path::Path; + +use anyhow::anyhow; +use indexmap::{IndexMap, IndexSet}; +use patch_db::HasModel; +use serde::{Deserialize, Serialize}; + +use self::docker::DockerAction; +use crate::config::{Config, ConfigSpec}; +use crate::id::Id; +use crate::net::host::Hosts; +use crate::s9pk::manifest::PackageId; +use crate::util::{ValuePrimative, Version}; +use crate::volume::Volumes; +use crate::{Error, ResultExt}; + +pub mod docker; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)] +pub struct ActionId = String>(Id); +impl> AsRef> for ActionId { + fn as_ref(&self) -> &ActionId { + self + } +} +impl> std::fmt::Display for ActionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } +} +impl> AsRef for ActionId { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} +impl> AsRef for ActionId { + fn as_ref(&self) -> &Path { + self.0.as_ref().as_ref() + } +} +impl<'de, S> Deserialize<'de> for ActionId +where + S: AsRef, + Id: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + Ok(ActionId(Deserialize::deserialize(deserializer)?)) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Actions(pub IndexMap); + +#[derive(Debug, Deserialize)] +#[serde(tag = "version")] +pub enum ActionResult { + #[serde(rename = "0")] + V0(ActionResultV0), +} + +#[derive(Debug, Deserialize)] +pub struct ActionResultV0 { + pub message: String, + pub value: ValuePrimative, + pub copyable: bool, + pub qr: bool, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum DockerStatus { + Running, + Stopped, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Action { + pub name: String, + pub description: String, + #[serde(default)] + pub warning: Option, + pub implementation: ActionImplementation, + pub allowed_statuses: IndexSet, + #[serde(default)] + pub input_spec: ConfigSpec, +} +impl Action { + pub async fn execute( + &self, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + hosts: &Hosts, + input: Config, + ) -> Result { + self.input_spec + .matches(&input) + .with_kind(crate::ErrorKind::ConfigSpecViolation)?; + self.implementation + .execute(pkg_id, pkg_version, volumes, hosts, Some(input), true) + .await? + .map_err(|e| Error::new(anyhow!("{}", e.1), crate::ErrorKind::Action)) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] +#[serde(rename = "kebab-case")] +#[serde(tag = "type")] +pub enum ActionImplementation { + Docker(DockerAction), +} +impl ActionImplementation { + pub async fn install( + &self, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + ip: Ipv4Addr, + ) -> Result<(), Error> { + match self { + ActionImplementation::Docker(action) => { + action.create(pkg_id, pkg_version, volumes, ip).await + } + } + } + pub async fn execute Deserialize<'de>>( + &self, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + hosts: &Hosts, + input: Option, + allow_inject: bool, + ) -> Result, Error> { + match self { + ActionImplementation::Docker(action) => { + action + .execute(pkg_id, pkg_version, volumes, hosts, input, allow_inject) + .await + } + } + } + pub async fn sandboxed Deserialize<'de>>( + &self, + pkg_id: &PackageId, + pkg_version: &Version, + input: Option, + ) -> Result, Error> { + match self { + ActionImplementation::Docker(action) => { + action.sandboxed(pkg_id, pkg_version, input).await + } + } + } +} diff --git a/appmgr/src/actions.rs b/appmgr/src/actions.rs deleted file mode 100644 index ae5ba1853..000000000 --- a/appmgr/src/actions.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::os::unix::process::ExitStatusExt; -use std::process::Stdio; - -use linear_map::set::LinearSet; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, Error as IoError}; -use yajrc::RpcError; - -use crate::apps::DockerStatus; - -pub const STATUS_NOT_ALLOWED: i32 = -2; -pub const INVALID_COMMAND: i32 = -3; - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct Action { - pub id: String, - pub name: String, - pub description: String, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] - pub warning: Option, - pub allowed_statuses: LinearSet, - pub command: Vec, -} - -async fn tee( - mut r: R, - mut w: W, -) -> Result, IoError> { - let mut res = Vec::new(); - let mut buf = vec![0; 2048]; - let mut bytes; - while { - bytes = r.read(&mut buf).await?; - bytes != 0 - } { - res.extend_from_slice(&buf[..bytes]); - w.write_all(&buf[..bytes]).await?; - } - w.flush().await?; - Ok(res) -} - -impl Action { - pub async fn perform(&self, app_id: &str) -> Result { - let man = crate::apps::manifest(app_id) - .await - .map_err(failure::Error::from) - .map_err(failure::Error::compat)?; - let status = crate::apps::status(app_id, true) - .await - .map_err(failure::Error::from) - .map_err(failure::Error::compat)? - .status; - if !self.allowed_statuses.contains(&status) { - return Err(RpcError { - code: STATUS_NOT_ALLOWED, - message: format!( - "{} is in status {:?} which is not allowed by {}", - app_id, status, self.id - ), - data: None, - }); - } - let mut cmd = if status == DockerStatus::Running { - let mut cmd = tokio::process::Command::new("docker"); - cmd.arg("exec").arg(&app_id).args(&self.command); - cmd - } else { - let mut cmd = tokio::process::Command::new("docker"); - let entrypoint = self.command.get(0).ok_or_else(|| RpcError { - code: INVALID_COMMAND, - message: "Command Cannot Be Empty".to_owned(), - data: None, - })?; - cmd.arg("run") - .arg("--rm") - .arg("--name") - .arg(format!("{}_{}", app_id, self.id)) - .arg("--mount") - .arg(format!( - "type=bind,src={}/{},dst={}", - crate::VOLUMES, - app_id, - man.mount.display() - )) - .arg("--entrypoint") - .arg(entrypoint) - .arg(format!("start9/{}", app_id)) - .args(&self.command[1..]); - // TODO: 0.3.0: net, tor, shm - cmd - }; - cmd.stdout(Stdio::piped()); - cmd.stderr(Stdio::piped()); - let mut child = cmd.spawn()?; - - let (stdout, stderr) = futures::try_join!( - tee(child.stdout.take().unwrap(), tokio::io::sink()), - tee(child.stderr.take().unwrap(), tokio::io::sink()) - )?; - - let status = child.wait().await?; - if status.success() { - String::from_utf8(stdout).map_err(From::from) - } else { - Err(RpcError { - code: status - .code() - .unwrap_or_else(|| status.signal().unwrap_or(0) + 128), - message: String::from_utf8(stderr)?, - data: None, - }) - } - } -} diff --git a/appmgr/src/apps.rs b/appmgr/src/apps.rs deleted file mode 100644 index 00e6c4dc6..000000000 --- a/appmgr/src/apps.rs +++ /dev/null @@ -1,457 +0,0 @@ -use failure::ResultExt as _; -use futures::future::{BoxFuture, FutureExt, OptionFuture}; -use linear_map::{set::LinearSet, LinearMap}; -use rand::SeedableRng; - -use crate::dependencies::AppDependencies; -use crate::manifest::{Manifest, ManifestLatest}; -use crate::util::Apply; -use crate::util::{from_yaml_async_reader, PersistencePath, YamlUpdateHandle}; -use crate::Error; -use crate::ResultExt as _; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum DockerStatus { - Running, - Stopped, // created || exited - Paused, - Restarting, - Removing, - Dead, -} - -fn not(b: &bool) -> bool { - !b -} - -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct AppInfo { - pub title: String, - pub version: emver::Version, - pub tor_address: Option, - pub configured: bool, - #[serde(default)] - #[serde(skip_serializing_if = "not")] - pub recoverable: bool, - #[serde(default)] - #[serde(skip_serializing_if = "not")] - pub needs_restart: bool, -} - -#[derive(Clone, Debug, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct AppStatus { - pub status: DockerStatus, -} - -#[derive(Debug, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct AppConfig { - pub spec: crate::config::ConfigSpec, - pub rules: Vec, - pub config: Option, -} - -#[derive(Debug, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct AppInfoFull { - #[serde(flatten)] - pub info: AppInfo, - #[serde(flatten)] - #[serde(skip_serializing_if = "Option::is_none")] - pub status: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub manifest: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub config: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub dependencies: Option, -} - -pub async fn list_info() -> Result, Error> { - let apps_path = PersistencePath::from_ref("apps.yaml"); - let mut f = match apps_path.maybe_read(false).await.transpose()? { - Some(a) => a, - None => return Ok(LinearMap::new()), - }; - from_yaml_async_reader(&mut *f).await -} - -pub async fn list_info_mut() -> Result>, Error> { - let apps_path = PersistencePath::from_ref("apps.yaml"); - YamlUpdateHandle::new_or_default(apps_path).await -} - -pub async fn add(id: &str, info: AppInfo) -> Result<(), failure::Error> { - let mut apps = list_info_mut().await?; - apps.insert(id.to_string(), info); - apps.commit().await?; - Ok(()) -} - -pub async fn set_configured(id: &str, configured: bool) -> Result<(), Error> { - let mut apps = list_info_mut().await?; - let mut app = apps - .get_mut(id) - .ok_or_else(|| failure::format_err!("App Not Installed: {}", id)) - .with_code(crate::error::NOT_FOUND)?; - app.configured = configured; - apps.commit().await?; - Ok(()) -} - -pub async fn set_needs_restart(id: &str, needs_restart: bool) -> Result<(), Error> { - let mut apps = list_info_mut().await?; - let mut app = apps - .get_mut(id) - .ok_or_else(|| failure::format_err!("App Not Installed: {}", id)) - .with_code(crate::error::NOT_FOUND)?; - app.needs_restart = needs_restart; - apps.commit().await?; - Ok(()) -} - -pub async fn set_recoverable(id: &str, recoverable: bool) -> Result<(), Error> { - let mut apps = list_info_mut().await?; - let mut app = apps - .get_mut(id) - .ok_or_else(|| failure::format_err!("App Not Installed: {}", id)) - .with_code(crate::error::NOT_FOUND)?; - app.recoverable = recoverable; - apps.commit().await?; - Ok(()) -} - -pub async fn remove(id: &str) -> Result<(), failure::Error> { - let mut apps = list_info_mut().await?; - apps.remove(id); - apps.commit().await?; - Ok(()) -} - -pub async fn status(id: &str, remap_crashed: bool) -> Result { - let output = std::process::Command::new("docker") - .args(&["inspect", id, "--format", "{{.State.Status}}"]) - .stdout(std::process::Stdio::piped()) - .stderr(match log::max_level() { - log::LevelFilter::Error => std::process::Stdio::null(), - _ => std::process::Stdio::inherit(), - }) - .spawn()? - .wait_with_output()?; - crate::ensure_code!( - output.status.success(), - crate::error::DOCKER_ERROR, - "{}: Docker Error: {}", - id, - std::str::from_utf8(&output.stderr).no_code()? - ); - let status = std::str::from_utf8(&output.stdout).no_code()?; - Ok(AppStatus { - status: match status.trim() { - "running" => DockerStatus::Running, - "restarting" => DockerStatus::Restarting, - "removing" => DockerStatus::Removing, - "dead" => DockerStatus::Dead, - "exited" - if remap_crashed && { - let path = PersistencePath::from_ref("running.yaml"); - if let Some(mut f) = path.maybe_read(false).await.transpose()? { - let running: Vec = from_yaml_async_reader(&mut *f).await?; - running.iter().filter(|a| a.as_str() == id).next().is_some() - } else { - false - } - } => - { - DockerStatus::Restarting - } - "created" | "exited" => DockerStatus::Stopped, - "paused" => DockerStatus::Paused, - _ => Err(format_err!("unknown status: {}", status))?, - }, - }) -} - -pub async fn manifest(id: &str) -> Result { - let manifest: Manifest = from_yaml_async_reader( - &mut *PersistencePath::from_ref("apps") - .join(id) - .join("manifest.yaml") - .read(false) - .await?, - ) - .await?; - Ok(manifest.into_latest()) -} - -pub async fn config(id: &str) -> Result { - let spec = PersistencePath::from_ref("apps") - .join(id) - .join("config_spec.yaml"); - let spec: crate::config::ConfigSpec = - crate::util::from_yaml_async_reader(&mut *spec.read(false).await?) - .await - .no_code()?; - let rules = PersistencePath::from_ref("apps") - .join(id) - .join("config_rules.yaml"); - let rules: Vec = - crate::util::from_yaml_async_reader(&mut *rules.read(false).await?) - .await - .no_code()?; - let config = PersistencePath::from_ref("apps") - .join(id) - .join("config.yaml"); - let config: Option = match config - .maybe_read(false) - .await - .transpose()? - .map(|mut f| async move { from_yaml_async_reader(&mut *f).await }) - .apply(OptionFuture::from) - .await - { - Some(Ok(cfg)) => Some(cfg), - #[cfg(not(feature = "production"))] - Some(Err(e)) => return Err(e), - _ => { - let volume_config = std::path::Path::new(crate::VOLUMES) - .join(id) - .join("start9") - .join("config.yaml"); - if volume_config.exists() { - let cfg_path = config.path(); - tokio::fs::copy(&volume_config, &cfg_path) - .await - .with_context(|e| { - format!( - "{}: {} -> {}", - e, - volume_config.display(), - cfg_path.display() - ) - }) - .with_code(crate::error::FILESYSTEM_ERROR)?; - let mut f = tokio::fs::File::open(&volume_config) - .await - .with_context(|e| format!("{}: {}", e, volume_config.display())) - .with_code(crate::error::FILESYSTEM_ERROR)?; - match from_yaml_async_reader(&mut f).await { - Ok(a) => Some(a), - #[cfg(not(feature = "production"))] - Err(e) => return Err(e), - #[cfg(feature = "production")] - _ => None, - } - } else { - None - } - } - }; - Ok(AppConfig { - spec, - rules, - config, - }) -} - -pub async fn config_or_default(id: &str) -> Result { - let config = config(id).await?; - Ok(if let Some(config) = config.config { - config - } else { - config - .spec - .gen(&mut rand::rngs::StdRng::from_entropy(), &None) - .with_code(crate::error::CFG_SPEC_VIOLATION)? - }) -} - -pub async fn info(id: &str) -> Result { - list_info() - .await - .map_err(Error::from)? - .get(id) - .ok_or_else(|| Error::new(failure::format_err!("{} is not installed", id), Some(6))) - .map(Clone::clone) -} - -pub async fn info_full( - id: &str, - with_status: bool, - with_manifest: bool, - with_config: bool, - with_dependencies: bool, -) -> Result { - Ok(AppInfoFull { - info: info(id).await?, - status: if with_status { - Some(status(id, true).await?) - } else { - None - }, - manifest: if with_manifest { - Some(manifest(id).await?) - } else { - None - }, - config: if with_config { - Some(config(id).await?) - } else { - None - }, - dependencies: if with_dependencies { - Some(dependencies(id, true).await?) - } else { - None - }, - }) -} - -pub async fn dependencies(id_version: &str, local_only: bool) -> Result { - let mut id_version_iter = id_version.split("@"); - let id = id_version_iter.next().unwrap(); - let version_range = id_version_iter - .next() - .map(|a| a.parse::()) - .transpose() - .with_context(|e| format!("Failed to Parse Version Requirement: {}", e)) - .no_code()? - .unwrap_or_else(emver::VersionRange::any); - let (manifest, config_info) = match list_info().await?.get(id) { - Some(info) if info.version.satisfies(&version_range) => { - futures::try_join!(manifest(id), config(id))? - } - _ if !local_only => futures::try_join!( - crate::registry::manifest(id, &version_range), - crate::registry::config(id, &version_range) - )?, - _ => { - return Err(failure::format_err!("App Not Installed: {}", id)) - .with_code(crate::error::NOT_FOUND) - } - }; - let config = if let Some(cfg) = config_info.config { - cfg - } else { - config_info - .spec - .gen(&mut rand::rngs::StdRng::from_entropy(), &None) - .unwrap_or_default() - }; - crate::dependencies::check_dependencies(manifest, &config, &config_info.spec).await -} - -pub async fn dependents(id: &str, transitive: bool) -> Result, Error> { - pub fn dependents_rec<'a>( - id: &'a str, - transitive: bool, - res: &'a mut LinearSet, - ) -> BoxFuture<'a, Result<(), Error>> { - async move { - for (app_id, _) in list_info().await? { - let manifest = manifest(&app_id).await?; - match manifest.dependencies.0.get(id) { - Some(info) if !res.contains(&app_id) => { - let config_info = config(&app_id).await?; - let config = if let Some(cfg) = config_info.config { - cfg - } else { - config_info - .spec - .gen(&mut rand::rngs::StdRng::from_entropy(), &None) - .unwrap_or_default() - }; - if info.optional.is_none() || config_info.spec.requires(&id, &config) { - res.insert(app_id.clone()); - if transitive { - dependents_rec(&app_id, true, res).await?; - } - } - } - _ => (), - } - } - Ok(()) - } - .boxed() - } - let mut res = LinearSet::new(); - dependents_rec(id, transitive, &mut res).await?; - Ok(res) -} - -pub async fn list( - with_status: bool, - with_manifest: bool, - with_config: bool, - with_dependencies: bool, -) -> Result, Error> { - let info = list_info().await?; - futures::future::join_all(info.into_iter().map(move |(id, info)| async move { - let (status, manifest, config, dependencies) = futures::try_join!( - OptionFuture::from(if with_status { - Some(status(&id, true)) - } else { - None - }) - .map(Option::transpose), - OptionFuture::from(if with_manifest { - Some(manifest(&id)) - } else { - None - }) - .map(Option::transpose), - OptionFuture::from(if with_config { Some(config(&id)) } else { None }) - .map(Option::transpose), - OptionFuture::from(if with_dependencies { - Some(dependencies(&id, true)) - } else { - None - }) - .map(Option::transpose) - )?; - Ok(( - id, - AppInfoFull { - info, - status, - manifest, - config, - dependencies, - }, - )) - })) - .await - .into_iter() - .collect() -} - -pub async fn print_instructions(id: &str) -> Result<(), Error> { - if let Some(file) = PersistencePath::from_ref("apps") - .join(id) - .join("instructions.md") - .maybe_read(false) - .await - { - use tokio::io::AsyncWriteExt; - - let mut stdout = tokio::io::stdout(); - tokio::io::copy(&mut *file?, &mut stdout) - .await - .with_code(crate::error::FILESYSTEM_ERROR)?; - stdout - .flush() - .await - .with_code(crate::error::FILESYSTEM_ERROR)?; - stdout - .shutdown() - .await - .with_code(crate::error::FILESYSTEM_ERROR)?; - Ok(()) - } else { - Err(failure::format_err!("No Instructions: {}", id)).with_code(crate::error::NOT_FOUND) - } -} diff --git a/appmgr/src/backup.rs b/appmgr/src/backup.rs deleted file mode 100644 index 4031c8226..000000000 --- a/appmgr/src/backup.rs +++ /dev/null @@ -1,286 +0,0 @@ -use std::os::unix::process::ExitStatusExt; -use std::path::Path; - -use argon2::Config; -use emver::Version; -use futures::try_join; -use futures::TryStreamExt; -use rand::Rng; -use serde::Serialize; - -use crate::util::from_yaml_async_reader; -use crate::util::to_yaml_async_writer; -use crate::util::Invoke; -use crate::util::PersistencePath; -use crate::version::VersionT; -use crate::Error; -use crate::ResultExt; - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct Metadata { - pub app_version: Version, - pub os_version: &'static Version, -} - -pub async fn create_backup>( - path: P, - app_id: &str, - password: &str, -) -> Result<(), Error> { - let path = tokio::fs::canonicalize(path).await?; - crate::ensure_code!( - path.is_dir(), - crate::error::FILESYSTEM_ERROR, - "Backup Path Must Be Directory" - ); - let metadata_path = path.join("metadata.yaml"); - let pw_path = path.join("password"); - let data_path = path.join("data"); - let tor_path = path.join("tor"); - let volume_path = Path::new(crate::VOLUMES).join(app_id); - let hidden_service_path = - Path::new(crate::tor::HIDDEN_SERVICE_DIR_ROOT).join(format!("app-{}", app_id)); - - if pw_path.exists() { - use tokio::io::AsyncReadExt; - - let mut f = tokio::fs::File::open(&pw_path).await?; - let mut hash = String::new(); - f.read_to_string(&mut hash).await?; - crate::ensure_code!( - argon2::verify_encoded(&hash, password.as_bytes()) - .with_code(crate::error::INVALID_BACKUP_PASSWORD)?, - crate::error::INVALID_BACKUP_PASSWORD, - "Invalid Backup Decryption Password" - ); - } - { - // save password - use tokio::io::AsyncWriteExt; - let salt = rand::thread_rng().gen::<[u8; 32]>(); - let hash = argon2::hash_encoded(password.as_bytes(), &salt, &Config::default()).unwrap(); // this is safe because apparently the API was poorly designed - let mut f = tokio::fs::File::create(pw_path).await?; - f.write_all(hash.as_bytes()).await?; - f.flush().await?; - } - - let info = crate::apps::info(app_id).await?; - to_yaml_async_writer( - tokio::fs::File::create(metadata_path).await?, - &Metadata { - app_version: info.version, - os_version: crate::version::Current::new().semver(), - }, - ) - .await?; - - let status = crate::apps::status(app_id, false).await?; - let exclude = if volume_path.is_dir() { - let ignore_path = volume_path.join(".backupignore"); - if ignore_path.is_file() { - use tokio::io::AsyncBufReadExt; - tokio::io::BufReader::new(tokio::fs::File::open(ignore_path).await?) - .lines() - .try_filter(|l| futures::future::ready(!l.is_empty())) - .try_collect() - .await? - } else { - Vec::new() - } - } else { - return Err(format_err!("Volume For {} Does Not Exist", app_id)) - .with_code(crate::error::NOT_FOUND); - }; - let running = status.status == crate::apps::DockerStatus::Running; - if running { - crate::control::pause_app(&app_id).await?; - } - let mut data_cmd = tokio::process::Command::new("duplicity"); - for exclude in exclude { - if exclude.starts_with('!') { - data_cmd.arg(format!( - "--include={}", - volume_path.join(exclude.trim_start_matches('!')).display() - )); - } else { - data_cmd.arg(format!("--exclude={}", volume_path.join(exclude).display())); - } - } - let data_res = data_cmd - .env("PASSPHRASE", password) - .arg(volume_path) - .arg(format!("file://{}", data_path.display())) - .invoke("Duplicity") - .await; - let tor_res = tokio::process::Command::new("duplicity") - .env("PASSPHRASE", password) - .arg(hidden_service_path) - .arg(format!("file://{}", tor_path.display())) - .invoke("Duplicity") - .await; - if running { - if crate::apps::info(&app_id).await?.needs_restart { - crate::control::restart_app(&app_id).await?; - } else { - crate::control::resume_app(&app_id).await?; - } - } - data_res?; - tor_res?; - - Ok(()) -} - -pub async fn restore_backup>( - path: P, - app_id: &str, - password: &str, -) -> Result<(), Error> { - let path = tokio::fs::canonicalize(path).await?; - crate::ensure_code!( - path.is_dir(), - crate::error::FILESYSTEM_ERROR, - "Backup Path Must Be Directory" - ); - let metadata_path = path.join("metadata.yaml"); - let pw_path = path.join("password"); - let data_path = path.join("data"); - let tor_path = path.join("tor"); - let volume_path = Path::new(crate::VOLUMES).join(app_id); - let hidden_service_path = - Path::new(crate::tor::HIDDEN_SERVICE_DIR_ROOT).join(format!("app-{}", app_id)); - - if pw_path.exists() { - use tokio::io::AsyncReadExt; - - let mut f = tokio::fs::File::open(&pw_path).await?; - let mut hash = String::new(); - f.read_to_string(&mut hash).await?; - crate::ensure_code!( - argon2::verify_encoded(&hash, password.as_bytes()) - .with_code(crate::error::INVALID_BACKUP_PASSWORD)?, - crate::error::INVALID_BACKUP_PASSWORD, - "Invalid Backup Decryption Password" - ); - } - - let status = crate::apps::status(app_id, false).await?; - let running = status.status == crate::apps::DockerStatus::Running; - if running { - crate::control::stop_app(app_id, true, false).await?; - } - - let mut data_cmd = tokio::process::Command::new("duplicity"); - data_cmd - .env("PASSPHRASE", password) - .arg("--force") - .arg(format!("file://{}", data_path.display())) - .arg(&volume_path); - - let mut tor_cmd = tokio::process::Command::new("duplicity"); - tor_cmd - .env("PASSPHRASE", password) - .arg("--force") - .arg(format!("file://{}", tor_path.display())) - .arg(&hidden_service_path); - - let (data_output, tor_output) = try_join!(data_cmd.status(), tor_cmd.status())?; - crate::ensure_code!( - data_output.success(), - crate::error::GENERAL_ERROR, - "Duplicity Error" - ); - crate::ensure_code!( - tor_output.success(), - crate::error::GENERAL_ERROR, - "Duplicity Error" - ); - - // Fix the tor address in apps.yaml - let mut yhdl = crate::apps::list_info_mut().await?; - if let Some(app_info) = yhdl.get_mut(app_id) { - app_info.tor_address = Some(crate::tor::read_tor_address(app_id, None).await?); - } - yhdl.commit().await?; - - tokio::fs::copy( - metadata_path, - Path::new(crate::VOLUMES) - .join(app_id) - .join("start9") - .join("restore.yaml"), - ) - .await?; - - // Attempt to configure the service with the config coming from restoration - let cfg_path = Path::new(crate::VOLUMES) - .join(app_id) - .join("start9") - .join("config.yaml"); - if cfg_path.exists() { - let cfg = from_yaml_async_reader(tokio::fs::File::open(cfg_path).await?).await?; - if let Err(e) = crate::config::configure(app_id, cfg, None, false).await { - log::warn!("Could not restore backup configuration: {}", e); - } - } - - crate::tor::restart().await?; - // Delete the fullchain certificate, so it can be regenerated with the restored tor pubkey address - PersistencePath::from_ref("apps") - .join(&app_id) - .join("cert-local.fullchain.crt.pem") - .delete() - .await?; - crate::tor::write_lan_services( - &crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?, - ) - .await?; - let svc_exit = std::process::Command::new("service") - .args(&["nginx", "reload"]) - .status()?; - crate::ensure_code!( - svc_exit.success(), - crate::error::GENERAL_ERROR, - "Failed to Reload Nginx: {}", - svc_exit - .code() - .or_else(|| { svc_exit.signal().map(|a| 128 + a) }) - .unwrap_or(0) - ); - - Ok(()) -} - -pub async fn backup_to_partition( - logicalname: &str, - app_id: &str, - password: &str, -) -> Result<(), Error> { - let backup_mount_path = Path::new(crate::BACKUP_MOUNT_POINT); - let guard = crate::disks::MountGuard::new(logicalname, &backup_mount_path).await?; - let backup_dir_path = backup_mount_path.join(crate::BACKUP_DIR).join(app_id); - tokio::fs::create_dir_all(&backup_dir_path).await?; - - let res = create_backup(backup_dir_path, app_id, password).await; - - guard.unmount().await?; - - res -} - -pub async fn restore_from_partition( - logicalname: &str, - app_id: &str, - password: &str, -) -> Result<(), Error> { - let backup_mount_path = Path::new(crate::BACKUP_MOUNT_POINT); - let guard = crate::disks::MountGuard::new(logicalname, &backup_mount_path).await?; - let backup_dir_path = backup_mount_path.join(crate::BACKUP_DIR).join(app_id); - - let res = restore_backup(backup_dir_path, app_id, password).await; - - guard.unmount().await?; - - res -} diff --git a/appmgr/src/backup/mod.rs b/appmgr/src/backup/mod.rs new file mode 100644 index 000000000..4a348cc4f --- /dev/null +++ b/appmgr/src/backup/mod.rs @@ -0,0 +1,51 @@ +use anyhow::anyhow; +use patch_db::HasModel; +use serde::{Deserialize, Serialize}; + +use crate::action::ActionImplementation; +use crate::net::host::Hosts; +use crate::s9pk::manifest::PackageId; +use crate::util::Version; +use crate::volume::{Volume, VolumeId, Volumes}; +use crate::{Error, ResultExt}; + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] +pub struct BackupActions { + pub create: ActionImplementation, + pub restore: ActionImplementation, +} +impl BackupActions { + pub async fn backup( + &self, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + hosts: &Hosts, + ) -> Result<(), Error> { + let mut volumes = volumes.to_readonly(); + volumes.insert(VolumeId::Backup, Volume::Backup { readonly: false }); + self.create + .execute(pkg_id, pkg_version, &volumes, hosts, None::<()>, false) + .await? + .map_err(|e| anyhow!("{}", e.1)) + .with_kind(crate::ErrorKind::Backup)?; + Ok(()) + } + + pub async fn restore( + &self, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + hosts: &Hosts, + ) -> Result<(), Error> { + let mut volumes = volumes.clone(); + volumes.insert(VolumeId::Backup, Volume::Backup { readonly: true }); + self.restore + .execute(pkg_id, pkg_version, &volumes, hosts, None::<()>, false) + .await? + .map_err(|e| anyhow!("{}", e.1)) + .with_kind(crate::ErrorKind::Restore)?; + Ok(()) + } +} diff --git a/appmgr/src/bin/embassy-cli.rs b/appmgr/src/bin/embassy-cli.rs new file mode 100644 index 000000000..3e232d28d --- /dev/null +++ b/appmgr/src/bin/embassy-cli.rs @@ -0,0 +1,28 @@ +use clap::Arg; +use embassy::context::{CliContext, EitherContext}; +use embassy::Error; +use rpc_toolkit::run_cli; + +fn inner_main() -> Result<(), Error> { + simple_logging::log_to_stderr(log::LevelFilter::Info); + run_cli!( + embassy::main_api, + app => app + .arg(Arg::with_name("host").long("host").short("h").takes_value(true)) + .arg(Arg::with_name("port").long("port").short("p").takes_value(true)), + matches => EitherContext::Cli(CliContext::init(matches)?), + |code| if code < 0 { 1 } else { code } + ) +} + +fn main() { + match inner_main() { + Ok(_) => (), + Err(e) => { + eprintln!("{}", e.source); + log::debug!("{:?}", e.source); + drop(e.source); + std::process::exit(e.kind as i32) + } + } +} diff --git a/appmgr/src/bin/embassy-init.rs b/appmgr/src/bin/embassy-init.rs new file mode 100644 index 000000000..f92097a52 --- /dev/null +++ b/appmgr/src/bin/embassy-init.rs @@ -0,0 +1,22 @@ +use embassy::Error; + +async fn inner_main() -> Result<(), Error> { + // os sync + embassy::volume::disk::mount("/dev/sda", "/mnt/embassy-os-crypt").await?; + + Ok(()) +} + +fn main() { + let rt = tokio::runtime::Runtime::new().expect("failed to initialize runtime"); + match rt.block_on(inner_main()) { + Ok(_) => (), + Err(e) => { + drop(rt); + eprintln!("{}", e.source); + log::debug!("{:?}", e.source); + drop(e.source); + std::process::exit(e.kind as i32) + } + } +} diff --git a/appmgr/src/bin/embassy-sdk.rs b/appmgr/src/bin/embassy-sdk.rs new file mode 100644 index 000000000..d9dee2a1b --- /dev/null +++ b/appmgr/src/bin/embassy-sdk.rs @@ -0,0 +1,28 @@ +use clap::Arg; +use embassy::context::{CliContext, EitherContext}; +use embassy::Error; +use rpc_toolkit::run_cli; + +fn inner_main() -> Result<(), Error> { + simple_logging::log_to_stderr(log::LevelFilter::Info); + run_cli!( + embassy::portable_api, + app => app + .arg(Arg::with_name("host").long("host").short("h").takes_value(true)) + .arg(Arg::with_name("port").long("port").short("p").takes_value(true)), + matches => EitherContext::Cli(CliContext::init(matches)?), + |code| if code < 0 { 1 } else { code } + ) +} + +fn main() { + match inner_main() { + Ok(_) => (), + Err(e) => { + eprintln!("{}", e.source); + log::debug!("{:?}", e.source); + drop(e.source); + std::process::exit(e.kind as i32) + } + } +} diff --git a/appmgr/src/bin/embassyd.rs b/appmgr/src/bin/embassyd.rs new file mode 100644 index 000000000..19719baa7 --- /dev/null +++ b/appmgr/src/bin/embassyd.rs @@ -0,0 +1,84 @@ +use std::time::Duration; + +use embassy::context::{EitherContext, RpcContext}; +use embassy::db::model::Database; +use embassy::status::{check_all, synchronize_all}; +use embassy::util::daemon; +use embassy::{Error, ErrorKind}; +use futures::TryFutureExt; +use patch_db::json_ptr::JsonPointer; +use rpc_toolkit::hyper::StatusCode; +use rpc_toolkit::rpc_server; + +fn status_fn(_: i32) -> StatusCode { + StatusCode::OK +} + +async fn inner_main() -> Result<(), Error> { + simple_logging::log_to_stderr(log::LevelFilter::Info); + let rpc_ctx = RpcContext::init().await?; + if !rpc_ctx.db.exists(&::default()).await? { + rpc_ctx + .db + .put(&::default(), &Database::init(), None) + .await?; + } + let ctx = EitherContext::Rpc(rpc_ctx.clone()); + let server = rpc_server!(embassy::main_api, ctx, status_fn); + let status_ctx = rpc_ctx.clone(); + let status_daemon = daemon( + move || { + let ctx = status_ctx.clone(); + async move { + if let Err(e) = synchronize_all(&ctx).await { + log::error!("Error in Status Sync daemon: {}", e); + log::debug!("{:?}", e); + } else { + log::info!("Status Sync completed successfully"); + } + } + }, + Duration::from_millis(500), + ); + let health_ctx = rpc_ctx.clone(); + let health_daemon = daemon( + move || { + let ctx = health_ctx.clone(); + async move { + if let Err(e) = check_all(&ctx).await { + log::error!("Error in Health Check daemon: {}", e); + log::debug!("{:?}", e); + } else { + log::info!("Health Check completed successfully"); + } + } + }, + Duration::from_millis(500), + ); + futures::try_join!( + server.map_err(|e| Error::new(e, ErrorKind::Network)), + status_daemon.map_err(|e| Error::new( + e.context("Status Sync daemon panicked!"), + ErrorKind::Unknown + )), + health_daemon.map_err(|e| Error::new( + e.context("Health Check daemon panicked!"), + ErrorKind::Unknown + )), + )?; + Ok(()) +} + +fn main() { + let rt = tokio::runtime::Runtime::new().expect("failed to initialize runtime"); + match rt.block_on(inner_main()) { + Ok(_) => (), + Err(e) => { + drop(rt); + eprintln!("{}", e.source); + log::debug!("{:?}", e.source); + drop(e.source); + std::process::exit(e.kind as i32) + } + } +} diff --git a/appmgr/src/config/action.rs b/appmgr/src/config/action.rs new file mode 100644 index 000000000..216c3b9ca --- /dev/null +++ b/appmgr/src/config/action.rs @@ -0,0 +1,81 @@ +use anyhow::anyhow; +use indexmap::{IndexMap, IndexSet}; +use nix::sys::signal::Signal; +use patch_db::HasModel; +use serde::{Deserialize, Serialize}; + +use super::{Config, ConfigSpec}; +use crate::action::ActionImplementation; +use crate::dependencies::Dependencies; +use crate::net::host::Hosts; +use crate::s9pk::manifest::PackageId; +use crate::status::health_check::HealthCheckId; +use crate::util::Version; +use crate::volume::Volumes; +use crate::Error; + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +pub struct ConfigRes { + pub config: Option, + pub spec: ConfigSpec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] +pub struct ConfigActions { + pub get: ActionImplementation, + pub set: ActionImplementation, +} +impl ConfigActions { + pub async fn get( + &self, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + hosts: &Hosts, + ) -> Result { + self.get + .execute(pkg_id, pkg_version, volumes, hosts, None::<()>, false) + .await + .and_then(|res| { + res.map_err(|e| Error::new(anyhow!("{}", e.1), crate::ErrorKind::ConfigGen)) + }) + } + + pub async fn set( + &self, + pkg_id: &PackageId, + pkg_version: &Version, + dependencies: &Dependencies, + volumes: &Volumes, + hosts: &Hosts, + input: &Config, + ) -> Result { + let res: SetResult = self + .set + .execute(pkg_id, pkg_version, volumes, hosts, Some(input), false) + .await + .and_then(|res| { + res.map_err(|e| { + Error::new(anyhow!("{}", e.1), crate::ErrorKind::ConfigRulesViolation) + }) + })?; + Ok(SetResult { + signal: res.signal, + depends_on: res + .depends_on + .into_iter() + .filter(|(pkg, _)| dependencies.0.contains_key(pkg)) + .collect(), + }) + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct SetResult { + #[serde(deserialize_with = "crate::util::deserialize_from_str_opt")] + #[serde(serialize_with = "crate::util::serialize_display_opt")] + pub signal: Option, + pub depends_on: IndexMap>, +} diff --git a/appmgr/src/config/mod.rs b/appmgr/src/config/mod.rs index 20b41b5a6..69d31f8ee 100644 --- a/appmgr/src/config/mod.rs +++ b/appmgr/src/config/mod.rs @@ -1,56 +1,81 @@ -use std::borrow::Cow; -use std::path::Path; use std::time::Duration; -use failure::ResultExt as _; +use anyhow::anyhow; +use bollard::container::KillContainerOptions; +use bollard::Docker; use futures::future::{BoxFuture, FutureExt}; +use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; -use linear_map::{set::LinearSet, LinearMap}; +use patch_db::DbHandle; use rand::SeedableRng; use regex::Regex; +use rpc_toolkit::command; +use serde_json::Value; -use crate::dependencies::{DependencyError, TaggedDependencyError}; -use crate::util::PersistencePath; -use crate::util::{from_yaml_async_reader, to_yaml_async_writer}; -use crate::ResultExt as _; +use crate::action::docker::DockerAction; +use crate::config::spec::PackagePointerSpecVariant; +use crate::context::{EitherContext, ExtendedContext}; +use crate::db::model::{CurrentDependencyInfo, InstalledPackageDataEntryModel}; +use crate::db::util::WithRevision; +use crate::dependencies::{BreakageRes, DependencyError, TaggedDependencyError}; +use crate::net::host::Hosts; +use crate::s9pk::manifest::PackageId; +use crate::util::{ + display_none, display_serializable, parse_duration, parse_stdin_deserializable, IoFormat, +}; +use crate::{Error, ResultExt as _}; -pub mod rules; +pub mod action; pub mod spec; pub mod util; -pub mod value; -pub use rules::{ConfigRuleEntry, ConfigRuleEntryWithSuggestions}; pub use spec::{ConfigSpec, Defaultable}; use util::NumRange; -pub use value::Config; -#[derive(Debug, Fail)] +use self::action::ConfigRes; +use self::spec::{PackagePointerSpec, ValueSpecPointer}; + +pub type Config = serde_json::Map; +pub trait TypeOf { + fn type_of(&self) -> &'static str; +} +impl TypeOf for Value { + fn type_of(&self) -> &'static str { + match self { + Value::Array(_) => "list", + Value::Bool(_) => "boolean", + Value::Null => "null", + Value::Number(_) => "number", + Value::Object(_) => "object", + Value::String(_) => "string", + } + } +} + +#[derive(Debug, thiserror::Error)] pub enum ConfigurationError { - #[fail(display = "Timeout Error")] - TimeoutError, - #[fail(display = "No Match: {}", _0)] - NoMatch(NoMatchWithPath), - #[fail(display = "Invalid Variant: {}", _0)] - InvalidVariant(String), - #[fail(display = "System Error: {}", _0)] - SystemError(crate::Error), + #[error("Timeout Error")] + TimeoutError(#[from] TimeoutError), + #[error("No Match: {0}")] + NoMatch(#[from] NoMatchWithPath), + #[error("System Error: {0}")] + SystemError(Error), } -impl From for ConfigurationError { - fn from(_: TimeoutError) -> Self { - ConfigurationError::TimeoutError - } -} -impl From for ConfigurationError { - fn from(e: NoMatchWithPath) -> Self { - ConfigurationError::NoMatch(e) +impl From for Error { + fn from(err: ConfigurationError) -> Self { + let kind = match &err { + ConfigurationError::SystemError(e) => e.kind, + _ => crate::ErrorKind::ConfigGen, + }; + crate::Error::new(err, kind) } } -#[derive(Clone, Copy, Debug, Fail)] -#[fail(display = "Timeout Error")] +#[derive(Clone, Copy, Debug, thiserror::Error)] +#[error("Timeout Error")] pub struct TimeoutError; -#[derive(Clone, Debug, Fail)] +#[derive(Clone, Debug, thiserror::Error)] pub struct NoMatchWithPath { pub path: Vec, pub error: MatchError, @@ -72,256 +97,504 @@ impl std::fmt::Display for NoMatchWithPath { write!(f, "{}: {}", self.path.iter().rev().join("."), self.error) } } +impl From for Error { + fn from(e: NoMatchWithPath) -> Self { + ConfigurationError::from(e).into() + } +} -#[derive(Clone, Debug, Fail)] +#[derive(Clone, Debug, thiserror::Error)] pub enum MatchError { - #[fail(display = "String {:?} Does Not Match Pattern {}", _0, _1)] + #[error("String {0:?} Does Not Match Pattern {1}")] Pattern(String, Regex), - #[fail(display = "String {:?} Is Not In Enum {:?}", _0, _1)] - Enum(String, LinearSet), - #[fail(display = "Field Is Not Nullable")] + #[error("String {0:?} Is Not In Enum {1:?}")] + Enum(String, IndexSet), + #[error("Field Is Not Nullable")] NotNullable, - #[fail(display = "Length Mismatch: expected {}, actual: {}", _0, _1)] + #[error("Length Mismatch: expected {0}, actual: {1}")] LengthMismatch(NumRange, usize), - #[fail(display = "Invalid Type: expected {}, actual: {}", _0, _1)] + #[error("Invalid Type: expected {0}, actual: {1}")] InvalidType(&'static str, &'static str), - #[fail(display = "Number Out Of Range: expected {}, actual: {}", _0, _1)] + #[error("Number Out Of Range: expected {0}, actual: {1}")] OutOfRange(NumRange, f64), - #[fail(display = "Number Is Not Integral: {}", _0)] + #[error("Number Is Not Integral: {0}")] NonIntegral(f64), - #[fail(display = "Variant {:?} Is Not In Union {:?}", _0, _1)] - Union(String, LinearSet), - #[fail(display = "Variant Is Missing Tag {:?}", _0)] + #[error("Variant {0:?} Is Not In Union {1:?}")] + Union(String, IndexSet), + #[error("Variant Is Missing Tag {0:?}")] MissingTag(String), - #[fail( - display = "Property {:?} Of Variant {:?} Conflicts With Union Tag", - _0, _1 - )] + #[error("Property {0:?} Of Variant {1:?} Conflicts With Union Tag")] PropertyMatchesUnionTag(String, String), - #[fail(display = "Name of Property {:?} Conflicts With Map Tag Name", _0)] + #[error("Name of Property {0:?} Conflicts With Map Tag Name")] PropertyNameMatchesMapTag(String), - #[fail(display = "Pointer Is Invalid: {}", _0)] + #[error("Pointer Is Invalid: {0}")] InvalidPointer(spec::ValueSpecPointer), - #[fail(display = "Object Key Is Invalid: {}", _0)] + #[error("Object Key Is Invalid: {0}")] InvalidKey(String), - #[fail(display = "Value In List Is Not Unique")] + #[error("Value In List Is Not Unique")] ListUniquenessViolation, } -#[derive(Clone, Debug, Default, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ConfigurationRes { - pub changed: LinearMap, - pub needs_restart: LinearSet, - pub stopped: LinearMap, +#[command(subcommands(get, set))] +pub fn config( + #[context] ctx: EitherContext, + #[arg] id: PackageId, +) -> Result, Error> { + Ok(ExtendedContext::from(ctx).map(|_| id)) } -// returns apps with changed configurations -pub async fn configure( - name: &str, - config: Option, - timeout: Option, - dry_run: bool, -) -> Result { - async fn handle_broken_dependent( - name: &str, - dependent: String, - dry_run: bool, - res: &mut ConfigurationRes, - error: DependencyError, - ) -> Result<(), crate::Error> { - crate::control::stop_dependents( - &dependent, - dry_run, - DependencyError::NotRunning, - &mut res.stopped, - ) +#[command(display(display_serializable))] +pub async fn get( + #[context] ctx: ExtendedContext, + #[allow(unused_variables)] + #[arg(long = "format")] + format: Option, +) -> Result { + let mut db = ctx.base().as_rpc().unwrap().db.handle(); + let pkg_model = crate::db::DatabaseModel::new() + .package_data() + .idx_model(ctx.extension()) + .and_then(|m| m.installed()) + .expect(&mut db) + .await + .with_kind(crate::ErrorKind::NotFound)?; + let action = pkg_model + .clone() + .manifest() + .config() + .get(&mut db) + .await? + .to_owned() + .ok_or_else(|| { + Error::new( + anyhow!("{} has no config", ctx.extension()), + crate::ErrorKind::NotFound, + ) + })?; + let version = pkg_model.clone().manifest().version().get(&mut db).await?; + let volumes = pkg_model.manifest().volumes().get(&mut db).await?; + let hosts = crate::db::DatabaseModel::new() + .network() + .hosts() + .get(&mut db) .await?; - if crate::apps::status(&dependent, false).await?.status - != crate::apps::DockerStatus::Stopped - { - crate::control::stop_app(&dependent, false, dry_run).await?; - res.stopped.insert( - // TODO: maybe don't do this if its not running - dependent, - TaggedDependencyError { - dependency: name.to_owned(), - error, - }, - ); - } - Ok(()) - } - fn configure_rec<'a>( - name: &'a str, - config: Option, - timeout: Option, - dry_run: bool, - res: &'a mut ConfigurationRes, - ) -> BoxFuture<'a, Result> { - async move { - let info = crate::apps::list_info() - .await? - .remove(name) - .ok_or_else(|| failure::format_err!("{} is not installed", name)) - .with_code(crate::error::NOT_FOUND)?; - let mut rng = rand::rngs::StdRng::from_entropy(); - let spec_path = PersistencePath::from_ref("apps") - .join(name) - .join("config_spec.yaml"); - let rules_path = PersistencePath::from_ref("apps") - .join(name) - .join("config_rules.yaml"); - let config_path = PersistencePath::from_ref("apps") - .join(name) - .join("config.yaml"); - let spec: ConfigSpec = - from_yaml_async_reader(&mut *spec_path.read(false).await?).await?; - let rules: Vec = - from_yaml_async_reader(&mut *rules_path.read(false).await?).await?; - let old_config: Option = - if let Some(mut f) = config_path.maybe_read(false).await.transpose()? { - Some(from_yaml_async_reader(&mut *f).await?) + action + .get(ctx.extension(), &*version, &*volumes, &*hosts) + .await +} + +#[command(subcommands(self(set_impl(async)), set_dry), display(display_none))] +pub fn set( + #[context] ctx: ExtendedContext, + #[allow(unused_variables)] + #[arg(long = "format")] + format: Option, + #[arg(long = "timeout", parse(parse_duration))] timeout: Option, + #[arg(stdin, parse(parse_stdin_deserializable))] config: Option, + #[arg(rename = "expire-id", long = "expire-id")] expire_id: Option, +) -> Result< + ExtendedContext, Option, Option)>, + Error, +> { + Ok(ctx.map(|id| (id, config, timeout, expire_id))) +} + +#[command(display(display_serializable))] +pub async fn set_dry( + #[context] ctx: ExtendedContext< + EitherContext, + (PackageId, Option, Option, Option), + >, +) -> Result { + let (ctx, (id, config, timeout, _)) = ctx.split(); + let rpc_ctx = ctx.as_rpc().unwrap(); + let mut db = rpc_ctx.db.handle(); + let hosts = crate::db::DatabaseModel::new() + .network() + .hosts() + .get(&mut db) + .await?; + let mut tx = db.begin().await?; + let mut breakages = IndexMap::new(); + configure( + &mut tx, + &rpc_ctx.docker, + &*hosts, + &id, + config, + &timeout, + true, + &mut IndexMap::new(), + &mut breakages, + ) + .await?; + crate::db::DatabaseModel::new() + .package_data() + .idx_model(&id) + .expect(&mut tx) + .await? + .installed() + .expect(&mut tx) + .await? + .status() + .configured() + .put(&mut tx, &true) + .await?; + Ok(BreakageRes { + patch: tx.abort().await?, + breakages, + }) +} + +pub async fn set_impl( + ctx: ExtendedContext< + EitherContext, + (PackageId, Option, Option, Option), + >, +) -> Result, Error> { + let (ctx, (id, config, timeout, expire_id)) = ctx.split(); + let rpc_ctx = ctx.as_rpc().unwrap(); + let mut db = rpc_ctx.db.handle(); + let hosts = crate::db::DatabaseModel::new() + .network() + .hosts() + .get(&mut db) + .await?; + let mut tx = db.begin().await?; + let mut breakages = IndexMap::new(); + configure( + &mut tx, + &rpc_ctx.docker, + &*hosts, + &id, + config, + &timeout, + false, + &mut IndexMap::new(), + &mut breakages, + ) + .await?; + crate::db::DatabaseModel::new() + .package_data() + .idx_model(&id) + .expect(&mut tx) + .await? + .installed() + .expect(&mut tx) + .await? + .status() + .configured() + .put(&mut tx, &true) + .await?; + Ok(WithRevision { + response: (), + revision: tx.commit(expire_id).await?, + }) +} + +pub fn configure<'a, Db: DbHandle>( + db: &'a mut Db, + docker: &'a Docker, + hosts: &'a Hosts, + id: &'a PackageId, + config: Option, + timeout: &'a Option, + dry_run: bool, + overrides: &'a mut IndexMap, + breakages: &'a mut IndexMap, +) -> BoxFuture<'a, Result<(), Error>> { + async move { + // fetch data from db + let pkg_model = crate::db::DatabaseModel::new() + .package_data() + .idx_model(id) + .and_then(|m| m.installed()) + .expect(db) + .await + .with_kind(crate::ErrorKind::NotFound)?; + let action = pkg_model + .clone() + .manifest() + .config() + .get(db) + .await? + .to_owned() + .ok_or_else(|| { + Error::new(anyhow!("{} has no config", id), crate::ErrorKind::NotFound) + })?; + let version = pkg_model.clone().manifest().version().get(db).await?; + let dependencies = pkg_model.clone().manifest().dependencies().get(db).await?; + let volumes = pkg_model.clone().manifest().volumes().get(db).await?; + + // get current config and current spec + let ConfigRes { + config: old_config, + spec, + } = action.get(id, &*version, &*volumes, &*hosts).await?; + + // determine new config to use + let mut config = if let Some(config) = config.or_else(|| old_config.clone()) { + config + } else { + spec.gen(&mut rand::rngs::StdRng::from_entropy(), timeout)? + }; + + spec.matches(&config)?; // check that new config matches spec + spec.update(db, &*overrides, &mut config).await?; // dereference pointers in the new config + + // create backreferences to pointers + let mut sys = pkg_model.clone().system_pointers().get_mut(db).await?; + sys.truncate(0); + let mut current_dependencies: IndexMap = dependencies + .0 + .iter() + .filter_map(|(id, info)| { + if info.optional.is_none() { + Some((id.clone(), CurrentDependencyInfo::default())) } else { None - }; - let mut config = if let Some(cfg) = config { - cfg - } else { - if let Some(old) = &old_config { - old.clone() - } else { - spec.gen(&mut rng, &timeout) - .with_code(crate::error::CFG_SPEC_VIOLATION)? } - }; - spec.matches(&config) - .with_code(crate::error::CFG_SPEC_VIOLATION)?; - spec.update(&mut config) - .await - .with_code(crate::error::CFG_SPEC_VIOLATION)?; - let mut cfgs = LinearMap::new(); - cfgs.insert(name, Cow::Borrowed(&config)); - for rule in rules { - rule.check(&config, &cfgs) - .with_code(crate::error::CFG_RULES_VIOLATION)?; + }) + .collect(); + for ptr in spec.pointers(&config)? { + match ptr { + ValueSpecPointer::Package(PackagePointerSpec { package_id, target }) => { + if let Some(current_dependency) = current_dependencies.get_mut(&package_id) { + current_dependency.pointers.push(target); + } else { + current_dependencies.insert( + package_id, + CurrentDependencyInfo { + pointers: vec![target], + health_checks: IndexSet::new(), + }, + ); + } + } + ValueSpecPointer::System(s) => sys.push(s), } - match old_config { - Some(old) if &old == &config && info.configured && !info.recoverable => { - return Ok(config) + } + sys.save(db).await?; + + let signal = if !dry_run { + // run config action + let res = action + .set(id, &*version, &*dependencies, &*volumes, hosts, &config) + .await?; + + // track dependencies with no pointers + for (package_id, health_checks) in res.depends_on.into_iter() { + if let Some(current_dependency) = current_dependencies.get_mut(&package_id) { + current_dependency.health_checks.extend(health_checks); + } else { + current_dependencies.insert( + package_id, + CurrentDependencyInfo { + pointers: Vec::new(), + health_checks, + }, + ); } - _ => (), - }; - res.changed.insert(name.to_owned(), config.clone()); - for dependent in crate::apps::dependents(name, false).await? { - match configure_rec(&dependent, None, timeout, dry_run, res).await { - Ok(dependent_config) => { - let man = crate::apps::manifest(&dependent).await?; - if let Some(dep_info) = man.dependencies.0.get(name) { - match dep_info - .satisfied( - name, - Some(config.clone()), - &dependent, - &dependent_config, - ) + } + + // track dependency health checks + let mut deps = pkg_model.clone().current_dependencies().get_mut(db).await?; + *deps = current_dependencies.clone(); + deps.save(db).await?; + res.signal + } else { + None + }; + + // update dependencies + for (dependency, dep_info) in current_dependencies { + if let Some(dependency_model) = crate::db::DatabaseModel::new() + .package_data() + .idx_model(&dependency) + .and_then(|pkg| pkg.installed()) + .check(db) + .await? + { + dependency_model + .current_dependents() + .idx_model(id) + .put(db, &dep_info) + .await?; + } + } + + // cache current config for dependents + overrides.insert(id.clone(), config.clone()); + + // handle dependents + let dependents = pkg_model.clone().current_dependents().get(db).await?; + let prev = old_config.map(Value::Object).unwrap_or_default(); + let next = Value::Object(config.clone()); + for (dependent, dep_info) in &*dependents { + fn handle_broken_dependents<'a, Db: DbHandle>( + db: &'a mut Db, + id: &'a PackageId, + dependency: &'a PackageId, + model: InstalledPackageDataEntryModel, + error: DependencyError, + breakages: &'a mut IndexMap, + ) -> BoxFuture<'a, Result<(), Error>> { + async move { + let mut status = model.clone().status().get_mut(db).await?; + + let old = status.dependency_errors.0.remove(id); + let newly_broken = old.is_none(); + status.dependency_errors.0.insert( + id.clone(), + if let Some(old) = old { + old.merge_with(error.clone()) + } else { + error.clone() + }, + ); + if newly_broken { + breakages.insert( + id.clone(), + TaggedDependencyError { + dependency: dependency.clone(), + error: error.clone(), + }, + ); + if status.main.running() { + if model + .clone() + .manifest() + .dependencies() + .idx_model(dependency) + .expect(db) .await? + .get(db) + .await? + .critical { - Ok(_) => (), - Err(e) => { - handle_broken_dependent(name, dependent, dry_run, res, e) + status.main.stop(); + let dependents = model.current_dependents().get(db).await?; + for (dependent, _) in &*dependents { + let dependent_model = crate::db::DatabaseModel::new() + .package_data() + .idx_model(dependent) + .and_then(|pkg| pkg.installed()) + .expect(db) .await?; + handle_broken_dependents( + db, + dependent, + id, + dependent_model, + DependencyError::NotRunning, + breakages, + ) + .await?; } } } } - Err(e) => { - if e.code == Some(crate::error::CFG_RULES_VIOLATION) - || e.code == Some(crate::error::CFG_SPEC_VIOLATION) - { - if !dry_run { - crate::apps::set_configured(&dependent, false).await?; + + status.save(db).await?; + + Ok(()) + } + .boxed() + } + + // check if config passes dependent check + let dependent_model = crate::db::DatabaseModel::new() + .package_data() + .idx_model(dependent) + .and_then(|pkg| pkg.installed()) + .expect(db) + .await?; + if let Some(cfg) = &*dependent_model + .clone() + .manifest() + .dependencies() + .idx_model(id) + .expect(db) + .await? + .config() + .get(db) + .await? + { + let version = dependent_model.clone().manifest().version().get(db).await?; + if let Err(error) = cfg.check(dependent, &*version, &config).await? { + let dep_err = DependencyError::ConfigUnsatisfied { error }; + handle_broken_dependents( + db, + dependent, + id, + dependent_model, + dep_err, + breakages, + ) + .await?; + } + + // handle backreferences + for ptr in &dep_info.pointers { + if let PackagePointerSpecVariant::Config { selector, multi } = ptr { + if selector.select(*multi, &next) != selector.select(*multi, &prev) { + if let Err(e) = configure( + db, docker, hosts, dependent, None, timeout, dry_run, overrides, + breakages, + ) + .await + { + if e.kind == crate::ErrorKind::ConfigRulesViolation { + let dependent_model = crate::db::DatabaseModel::new() + .package_data() + .idx_model(dependent) + .and_then(|pkg| pkg.installed()) + .expect(db) + .await?; + handle_broken_dependents( + db, + dependent, + id, + dependent_model, + DependencyError::ConfigUnsatisfied { + error: format!("{}", e), + }, + breakages, + ) + .await?; + } else { + return Err(e); + } } - handle_broken_dependent( - name, - dependent, - dry_run, - res, - DependencyError::PointerUpdateError(format!("{}", e)), - ) - .await?; - } else { - handle_broken_dependent( - name, - dependent, - dry_run, - res, - DependencyError::Other(format!("{}", e)), - ) - .await?; } } } } - if !dry_run { - let mut file = config_path.write(None).await?; - to_yaml_async_writer(file.as_mut(), &config).await?; - file.commit().await?; - let volume_config = Path::new(crate::VOLUMES) - .join(name) - .join("start9") - .join("config.yaml"); - tokio::fs::copy(config_path.path(), &volume_config) - .await - .with_context(|e| { - format!( - "{}: {} -> {}", - e, - config_path.path().display(), - volume_config.display() - ) - }) - .with_code(crate::error::FILESYSTEM_ERROR)?; - crate::apps::set_configured(name, true).await?; - crate::apps::set_recoverable(name, false).await?; - } - if crate::apps::status(name, false).await?.status != crate::apps::DockerStatus::Stopped - { - if !dry_run { - crate::apps::set_needs_restart(name, true).await?; - } - res.needs_restart.insert(name.to_string()); - } - Ok(config) } - .boxed() - } - let mut res = ConfigurationRes::default(); - configure_rec(name, config, timeout, dry_run, &mut res).await?; - Ok(res) -} -pub async fn remove(name: &str) -> Result<(), crate::Error> { - let config_path = PersistencePath::from_ref("apps") - .join(name) - .join("config.yaml") - .path(); - if config_path.exists() { - tokio::fs::remove_file(&config_path) - .await - .with_context(|e| format!("{}: {}", e, config_path.display())) - .with_code(crate::error::FILESYSTEM_ERROR)?; + if let Some(signal) = signal { + docker + .kill_container( + &DockerAction::container_name(id, &*version), + Some(KillContainerOptions { + signal: signal.to_string(), + }), + ) + .await + // ignore container is not running https://docs.docker.com/engine/api/v1.41/#operation/ContainerKill + .or_else(|e| { + if matches!( + e, + bollard::errors::Error::DockerResponseConflictError { .. } + ) { + Ok(()) + } else { + Err(e) + } + })?; + } + + Ok(()) } - let volume_config = Path::new(crate::VOLUMES) - .join(name) - .join("start9") - .join("config.yaml"); - if volume_config.exists() { - tokio::fs::remove_file(&volume_config) - .await - .with_context(|e| format!("{}: {}", e, volume_config.display())) - .with_code(crate::error::FILESYSTEM_ERROR)?; - } - crate::apps::set_configured(name, false).await?; - Ok(()) + .boxed() } diff --git a/appmgr/src/config/rule_parser.pest b/appmgr/src/config/rule_parser.pest deleted file mode 100644 index 53be53252..000000000 --- a/appmgr/src/config/rule_parser.pest +++ /dev/null @@ -1,76 +0,0 @@ -num = @{ int ~ ("." ~ ASCII_DIGIT*)? ~ (^"e" ~ int)? } - int = @{ ("+" | "-")? ~ ASCII_DIGIT+ } - -raw_string = @{ (!("\\" | "\"") ~ ANY)+ } -predefined = @{ "n" | "r" | "t" | "\\" | "0" | "\"" | "'" } -escape = @{ "\\" ~ predefined } -str = @{ "\"" ~ (raw_string | escape)* ~ "\"" } - -ident_char = @{ ASCII_ALPHANUMERIC | "-" } -sub_ident = _{ sub_ident_regular | sub_ident_index | sub_ident_any | sub_ident_all | sub_ident_fn } - sub_ident_regular = { sub_ident_regular_base | sub_ident_regular_expr } - sub_ident_regular_base = @{ ASCII_ALPHA ~ ident_char* } - sub_ident_regular_expr = ${ "[" ~ str_expr ~ "]" } - sub_ident_index = { sub_ident_index_base | sub_ident_index_expr } - sub_ident_index_base = @{ ASCII_DIGIT+ } - sub_ident_index_expr = ${ "[" ~ num_expr ~ "]" } - sub_ident_any = @{ "*" } - sub_ident_all = @{ "&" } - sub_ident_fn = ${ "[" ~ list_access_function ~ "]"} - list_access_function = _{ list_access_function_first | list_access_function_last | list_access_function_any | list_access_function_all } - list_access_function_first = !{ "first" ~ "(" ~ sub_ident_regular ~ "=>" ~ bool_expr ~ ")" } - list_access_function_last = !{ "last" ~ "(" ~ sub_ident_regular ~ "=>" ~ bool_expr ~ ")" } - list_access_function_any = !{ "any" ~ "(" ~ sub_ident_regular ~ "=>" ~ bool_expr ~ ")" } - list_access_function_all = !{ "all" ~ "(" ~ sub_ident_regular ~ "=>" ~ bool_expr ~ ")" } - -app_id = ${ "[" ~ sub_ident_regular ~ "]" } -ident = _{ (app_id ~ ".")? ~ sub_ident_regular ~ ("." ~ sub_ident)* } -bool_var = ${ ident ~ "?" } -num_var = ${ "#" ~ ident } -str_var = ${ "'" ~ ident } -any_var = ${ ident } - -bool_op = _{ and | or | xor } - and = { "AND" } - or = { "OR" } - xor = { "XOR" } - -num_cmp_op = _{ lt | lte | eq | neq | gt | gte } -str_cmp_op = _{ lt | lte | eq | neq | gt | gte } - lt = { "<" } - lte = { "<=" } - eq = { "=" } - neq = { "!=" } - gt = { ">" } - gte = { ">=" } - -num_op = _{ add | sub | mul | div | pow } -str_op = _{ add } - add = { "+" } - sub = { "-" } - mul = { "*" } - div = { "/" } - pow = { "^" } - -num_expr = !{ num_term ~ (num_op ~ num_term)* } -num_term = _{ num | num_var | "(" ~ num_expr ~ ")" } - -str_expr = !{ str_term ~ (str_op ~ str_term)* } -str_term = _{ str | str_var | "(" ~ str_expr ~ ")" } - -num_cmp_expr = { num_expr ~ num_cmp_op ~ num_expr } -str_cmp_expr = { str_expr ~ str_cmp_op ~ str_expr } - -bool_expr = !{ bool_term ~ (bool_op ~ bool_term)* } -inv_bool_expr = { "!(" ~ bool_expr ~ ")" } -bool_term = _{ bool_var | "(" ~ bool_expr ~ ")" | inv_bool_expr | num_cmp_expr | str_cmp_expr } - -val_expr = _{ any_var | str_expr | num_expr | bool_expr } - -rule = _{ SOI ~ bool_expr ~ EOI } -reference = _{ SOI ~ any_var ~ EOI } -value = _{ SOI ~ val_expr ~ EOI } -del_action = _{ SOI ~ "FROM" ~ any_var ~ "AS" ~ sub_ident_regular ~ "WHERE" ~ bool_expr ~ EOI } -obj_key = _{ SOI ~ sub_ident_regular ~ EOI } - -WHITESPACE = _{ " " | "\t" } \ No newline at end of file diff --git a/appmgr/src/config/rules.rs b/appmgr/src/config/rules.rs deleted file mode 100644 index f64cff1ef..000000000 --- a/appmgr/src/config/rules.rs +++ /dev/null @@ -1,1252 +0,0 @@ -use std::borrow::Cow; -use std::sync::Arc; - -use linear_map::LinearMap; -use pest::iterators::Pairs; -use pest::Parser; -use rand::SeedableRng; - -use super::util::STATIC_NULL; -use super::value::{Config, Value}; - -#[derive(Parser)] -#[grammar = "config/rule_parser.pest"] -struct RuleParser; - -lazy_static::lazy_static! { - static ref NUM_PREC_CLIMBER: pest::prec_climber::PrecClimber = { - use pest::prec_climber::*; - use Rule::*; - use Assoc::*; - - PrecClimber::new(vec![ - Operator::new(add, Left) | Operator::new(sub, Left), - Operator::new(mul, Left) | Operator::new(div, Left), - Operator::new(pow, Right) - ]) - }; - - static ref STR_PREC_CLIMBER: pest::prec_climber::PrecClimber = { - use pest::prec_climber::*; - use Rule::*; - use Assoc::*; - - PrecClimber::new(vec![ - Operator::new(add, Left) - ]) - }; - - static ref BOOL_PREC_CLIMBER: pest::prec_climber::PrecClimber = { - use pest::prec_climber::*; - use Rule::*; - use Assoc::*; - - PrecClimber::new(vec![ - Operator::new(or, Left), - Operator::new(xor, Left), - Operator::new(and, Left) - ]) - }; -} - -pub type Accessor = Box< - dyn for<'a> Fn(&'a Value, &LinearMap<&str, Cow>) -> VarRes<&'a Value> + Send + Sync, ->; -pub type AccessorMut = Box< - dyn for<'a> Fn(&'a mut Value, &LinearMap<&str, Cow>) -> Option<&'a mut Value> - + Send - + Sync, ->; -pub type CompiledExpr = Box>) -> T + Send + Sync>; -pub type CompiledReference = Box< - dyn for<'a> Fn(&'a mut Config, &LinearMap<&str, Cow>) -> Option<&'a mut Value> - + Send - + Sync, ->; -pub type Mutator = Box>) + Send + Sync>; -pub type CompiledRule = Box>) -> bool + Send + Sync>; -pub type CompiledRuleRes = Result; - -#[derive(Clone)] -pub struct ConfigRule { - pub src: String, - pub compiled: Arc, -} -impl std::fmt::Debug for ConfigRule { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ConfigRule") - .field("src", &self.src) - .field("compiled", &"Fn(&Config, &Config) -> bool") - .finish() - } -} -impl<'de> serde::de::Deserialize<'de> for ConfigRule { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - let src = String::deserialize(deserializer)?; - let compiled = compile(&src).map_err(serde::de::Error::custom)?; - Ok(ConfigRule { - src, - compiled: Arc::new(compiled), - }) - } -} -impl serde::ser::Serialize for ConfigRule { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - serializer.serialize_str(&self.src) - } -} -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -pub struct ConfigRuleEntry { - pub rule: ConfigRule, - pub description: String, -} -impl ConfigRuleEntry { - pub fn check( - &self, - cfg: &Config, - cfgs: &LinearMap<&str, Cow>, - ) -> Result<(), failure::Error> { - if !(self.rule.compiled)(cfg, cfgs) { - failure::bail!("{}", self.description); - } - Ok(()) - } -} - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub enum SetVariant { - To(String), - ToValue(Value), - ToEntropy(super::spec::Entropy), -} - -#[derive(Clone)] -pub enum SuggestionVariant { - Set { - var: String, - to: SetVariant, - compiled: Arc, - }, - Delete { - src: String, - compiled: Arc, - }, - Push { - to: String, - value: Value, - compiled: Arc, - }, -} -impl SuggestionVariant { - pub fn apply<'a>( - &self, - id: &'a str, - cfg: &mut Config, - cfgs: &mut LinearMap<&'a str, Cow>, - ) { - match self { - SuggestionVariant::Set { ref compiled, .. } => compiled(cfg, cfgs), - SuggestionVariant::Delete { ref compiled, .. } => compiled(cfg, cfgs), - SuggestionVariant::Push { ref compiled, .. } => compiled(cfg, cfgs), - } - cfgs.insert(id, Cow::Owned(cfg.clone())); - } -} -impl std::fmt::Debug for SuggestionVariant { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SuggestionVariant::Set { - ref var, ref to, .. - } => f - .debug_struct("SuggestionVariant::Set") - .field("var", var) - .field("to", to) - .field("compiled", &"Fn(&mut Config, Config)") - .finish(), - SuggestionVariant::Delete { ref src, .. } => f - .debug_struct("SuggestionVariant::Delete") - .field("src", src) - .field("compiled", &"Fn(&mut Config, Config)") - .finish(), - SuggestionVariant::Push { - ref to, ref value, .. - } => f - .debug_struct("SuggestionVariant::Delete") - .field("to", to) - .field("value", value) - .field("compiled", &"Fn(&mut Config, Config)") - .finish(), - } - } -} -impl<'de> serde::de::Deserialize<'de> for SuggestionVariant { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - #[derive(serde::Deserialize)] - enum _SuggestionVariant { - SET { - var: String, - #[serde(flatten)] - to: SetVariant, - }, - DELETE(String), - PUSH { - to: String, - value: Value, - }, - } - let raw = _SuggestionVariant::deserialize(deserializer)?; - Ok(match raw { - _SuggestionVariant::SET { var, to } => SuggestionVariant::Set { - compiled: Arc::new( - compile_set_action(&var, &to).map_err(serde::de::Error::custom)?, - ), - to: to, - var: var, - }, - _SuggestionVariant::DELETE(src) => SuggestionVariant::Delete { - compiled: Arc::new( - compile_del_action( - RuleParser::parse(Rule::del_action, &src) - .map_err(serde::de::Error::custom)?, - ) - .map_err(serde::de::Error::custom)?, - ), - src, - }, - _SuggestionVariant::PUSH { to, value } => SuggestionVariant::Push { - compiled: Arc::new( - compile_push_action( - RuleParser::parse(Rule::reference, &to) - .map_err(serde::de::Error::custom)?, - value.clone(), - ) - .map_err(serde::de::Error::custom)?, - ), - to, - value, - }, - }) - } -} -impl serde::ser::Serialize for SuggestionVariant { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - #[derive(serde::Serialize)] - enum _SuggestionVariant<'a> { - SET { - var: &'a str, - #[serde(flatten)] - to: &'a SetVariant, - }, - DELETE(&'a str), - PUSH { - to: &'a str, - value: &'a Value, - }, - } - match self { - SuggestionVariant::Set { - ref var, ref to, .. - } => serde::ser::Serialize::serialize(&_SuggestionVariant::SET { var, to }, serializer), - SuggestionVariant::Delete { ref src, .. } => { - serde::ser::Serialize::serialize(&_SuggestionVariant::DELETE(src), serializer) - } - SuggestionVariant::Push { - ref to, ref value, .. - } => serde::ser::Serialize::serialize( - &_SuggestionVariant::PUSH { to, value }, - serializer, - ), - } - } -} -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct Suggestion { - #[serde(rename = "if")] - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(default)] - pub condition: Option, - #[serde(flatten)] - pub variant: SuggestionVariant, -} -impl Suggestion { - pub fn apply<'a>( - &self, - id: &'a str, - cfg: &mut Config, - cfgs: &mut LinearMap<&'a str, Cow>, - ) { - match &self.condition { - Some(condition) if !(condition.compiled)(cfg, cfgs) => (), - _ => self.variant.apply(id, cfg, cfgs), - } - } -} -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ConfigRuleEntryWithSuggestions { - #[serde(flatten)] - pub entry: ConfigRuleEntry, - pub suggestions: Vec, -} -impl ConfigRuleEntryWithSuggestions { - pub fn apply<'a>( - &self, - id: &'a str, - cfg: &mut Config, - cfgs: &mut LinearMap<&'a str, Cow>, - ) -> Result<(), failure::Error> { - if self.entry.check(cfg, cfgs).is_err() { - for suggestion in &self.suggestions { - suggestion.apply(id, cfg, cfgs); - } - self.entry.check(cfg, cfgs) - } else { - Ok(()) - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum VarRes { - Exactly(T), - Any(Vec>), - All(Vec>), -} -impl VarRes { - fn map U>(self, mut f: F) -> VarRes { - fn map_rec U>(s: VarRes, f: &mut F) -> VarRes { - match s { - VarRes::Exactly(a) => VarRes::Exactly(f(a)), - VarRes::Any(a) => VarRes::Any(a.into_iter().map(|a| map_rec(a, f)).collect()), - VarRes::All(a) => VarRes::All(a.into_iter().map(|a| map_rec(a, f)).collect()), - } - } - map_rec(self, &mut f) - } - fn and_then VarRes>(self, mut f: F) -> VarRes { - fn and_then_rec VarRes>(s: VarRes, f: &mut F) -> VarRes { - match s { - VarRes::Exactly(a) => f(a), - VarRes::Any(a) => VarRes::Any(a.into_iter().map(|a| and_then_rec(a, f)).collect()), - VarRes::All(a) => VarRes::All(a.into_iter().map(|a| and_then_rec(a, f)).collect()), - } - } - and_then_rec(self, &mut f) - } -} -impl VarRes { - fn resolve(self) -> bool { - match self { - VarRes::Exactly(a) => a, - VarRes::Any(a) => a.into_iter().any(|a| a.resolve()), - VarRes::All(a) => a.into_iter().all(|a| a.resolve()), - } - } -} - -fn compile_var_rec(mut ident: Pairs) -> Option { - let idx = ident.next(); - if let Some(idx) = idx { - let deref: Accessor = match idx.as_rule() { - Rule::sub_ident_any => Box::new(|v, _| match v { - Value::List(l) => VarRes::Any(l.iter().map(VarRes::Exactly).collect()), - Value::Object(o) => { - VarRes::Any(o.0.iter().map(|(_, a)| VarRes::Exactly(a)).collect()) - } - _ => VarRes::Exactly(&STATIC_NULL), - }), - Rule::sub_ident_all => Box::new(|v, _| match v { - Value::List(l) => VarRes::All(l.iter().map(VarRes::Exactly).collect()), - Value::Object(o) => { - VarRes::All(o.0.iter().map(|(_, a)| VarRes::Exactly(a)).collect()) - } - _ => VarRes::Exactly(&STATIC_NULL), - }), - Rule::sub_ident_fn => { - let idx = idx.into_inner().next().unwrap(); - match idx.as_rule() { - Rule::list_access_function_first => { - let mut pred_iter = idx.into_inner(); - let item_var = pred_iter.next().unwrap().as_str().to_owned(); - let predicate = compile_bool_expr(pred_iter.next().unwrap().into_inner()); - Box::new(move |v, cfgs| match v { - Value::List(l) => VarRes::Exactly( - l.iter() - .filter(|item| { - let mut cfg = Config::default(); - cfg.0.insert(item_var.clone(), (*item).clone()); - predicate(&cfg, cfgs) - }) - .next() - .unwrap_or(&STATIC_NULL), - ), - Value::Object(o) => VarRes::Exactly( - o.0.iter() - .map(|(_, item)| item) - .filter(|item| { - let mut cfg = Config::default(); - cfg.0.insert(item_var.clone(), (*item).clone()); - predicate(&cfg, cfgs) - }) - .next() - .unwrap_or(&STATIC_NULL), - ), - _ => VarRes::Exactly(&STATIC_NULL), - }) - } - Rule::list_access_function_last => { - let mut pred_iter = idx.into_inner(); - let item_var = pred_iter.next().unwrap().as_str().to_owned(); - let predicate = compile_bool_expr(pred_iter.next().unwrap().into_inner()); - Box::new(move |v, cfgs| match v { - Value::List(l) => VarRes::Exactly( - l.iter() - .filter(|item| { - let mut cfg = Config::default(); - cfg.0.insert(item_var.clone(), (*item).clone()); - predicate(&cfg, cfgs) - }) - .next_back() - .unwrap_or(&STATIC_NULL), - ), - Value::Object(o) => VarRes::Exactly( - o.0.iter() - .map(|(_, item)| item) - .filter(|item| { - let mut cfg = Config::default(); - cfg.0.insert(item_var.clone(), (*item).clone()); - predicate(&cfg, cfgs) - }) - .next_back() - .unwrap_or(&STATIC_NULL), - ), - _ => VarRes::Exactly(&STATIC_NULL), - }) - } - Rule::list_access_function_any => { - let mut pred_iter = idx.into_inner(); - let item_var = pred_iter.next().unwrap().as_str().to_owned(); - let predicate = compile_bool_expr(pred_iter.next().unwrap().into_inner()); - Box::new(move |v, cfgs| match v { - Value::List(l) => VarRes::Any( - l.iter() - .filter(|item| { - let mut cfg = Config::default(); - cfg.0.insert(item_var.clone(), (*item).clone()); - predicate(&cfg, cfgs) - }) - .map(VarRes::Exactly) - .collect(), - ), - Value::Object(o) => VarRes::Any( - o.0.iter() - .map(|(_, item)| item) - .filter(|item| { - let mut cfg = Config::default(); - cfg.0.insert(item_var.clone(), (*item).clone()); - predicate(&cfg, cfgs) - }) - .map(VarRes::Exactly) - .collect(), - ), - _ => VarRes::Exactly(&STATIC_NULL), - }) - } - Rule::list_access_function_all => { - let mut pred_iter = idx.into_inner(); - let item_var = pred_iter.next().unwrap().as_str().to_owned(); - let predicate = compile_bool_expr(pred_iter.next().unwrap().into_inner()); - Box::new(move |v, cfgs| match v { - Value::List(l) => VarRes::All( - l.iter() - .filter(|item| { - let mut cfg = Config::default(); - cfg.0.insert(item_var.clone(), (*item).clone()); - predicate(&cfg, cfgs) - }) - .map(VarRes::Exactly) - .collect(), - ), - Value::Object(o) => VarRes::All( - o.0.iter() - .map(|(_, item)| item) - .filter(|item| { - let mut cfg = Config::default(); - cfg.0.insert(item_var.clone(), (*item).clone()); - predicate(&cfg, cfgs) - }) - .map(VarRes::Exactly) - .collect(), - ), - _ => VarRes::Exactly(&STATIC_NULL), - }) - } - _ => unreachable!(), - } - } - Rule::sub_ident_regular => { - let idx = idx.into_inner().next().unwrap(); - match idx.as_rule() { - Rule::sub_ident_regular_base => { - let idx = idx.as_str().to_owned(); - Box::new(move |v, _| match v { - Value::Object(o) => { - VarRes::Exactly(o.0.get(&idx).unwrap_or(&STATIC_NULL)) - } - _ => VarRes::Exactly(&STATIC_NULL), - }) - } - Rule::sub_ident_regular_expr => { - let idx = compile_str_expr(idx.into_inner().next().unwrap().into_inner()); - Box::new(move |v, dep_cfg| match v { - Value::Object(o) => idx(&Config::default(), dep_cfg).map(|idx| { - idx.and_then(|idx| o.0.get(&idx)).unwrap_or(&STATIC_NULL) - }), - _ => VarRes::Exactly(&STATIC_NULL), - }) - } - _ => unreachable!(), - } - } - Rule::sub_ident_index => { - let idx = idx.into_inner().next().unwrap(); - match idx.as_rule() { - Rule::sub_ident_index_base => { - let idx: usize = idx.as_str().parse().unwrap(); - Box::new(move |v, _| match v { - Value::List(l) => VarRes::Exactly(l.get(idx).unwrap_or(&STATIC_NULL)), - _ => VarRes::Exactly(&STATIC_NULL), - }) - } - Rule::sub_ident_index_expr => { - let idx = compile_num_expr(idx.into_inner().next().unwrap().into_inner()); - Box::new(move |v, dep_cfg| match v { - Value::List(l) => idx(&Config::default(), dep_cfg) - .map(|idx| l.get(idx as usize).unwrap_or(&STATIC_NULL)), - _ => VarRes::Exactly(&STATIC_NULL), - }) - } - _ => unreachable!(), - } - } - _ => unreachable!(), - }; - Some(if let Some(rest) = compile_var_rec(ident) { - Box::new(move |v, cfgs| deref(v, cfgs).and_then(|v| rest(v, cfgs))) - } else { - deref - }) - } else { - None - } -} - -fn compile_var(mut var: Pairs) -> CompiledExpr> { - let mut first_seg = var.next().unwrap(); - let app_id = if first_seg.as_rule() == Rule::app_id { - let app_id = first_seg.into_inner().next().unwrap().as_str().to_owned(); - first_seg = var.next().unwrap(); - Some(app_id) - } else { - None - }; - let first_seg_string = first_seg.as_str().to_owned(); - let accessor = compile_var_rec(var); - Box::new(move |cfg, cfgs| { - let mut cfg: &Config = cfg; - if let Some(ref app_id) = app_id { - cfg = if let Some(cfg) = cfgs.get(&app_id.as_str()) { - cfg - } else { - return VarRes::Exactly(Value::Null); - }; - } - let val = cfg.0.get(&first_seg_string).unwrap_or(&STATIC_NULL); - if let Some(accessor) = &accessor { - accessor(val, cfgs).map(|v| v.clone()) - } else { - VarRes::Exactly(val.clone()) - } - }) -} - -fn compile_var_mut_rec(mut ident: Pairs) -> Result, failure::Error> { - let idx = ident.next(); - Ok(if let Some(idx) = idx { - let deref: AccessorMut = match idx.as_rule() { - Rule::sub_ident_fn => { - let idx = idx.into_inner().next().unwrap(); - match idx.as_rule() { - Rule::list_access_function_first => { - let mut pred_iter = idx.into_inner(); - let item_var = pred_iter.next().unwrap().as_str().to_owned(); - let predicate = compile_bool_expr(pred_iter.next().unwrap().into_inner()); - Box::new(move |v, cfgs| match v { - Value::List(l) => l - .iter_mut() - .filter(|item| { - let mut cfg = Config::default(); - cfg.0.insert(item_var.clone(), (*item).clone()); - predicate(&cfg, cfgs) - }) - .next(), - Value::Object(o) => { - o.0.iter_mut() - .map(|(_, item)| item) - .filter(|item| { - let mut cfg = Config::default(); - cfg.0.insert(item_var.clone(), (*item).clone()); - predicate(&cfg, cfgs) - }) - .next() - } - _ => None, - }) - } - Rule::list_access_function_last => { - let mut pred_iter = idx.into_inner(); - let item_var = pred_iter.next().unwrap().as_str().to_owned(); - let predicate = compile_bool_expr(pred_iter.next().unwrap().into_inner()); - Box::new(move |v, cfgs| match v { - Value::List(l) => l - .iter_mut() - .filter(|item| { - let mut cfg = Config::default(); - cfg.0.insert(item_var.clone(), (*item).clone()); - predicate(&cfg, cfgs) - }) - .next_back(), - Value::Object(o) => { - o.0.iter_mut() - .map(|(_, item)| item) - .filter(|item| { - let mut cfg = Config::default(); - cfg.0.insert(item_var.clone(), (*item).clone()); - predicate(&cfg, cfgs) - }) - .next_back() - } - _ => None, - }) - } - Rule::list_access_function_any | Rule::list_access_function_all => { - failure::bail!("Any and All are immutable") - } - _ => unreachable!(), - } - } - Rule::sub_ident_regular => { - let idx = idx.into_inner().next().unwrap(); - match idx.as_rule() { - Rule::sub_ident_regular_base => { - let idx = idx.as_str().to_owned(); - Box::new(move |v, _| match v { - Value::Object(ref mut o) => { - if o.0.contains_key(&idx) { - o.0.get_mut(&idx) - } else { - o.0.insert(idx.clone(), Value::Null); - o.0.get_mut(&idx) - } - } - _ => None, - }) - } - Rule::sub_ident_regular_expr => { - let idx = compile_str_expr(idx.into_inner().next().unwrap().into_inner()); - Box::new( - move |v, dep_cfg| match (v, idx(&Config::default(), dep_cfg)) { - (Value::Object(ref mut o), VarRes::Exactly(Some(ref idx))) => { - if o.0.contains_key(idx) { - o.0.get_mut(idx) - } else { - o.0.insert(idx.clone(), Value::Null); - o.0.get_mut(idx) - } - } - _ => None, - }, - ) - } - _ => unreachable!(), - } - } - Rule::sub_ident_index => { - let idx = idx.into_inner().next().unwrap(); - match idx.as_rule() { - Rule::sub_ident_index_base => { - let idx: usize = idx.as_str().parse().unwrap(); - Box::new(move |v, _| match v { - Value::List(l) => { - if l.len() > idx { - l.get_mut(idx) - } else if idx == l.len() { - l.push(Value::Null); - l.get_mut(idx) - } else { - None - } - } - _ => None, - }) - } - Rule::sub_ident_index_expr => { - let idx = compile_num_expr(idx.into_inner().next().unwrap().into_inner()); - Box::new( - move |v, dep_cfg| match (v, idx(&Config::default(), dep_cfg)) { - (Value::List(l), VarRes::Exactly(idx)) => { - let idx = idx as usize; - if l.len() > idx { - l.get_mut(idx) - } else if idx == l.len() { - l.push(Value::Null); - l.get_mut(idx) - } else { - None - } - } - _ => None, - }, - ) - } - _ => unreachable!(), - } - } - _ => failure::bail!("invalid token: {:?}", idx.as_rule()), - }; - Some(if let Some(rest) = compile_var_mut_rec(ident)? { - Box::new(move |v, cfgs| deref(v, cfgs).and_then(|v| rest(v, cfgs))) - } else { - deref - }) - } else { - None - }) -} - -fn compile_var_mut(mut var: Pairs) -> Result { - let first_seg = var.next().unwrap(); - if first_seg.as_rule() == Rule::app_id { - failure::bail!("Can only assign to relative path"); - } - let first_seg_string = first_seg.as_str().to_owned(); - let accessor_mut = compile_var_mut_rec(var)?; - Ok(Box::new(move |cfg, cfgs| { - let var = if cfg.0.contains_key(&first_seg_string) { - cfg.0.get_mut(&first_seg_string).unwrap() - } else { - cfg.0.insert(first_seg_string.clone(), Value::Null); - cfg.0.get_mut(&first_seg_string).unwrap() - }; - if let Some(accessor_mut) = &accessor_mut { - accessor_mut(var, cfgs) - } else { - Some(var) - } - })) -} - -fn compile_bool_var(var: Pairs) -> CompiledRule { - let var = compile_var(var); - Box::new(move |cfg, cfgs| { - var(cfg, cfgs) - .map(|a| match a { - Value::Bool(false) | Value::Null => false, - _ => true, - }) - .resolve() - }) -} - -fn compile_num_var(var: Pairs) -> CompiledExpr> { - let var = compile_var(var); - Box::new(move |cfg, cfgs| { - var(cfg, cfgs).map(|a| match a { - Value::Number(n) => n, - Value::String(s) => match s.parse() { - Ok(n) => n, - Err(_) => std::f64::NAN, - }, - Value::Bool(b) => { - if b { - 1.0 - } else { - 0.0 - } - } - _ => std::f64::NAN, - }) - }) -} - -fn compile_num(num_str: &str) -> CompiledExpr> { - let num = VarRes::Exactly(num_str.parse().unwrap()); - Box::new(move |_, _| num.clone()) -} - -fn compile_num_expr(pairs: Pairs) -> CompiledExpr> { - NUM_PREC_CLIMBER.climb( - pairs, - |pair| match pair.as_rule() { - Rule::num_var => compile_num_var(pair.into_inner()), - Rule::num => compile_num(pair.as_str()), - Rule::num_expr => compile_num_expr(pair.into_inner()), - _ => unreachable!(), - }, - |lhs, op, rhs| match op.as_rule() { - Rule::add => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs).and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs + rhs)) - }), - Rule::sub => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs).and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs - rhs)) - }), - Rule::mul => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs).and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs * rhs)) - }), - Rule::div => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs).and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs / rhs)) - }), - Rule::pow => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs).and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs.powf(rhs))) - }), - _ => unreachable!(), - }, - ) -} - -fn compile_num_cmp_expr(mut pairs: Pairs) -> CompiledRule { - let lhs = compile_num_expr(pairs.next().unwrap().into_inner()); - let op = pairs.next().unwrap(); - let rhs = compile_num_expr(pairs.next().unwrap().into_inner()); - match op.as_rule() { - Rule::lt => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs) - .and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs < rhs)) - .resolve() - }), - Rule::lte => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs) - .and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs <= rhs)) - .resolve() - }), - Rule::eq => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs) - .and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs == rhs)) - .resolve() - }), - Rule::neq => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs) - .and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs != rhs)) - .resolve() - }), - Rule::gt => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs) - .and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs > rhs)) - .resolve() - }), - Rule::gte => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs) - .and_then(|lhs| rhs(cfg, cfgs).map(|rhs| lhs >= rhs)) - .resolve() - }), - _ => unreachable!(), - } -} - -fn compile_str_var(var: Pairs) -> CompiledExpr>> { - let var = compile_var(var); - Box::new(move |cfg, cfgs| { - var(cfg, cfgs).map(|a| match a { - Value::String(s) => Some(s), - Value::Number(n) => Some(format!("{}", n)), - Value::Bool(b) => Some(format!("{}", b)), - _ => None, - }) - }) -} - -fn compile_str(str_str: &str) -> CompiledExpr>> { - let str_str = &str_str[1..str_str.len() - 1]; - let mut out = String::with_capacity(str_str.len()); - let mut escape = false; - for c in str_str.chars() { - match c { - '\\' => { - if escape { - out.push('\\'); - } else { - escape = true; - } - } - 'n' if escape => out.push('\n'), - 'r' if escape => out.push('\r'), - 't' if escape => out.push('\t'), - '0' if escape => out.push('\0'), - '"' if escape => out.push('"'), - '\'' if escape => out.push('\''), - _ => { - if escape { - out.push('\\') - } - out.push(c) - } - } - } - let res = VarRes::Exactly(Some(out)); - Box::new(move |_, _| res.clone()) -} - -fn compile_str_expr(pairs: Pairs) -> CompiledExpr>> { - STR_PREC_CLIMBER.climb( - pairs, - |pair| match pair.as_rule() { - Rule::str_var => compile_str_var(pair.into_inner()), - Rule::str => compile_str(pair.as_str()), - Rule::str_expr => compile_str_expr(pair.into_inner()), - _ => unreachable!(), - }, - |lhs, op, rhs| match op.as_rule() { - Rule::add => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs).and_then(|lhs| { - rhs(cfg, cfgs).map(|rhs| { - let lhs = lhs.clone()?; - let rhs = rhs?; - Some(lhs + &rhs) - }) - }) - }), - _ => unreachable!(), - }, - ) -} - -fn compile_str_cmp_expr(mut pairs: Pairs) -> CompiledRule { - let lhs = compile_str_expr(pairs.next().unwrap().into_inner()); - let op = pairs.next().unwrap(); - let rhs = compile_str_expr(pairs.next().unwrap().into_inner()); - match op.as_rule() { - Rule::lt => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs) - .and_then(|lhs| { - rhs(cfg, cfgs).map(|rhs| match (&lhs, &rhs) { - (Some(lhs), Some(rhs)) => rhs.contains(lhs) && lhs.len() < rhs.len(), - _ => false, - }) - }) - .resolve() - }), - Rule::lte => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs) - .and_then(|lhs| { - rhs(cfg, cfgs).map(|rhs| match (&lhs, &rhs) { - (Some(lhs), Some(rhs)) => rhs.contains(lhs), - _ => false, - }) - }) - .resolve() - }), - Rule::eq => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs) - .and_then(|lhs| { - rhs(cfg, cfgs).map(|rhs| match (&lhs, &rhs) { - (Some(lhs), Some(rhs)) => lhs == rhs, - (None, None) => true, - _ => false, - }) - }) - .resolve() - }), - Rule::neq => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs) - .and_then(|lhs| { - rhs(cfg, cfgs).map(|rhs| match (&lhs, &rhs) { - (Some(lhs), Some(rhs)) => lhs != rhs, - (None, None) => false, - _ => true, - }) - }) - .resolve() - }), - Rule::gt => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs) - .and_then(|lhs| { - rhs(cfg, cfgs).map(|rhs| match (&lhs, &rhs) { - (Some(lhs), Some(rhs)) => lhs.contains(rhs) && lhs.len() > rhs.len(), - _ => true, - }) - }) - .resolve() - }), - Rule::gte => Box::new(move |cfg, cfgs| { - lhs(cfg, cfgs) - .and_then(|lhs| { - rhs(cfg, cfgs).map(|rhs| match (&lhs, &rhs) { - (Some(lhs), Some(rhs)) => lhs.contains(rhs), - _ => true, - }) - }) - .resolve() - }), - _ => unreachable!(), - } -} - -fn compile_inv_bool_expr(mut pairs: Pairs) -> CompiledRule { - let expr = compile_bool_expr(pairs.next().unwrap().into_inner()); - Box::new(move |cfg, cfgs| !expr(cfg, cfgs)) -} - -fn compile_bool_expr(pairs: Pairs) -> CompiledRule { - BOOL_PREC_CLIMBER.climb( - pairs, - |pair| match pair.as_rule() { - Rule::bool_var => compile_bool_var(pair.into_inner()), - Rule::bool_expr => compile_bool_expr(pair.into_inner()), - Rule::inv_bool_expr => compile_inv_bool_expr(pair.into_inner()), - Rule::num_cmp_expr => compile_num_cmp_expr(pair.into_inner()), - Rule::str_cmp_expr => compile_str_cmp_expr(pair.into_inner()), - _ => unreachable!(), - }, - |lhs, op, rhs| -> CompiledRule { - match op.as_rule() { - Rule::and => Box::new(move |cfg, cfgs| lhs(cfg, cfgs) && rhs(cfg, cfgs)), - Rule::or => Box::new(move |cfg, cfgs| lhs(cfg, cfgs) || rhs(cfg, cfgs)), - Rule::xor => Box::new(move |cfg, cfgs| lhs(cfg, cfgs) ^ rhs(cfg, cfgs)), - _ => unreachable!(), - } - }, - ) -} - -fn compile_value_expr(mut pairs: Pairs) -> CompiledExpr> { - let expr = pairs.next().unwrap(); - match expr.as_rule() { - Rule::any_var => compile_var(expr.into_inner()), - Rule::str_expr => { - let expr = compile_str_expr(expr.into_inner()); - Box::new(move |cfg, cfgs| { - expr(cfg, cfgs).map(|s| s.map(Value::String).unwrap_or(Value::Null)) - }) - } - Rule::num_expr => { - let expr = compile_num_expr(expr.into_inner()); - Box::new(move |cfg, cfgs| expr(cfg, cfgs).map(Value::Number)) - } - Rule::bool_expr => { - let expr = compile_bool_expr(expr.into_inner()); - Box::new(move |cfg, cfgs| VarRes::Exactly(expr(cfg, cfgs)).map(Value::Bool)) - } - _ => unreachable!(), - } -} - -fn compile_del_action(mut pairs: Pairs) -> Result { - let list_mut = compile_var_mut(pairs.next().unwrap().into_inner())?; - let var = pairs.next().unwrap().as_str().to_owned(); - let predicate = compile_bool_expr(pairs.next().unwrap().into_inner()); - Ok(Box::new(move |cfg, cfgs| match (&list_mut)(cfg, cfgs) { - Some(Value::List(ref mut l)) => { - *l = std::mem::take(l) - .into_iter() - .filter(|item| { - let mut obj = Config::default(); - obj.0.insert(var.clone(), item.clone()); - !predicate(&obj, cfgs) - }) - .collect(); - } - Some(Value::Object(Config(ref mut o))) => { - *o = std::mem::take(o) - .into_iter() - .filter(|(_, item)| { - let mut obj = Config::default(); - obj.0.insert(var.clone(), item.clone()); - !predicate(&obj, cfgs) - }) - .collect(); - } - _ => return, - })) -} - -fn compile_push_action(mut pairs: Pairs, value: Value) -> Result { - let list_mut = compile_var_mut(pairs.next().unwrap().into_inner())?; - Ok(Box::new(move |cfg, cfgs| { - let vec = match (&list_mut)(cfg, cfgs) { - Some(Value::List(ref mut a)) => a, - _ => return, - }; - vec.push(value.clone()) - })) -} - -fn compile_set_action(var: &str, to: &SetVariant) -> Result { - let mut var = RuleParser::parse(Rule::reference, var)?; - let get_mut = compile_var_mut(var.next().unwrap().into_inner())?; - Ok(match to { - SetVariant::To(expr) => { - let expr = compile_expr(&expr)?; - Box::new(move |cfg, cfgs| { - let val = expr(cfg, cfgs); - if let Some(var) = get_mut(cfg, cfgs) { - *var = val; - } - }) - } - SetVariant::ToValue(val) => { - let val = val.clone(); - Box::new(move |cfg, cfgs| { - if let Some(var) = get_mut(cfg, cfgs) { - *var = val.clone() - } - }) - } - SetVariant::ToEntropy(entropy) => { - let entropy = entropy.clone(); - Box::new(move |cfg, cfgs| { - if let Some(var) = get_mut(cfg, cfgs) { - *var = Value::String(entropy.gen(&mut rand::rngs::StdRng::from_entropy())); - } - }) - } - }) -} - -pub fn validate_key(key: &str) -> Result<(), pest::error::Error> { - RuleParser::parse(Rule::obj_key, key)?; - Ok(()) -} - -pub fn parse_and) -> T>( - rule: &str, - f: F, -) -> Result> { - let mut parsed = RuleParser::parse(Rule::rule, rule)?; - let pairs = parsed.next().unwrap().into_inner(); - Ok(f(pairs)) -} - -pub fn compile(rule: &str) -> Result { - parse_and(rule, compile_bool_expr).map_err(From::from) -} - -pub fn compile_expr(expr: &str) -> Result, failure::Error> { - let compiled = compile_value_expr(RuleParser::parse(Rule::value, expr)?); - Ok(Box::new(move |cfg, cfgs| match compiled(cfg, cfgs) { - VarRes::Exactly(v) => v, - _ => Value::Null, - })) -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_compile_str() { - assert_eq!( - compile_str("\"foo\"")(&Default::default(), &Default::default()), - VarRes::Exactly(Some("foo".to_owned())) - ); - } - - #[test] - fn test_access_expr() { - let mut cfg = Config::default(); - let mut cfgs = LinearMap::new(); - let mut foo = Config::default(); - foo.0.insert("bar!\"".to_owned(), Value::Number(3.0)); - cfg.0.insert( - "foo".to_owned(), - Value::List(vec![Value::Null, Value::Object(foo), Value::Number(3.0)]), - ); - cfgs.insert("my-app", Cow::Borrowed(&cfg)); - assert!((compile("#[my-app].foo.1.[\"ba\" + \"r!\\\"\"] = 3") - .map_err(|e| eprintln!("{}", e)) - .expect("compile failed"))(&cfg, &cfgs)); - assert!((compile("#[my-app].foo.[0 + 1].[\"bar!\\\"\"] = 3") - .map_err(|e| eprintln!("{}", e)) - .expect("compile failed"))(&cfg, &cfgs)); - } - - #[test] - fn test_any_all() { - let mut cfg = Config::default(); - let mut cfgs = LinearMap::new(); - let mut foo = Config::default(); - foo.0.insert("bar".to_owned(), Value::Number(3.0)); - cfg.0.insert( - "foo".to_owned(), - Value::List(vec![Value::Null, Value::Object(foo), Value::Number(3.0)]), - ); - cfgs.insert("my-app", Cow::Borrowed(&cfg)); - assert!((compile("#[my-app].foo.*.bar = 3") - .map_err(|e| eprintln!("{}", e)) - .expect("compile failed"))(&cfg, &cfgs)); - assert!(!(compile("#[my-app].foo.&.bar = 3") - .map_err(|e| eprintln!("{}", e)) - .expect("compile failed"))(&cfg, &cfgs)); - } - - #[test] - fn test_first_last() { - let mut cfg = Config::default(); - let mut cfgs = LinearMap::new(); - let mut foo = Config::default(); - foo.0.insert("bar".to_owned(), Value::Number(3.0)); - foo.0.insert("baz".to_owned(), Value::Number(4.0)); - let mut qux = Config::default(); - qux.0.insert("bar".to_owned(), Value::Number(7.0)); - qux.0.insert("baz".to_owned(), Value::Number(4.0)); - cfg.0.insert( - "foo".to_owned(), - Value::List(vec![ - Value::Null, - Value::Object(foo), - Value::Object(qux), - Value::Number(3.0), - ]), - ); - cfgs.insert("my-app", Cow::Borrowed(&cfg)); - assert!((compile("#foo.[first(item => #item.baz = 4)].bar = 3") - .map_err(|e| eprintln!("{}", e)) - .expect("compile failed"))(&cfg, &cfgs)); - assert!((compile("#foo.[last(item => #item.baz = 4)].bar = 7") - .map_err(|e| eprintln!("{}", e)) - .expect("compile failed"))(&cfg, &cfgs)); - } - - #[test] - fn test_app_id() { - let mut dependent_cfg = Config::default(); - let mut dependency_cfg = Config::default(); - let mut cfgs = LinearMap::new(); - dependent_cfg - .0 - .insert("foo".to_owned(), Value::String("bar".to_owned())); - dependency_cfg - .0 - .insert("foo".to_owned(), Value::String("bar!".to_owned())); - cfgs.insert("my-dependent", Cow::Borrowed(&dependent_cfg)); - cfgs.insert("my-dependency", Cow::Borrowed(&dependency_cfg)); - assert!((compile("'foo = '[my-dependent].foo + \"!\"") - .map_err(|e| eprintln!("{}", e)) - .expect("compile failed"))( - &dependency_cfg, &cfgs - )) - } -} diff --git a/appmgr/src/config/spec.rs b/appmgr/src/config/spec.rs index c5af97524..2bbb10fdb 100644 --- a/appmgr/src/config/spec.rs +++ b/appmgr/src/config/spec.rs @@ -6,18 +6,21 @@ use std::sync::Arc; use std::time::Duration; use async_trait::async_trait; +use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; -use linear_map::{set::LinearSet, LinearMap}; +use jsonpath_lib::Compiled as CompiledJsonPath; +use patch_db::{DbHandle, OptionModel}; use rand::{CryptoRng, Rng}; use regex::Regex; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::{Number, Value}; use super::util::{self, CharSet, NumRange, UniqueBy, STATIC_NULL}; -use super::value::{Config, Value}; -use super::{MatchError, NoMatchWithPath, TimeoutError}; - +use super::{Config, MatchError, NoMatchWithPath, TimeoutError, TypeOf}; use crate::config::ConfigurationError; -use crate::manifest::ManifestLatest; -use crate::util::PersistencePath; +use crate::id::InterfaceId; +use crate::s9pk::manifest::{Manifest, PackageId}; +use crate::Error; // Config Value Specifications #[async_trait] @@ -26,12 +29,19 @@ pub trait ValueSpec { // consistent with the spec in &self fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath>; // This function checks whether the value spec is consistent with itself, - // since not all invariants can be checked by the type - fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath>; + // since not all inVariant can be checked by the type + fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath>; // update is to fill in values for environment pointers recursively - async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError>; + async fn update( + &self, + db: &mut Db, + config_overrides: &IndexMap, + value: &mut Value, + ) -> Result<(), ConfigurationError>; + // returns all pointers that are live in the provided config + fn pointers(&self, value: &Value) -> Result, NoMatchWithPath>; // requires returns whether the app id is the target of a pointer within it - fn requires(&self, id: &str, value: &Value) -> bool; + fn requires(&self, id: &PackageId, value: &Value) -> bool; // defines if 2 values of this type are equal for the purpose of uniqueness fn eq(&self, lhs: &Value, rhs: &Value) -> bool; } @@ -52,7 +62,7 @@ pub trait ValueSpec { // and 'HasDefaultSpec'. pub trait DefaultableWith { type DefaultSpec: Sync; - type Error: failure::Fail; + type Error: std::error::Error; fn gen_with( &self, @@ -77,7 +87,7 @@ pub trait Defaultable { impl Defaultable for T where T: HasDefaultSpec + DefaultableWith + Sync, - E: failure::Fail, + E: std::error::Error, { type Error = E; @@ -92,7 +102,7 @@ where // WithDefault - trivial wrapper that pairs a 'DefaultableWith' type with a // default spec -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct WithDefault { #[serde(flatten)] pub inner: T, @@ -133,13 +143,21 @@ where fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { self.inner.matches(value) } - fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { self.inner.validate(manifest) } - async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { - self.inner.update(value).await + async fn update( + &self, + db: &mut Db, + config_overrides: &IndexMap, + value: &mut Value, + ) -> Result<(), ConfigurationError> { + self.inner.update(db, config_overrides, value).await } - fn requires(&self, id: &str, value: &Value) -> bool { + fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { + self.inner.pointers(value) + } + fn requires(&self, id: &PackageId, value: &Value) -> bool { self.inner.requires(id, value) } fn eq(&self, lhs: &Value, rhs: &Value) -> bool { @@ -147,7 +165,7 @@ where } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct WithNullable { #[serde(flatten)] pub inner: T, @@ -165,13 +183,21 @@ where _ => self.inner.matches(value), } } - fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { self.inner.validate(manifest) } - async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { - self.inner.update(value).await + async fn update( + &self, + db: &mut Db, + config_overrides: &IndexMap, + value: &mut Value, + ) -> Result<(), ConfigurationError> { + self.inner.update(db, config_overrides, value).await } - fn requires(&self, id: &str, value: &Value) -> bool { + fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { + self.inner.pointers(value) + } + fn requires(&self, id: &PackageId, value: &Value) -> bool { self.inner.requires(id, value) } fn eq(&self, lhs: &Value, rhs: &Value) -> bool { @@ -211,7 +237,7 @@ where } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WithDescription { #[serde(flatten)] @@ -230,13 +256,21 @@ where fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { self.inner.matches(value) } - fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { self.inner.validate(manifest) } - async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { - self.inner.update(value).await + async fn update( + &self, + db: &mut Db, + config_overrides: &IndexMap, + value: &mut Value, + ) -> Result<(), ConfigurationError> { + self.inner.update(db, config_overrides, value).await } - fn requires(&self, id: &str, value: &Value) -> bool { + fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { + self.inner.pointers(value) + } + fn requires(&self, id: &PackageId, value: &Value) -> bool { self.inner.requires(id, value) } fn eq(&self, lhs: &Value, rhs: &Value) -> bool { @@ -276,7 +310,7 @@ where } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] #[serde(tag = "type")] pub enum ValueSpecAny { @@ -323,7 +357,7 @@ impl ValueSpec for ValueSpecAny { ValueSpecAny::Pointer(a) => a.matches(value), } } - fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { match self { ValueSpecAny::Boolean(a) => a.validate(manifest), ValueSpecAny::Enum(a) => a.validate(manifest), @@ -335,19 +369,36 @@ impl ValueSpec for ValueSpecAny { ValueSpecAny::Pointer(a) => a.validate(manifest), } } - async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + async fn update( + &self, + db: &mut Db, + config_overrides: &IndexMap, + value: &mut Value, + ) -> Result<(), ConfigurationError> { match self { - ValueSpecAny::Boolean(a) => a.update(value).await, - ValueSpecAny::Enum(a) => a.update(value).await, - ValueSpecAny::List(a) => a.update(value).await, - ValueSpecAny::Number(a) => a.update(value).await, - ValueSpecAny::Object(a) => a.update(value).await, - ValueSpecAny::String(a) => a.update(value).await, - ValueSpecAny::Union(a) => a.update(value).await, - ValueSpecAny::Pointer(a) => a.update(value).await, + ValueSpecAny::Boolean(a) => a.update(db, config_overrides, value).await, + ValueSpecAny::Enum(a) => a.update(db, config_overrides, value).await, + ValueSpecAny::List(a) => a.update(db, config_overrides, value).await, + ValueSpecAny::Number(a) => a.update(db, config_overrides, value).await, + ValueSpecAny::Object(a) => a.update(db, config_overrides, value).await, + ValueSpecAny::String(a) => a.update(db, config_overrides, value).await, + ValueSpecAny::Union(a) => a.update(db, config_overrides, value).await, + ValueSpecAny::Pointer(a) => a.update(db, config_overrides, value).await, } } - fn requires(&self, id: &str, value: &Value) -> bool { + fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { + match self { + ValueSpecAny::Boolean(a) => a.pointers(value), + ValueSpecAny::Enum(a) => a.pointers(value), + ValueSpecAny::List(a) => a.pointers(value), + ValueSpecAny::Number(a) => a.pointers(value), + ValueSpecAny::Object(a) => a.pointers(value), + ValueSpecAny::String(a) => a.pointers(value), + ValueSpecAny::Union(a) => a.pointers(value), + ValueSpecAny::Pointer(a) => a.pointers(value), + } + } + fn requires(&self, id: &PackageId, value: &Value) -> bool { match self { ValueSpecAny::Boolean(a) => a.requires(id, value), ValueSpecAny::Enum(a) => a.requires(id, value), @@ -381,10 +432,10 @@ impl Defaultable for ValueSpecAny { timeout: &Option, ) -> Result { match self { - ValueSpecAny::Boolean(a) => a.gen(rng, timeout).map_err(crate::util::absurd), - ValueSpecAny::Enum(a) => a.gen(rng, timeout).map_err(crate::util::absurd), + ValueSpecAny::Boolean(a) => a.gen(rng, timeout).map_err(crate::util::Never::absurd), + ValueSpecAny::Enum(a) => a.gen(rng, timeout).map_err(crate::util::Never::absurd), ValueSpecAny::List(a) => a.gen(rng, timeout), - ValueSpecAny::Number(a) => a.gen(rng, timeout).map_err(crate::util::absurd), + ValueSpecAny::Number(a) => a.gen(rng, timeout).map_err(crate::util::Never::absurd), ValueSpecAny::Object(a) => a.gen(rng, timeout), ValueSpecAny::String(a) => a.gen(rng, timeout).map_err(ConfigurationError::from), ValueSpecAny::Union(a) => a.gen(rng, timeout), @@ -393,7 +444,7 @@ impl Defaultable for ValueSpecAny { } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct ValueSpecBoolean {} #[async_trait] impl ValueSpec for ValueSpecBoolean { @@ -407,13 +458,21 @@ impl ValueSpec for ValueSpecBoolean { ))), } } - fn validate(&self, _manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, _manifest: &Manifest) -> Result<(), NoMatchWithPath> { Ok(()) } - async fn update(&self, _value: &mut Value) -> Result<(), ConfigurationError> { + async fn update( + &self, + _db: &mut Db, + _config_overrides: &IndexMap, + _value: &mut Value, + ) -> Result<(), ConfigurationError> { Ok(()) } - fn requires(&self, _id: &str, _value: &Value) -> bool { + fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { + Ok(Vec::new()) + } + fn requires(&self, _id: &PackageId, _value: &Value) -> bool { false } fn eq(&self, lhs: &Value, rhs: &Value) -> bool { @@ -437,20 +496,20 @@ impl DefaultableWith for ValueSpecBoolean { } } -#[derive(Clone, Debug, serde::Serialize)] +#[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ValueSpecEnum { - pub values: LinearSet, - pub value_names: LinearMap, + pub values: IndexSet, + pub value_names: IndexMap, } impl<'de> serde::de::Deserialize<'de> for ValueSpecEnum { fn deserialize>(deserializer: D) -> Result { - #[derive(serde::Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct _ValueSpecEnum { - pub values: LinearSet, + pub values: IndexSet, #[serde(default)] - pub value_names: LinearMap, + pub value_names: IndexMap, } let mut r#enum = _ValueSpecEnum::deserialize(deserializer)?; @@ -486,13 +545,21 @@ impl ValueSpec for ValueSpecEnum { ))), } } - fn validate(&self, _manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, _manifest: &Manifest) -> Result<(), NoMatchWithPath> { Ok(()) } - async fn update(&self, _value: &mut Value) -> Result<(), ConfigurationError> { + async fn update( + &self, + _db: &mut Db, + _config_overrides: &IndexMap, + _value: &mut Value, + ) -> Result<(), ConfigurationError> { Ok(()) } - fn requires(&self, _id: &str, _value: &Value) -> bool { + fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { + Ok(Vec::new()) + } + fn requires(&self, _id: &PackageId, _value: &Value) -> bool { false } fn eq(&self, lhs: &Value, rhs: &Value) -> bool { @@ -516,7 +583,7 @@ impl DefaultableWith for ValueSpecEnum { } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct ListSpec { pub spec: T, pub range: NumRange, @@ -529,7 +596,7 @@ where { fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { match value { - Value::List(l) => { + Value::Array(l) => { if !self.range.contains(&l.len()) { Err(NoMatchWithPath { path: Vec::new(), @@ -562,13 +629,18 @@ where ))), } } - fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { self.spec.validate(manifest) } - async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { - if let Value::List(ref mut ls) = value { + async fn update( + &self, + db: &mut Db, + config_overrides: &IndexMap, + value: &mut Value, + ) -> Result<(), ConfigurationError> { + if let Value::Array(ref mut ls) = value { for (i, val) in ls.into_iter().enumerate() { - match self.spec.update(val).await { + match self.spec.update(db, config_overrides, val).await { Err(ConfigurationError::NoMatch(e)) => { Err(ConfigurationError::NoMatch(e.prepend(format!("{}", i)))) } @@ -582,8 +654,11 @@ where ))) } } - fn requires(&self, id: &str, value: &Value) -> bool { - if let Value::List(ref ls) = value { + fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { + Ok(Vec::new()) + } + fn requires(&self, id: &PackageId, value: &Value) -> bool { + if let Value::Array(ref ls) = value { ls.into_iter().any(|v| self.spec.requires(id, v)) } else { false @@ -591,7 +666,7 @@ where } fn eq(&self, lhs: &Value, rhs: &Value) -> bool { match (lhs, rhs) { - (Value::List(lhs), Value::List(rhs)) => { + (Value::Array(lhs), Value::Array(rhs)) => { lhs.iter().zip_longest(rhs.iter()).all(|zip| match zip { itertools::EitherOrBoth::Both(lhs, rhs) => lhs == rhs, _ => false, @@ -619,7 +694,7 @@ where for spec_member in spec.iter() { res.push(self.spec.gen_with(spec_member, rng, timeout)?); } - Ok(Value::List(res)) + Ok(Value::Array(res)) } } @@ -628,7 +703,7 @@ unsafe impl Send for ValueSpecObject {} // TODO: remove unsafe impl Sync for ValueSpecUnion {} // TODO: remove unsafe impl Send for ValueSpecUnion {} // TODO: remove -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] #[serde(tag = "subtype")] pub enum ValueSpecList { @@ -649,7 +724,7 @@ impl ValueSpec for ValueSpecList { ValueSpecList::Union(a) => a.matches(value), } } - fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { match self { ValueSpecList::Enum(a) => a.validate(manifest), ValueSpecList::Number(a) => a.validate(manifest), @@ -658,16 +733,30 @@ impl ValueSpec for ValueSpecList { ValueSpecList::Union(a) => a.validate(manifest), } } - async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + async fn update( + &self, + db: &mut Db, + config_overrides: &IndexMap, + value: &mut Value, + ) -> Result<(), ConfigurationError> { match self { - ValueSpecList::Enum(a) => a.update(value).await, - ValueSpecList::Number(a) => a.update(value).await, - ValueSpecList::Object(a) => a.update(value).await, - ValueSpecList::String(a) => a.update(value).await, - ValueSpecList::Union(a) => a.update(value).await, + ValueSpecList::Enum(a) => a.update(db, config_overrides, value).await, + ValueSpecList::Number(a) => a.update(db, config_overrides, value).await, + ValueSpecList::Object(a) => a.update(db, config_overrides, value).await, + ValueSpecList::String(a) => a.update(db, config_overrides, value).await, + ValueSpecList::Union(a) => a.update(db, config_overrides, value).await, } } - fn requires(&self, id: &str, value: &Value) -> bool { + fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { + match self { + ValueSpecList::Enum(a) => a.pointers(value), + ValueSpecList::Number(a) => a.pointers(value), + ValueSpecList::Object(a) => a.pointers(value), + ValueSpecList::String(a) => a.pointers(value), + ValueSpecList::Union(a) => a.pointers(value), + } + } + fn requires(&self, id: &PackageId, value: &Value) -> bool { match self { ValueSpecList::Enum(a) => a.requires(id, value), ValueSpecList::Number(a) => a.requires(id, value), @@ -696,11 +785,11 @@ impl Defaultable for ValueSpecList { timeout: &Option, ) -> Result { match self { - ValueSpecList::Enum(a) => a.gen(rng, timeout).map_err(crate::util::absurd), - ValueSpecList::Number(a) => a.gen(rng, timeout).map_err(crate::util::absurd), + ValueSpecList::Enum(a) => a.gen(rng, timeout).map_err(crate::util::Never::absurd), + ValueSpecList::Number(a) => a.gen(rng, timeout).map_err(crate::util::Never::absurd), ValueSpecList::Object(a) => { let mut ret = match a.gen(rng, timeout).unwrap() { - Value::List(l) => l, + Value::Array(l) => l, a => { return Err(ConfigurationError::NoMatch(NoMatchWithPath::new( MatchError::InvalidType("list", a.type_of()), @@ -721,7 +810,7 @@ impl Defaultable for ValueSpecList { .map_err(ConfigurationError::from)?, ); } - Ok(Value::List(ret)) + Ok(Value::Array(ret)) } ValueSpecList::String(a) => a.gen(rng, timeout).map_err(ConfigurationError::from), ValueSpecList::Union(a) => a.gen(rng, timeout).map_err(ConfigurationError::from), @@ -729,7 +818,7 @@ impl Defaultable for ValueSpecList { } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct ValueSpecNumber { range: Option>, #[serde(default)] @@ -742,14 +831,15 @@ impl ValueSpec for ValueSpecNumber { fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { match value { Value::Number(n) => { - if self.integral && n.floor() != *n { - return Err(NoMatchWithPath::new(MatchError::NonIntegral(*n))); + let n = n.as_f64().unwrap(); + if self.integral && n.floor() != n { + return Err(NoMatchWithPath::new(MatchError::NonIntegral(n))); } if let Some(range) = &self.range { - if !range.contains(n) { + if !range.contains(&n) { return Err(NoMatchWithPath::new(MatchError::OutOfRange( range.clone(), - *n, + n, ))); } } @@ -762,13 +852,21 @@ impl ValueSpec for ValueSpecNumber { ))), } } - fn validate(&self, _manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, _manifest: &Manifest) -> Result<(), NoMatchWithPath> { Ok(()) } - async fn update(&self, _value: &mut Value) -> Result<(), ConfigurationError> { + async fn update( + &self, + _db: &mut Db, + _config_overrides: &IndexMap, + _value: &mut Value, + ) -> Result<(), ConfigurationError> { Ok(()) } - fn requires(&self, _id: &str, _value: &Value) -> bool { + fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { + Ok(Vec::new()) + } + fn requires(&self, _id: &PackageId, _value: &Value) -> bool { false } fn eq(&self, lhs: &Value, rhs: &Value) -> bool { @@ -778,55 +876,56 @@ impl ValueSpec for ValueSpecNumber { } } } -#[derive(Clone, Copy, Debug, serde::Serialize)] -pub struct Number(pub f64); -impl<'de> serde::de::Deserialize<'de> for Number { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - use serde::de::*; - struct NumberVisitor; - impl<'de> Visitor<'de> for NumberVisitor { - type Value = Number; +// TODO: remove +// #[derive(Clone, Copy, Debug, Serialize)] +// pub struct Number(pub f64); +// impl<'de> serde::de::Deserialize<'de> for Number { +// fn deserialize(deserializer: D) -> Result +// where +// D: serde::de::Deserializer<'de>, +// { +// use serde::de::*; +// struct NumberVisitor; +// impl<'de> Visitor<'de> for NumberVisitor { +// type Value = Number; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a number") - } - fn visit_i8(self, value: i8) -> Result { - Ok(Number(value.into())) - } - fn visit_i16(self, value: i16) -> Result { - Ok(Number(value.into())) - } - fn visit_i32(self, value: i32) -> Result { - Ok(Number(value.into())) - } - fn visit_i64(self, value: i64) -> Result { - Ok(Number(value as f64)) - } - fn visit_u8(self, value: u8) -> Result { - Ok(Number(value.into())) - } - fn visit_u16(self, value: u16) -> Result { - Ok(Number(value.into())) - } - fn visit_u32(self, value: u32) -> Result { - Ok(Number(value.into())) - } - fn visit_u64(self, value: u64) -> Result { - Ok(Number(value as f64)) - } - fn visit_f32(self, value: f32) -> Result { - Ok(Number(value.into())) - } - fn visit_f64(self, value: f64) -> Result { - Ok(Number(value)) - } - } - deserializer.deserialize_any(NumberVisitor) - } -} +// fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { +// formatter.write_str("a number") +// } +// fn visit_i8(self, value: i8) -> Result { +// Ok(Number(value.into())) +// } +// fn visit_i16(self, value: i16) -> Result { +// Ok(Number(value.into())) +// } +// fn visit_i32(self, value: i32) -> Result { +// Ok(Number(value.into())) +// } +// fn visit_i64(self, value: i64) -> Result { +// Ok(Number(value as f64)) +// } +// fn visit_u8(self, value: u8) -> Result { +// Ok(Number(value.into())) +// } +// fn visit_u16(self, value: u16) -> Result { +// Ok(Number(value.into())) +// } +// fn visit_u32(self, value: u32) -> Result { +// Ok(Number(value.into())) +// } +// fn visit_u64(self, value: u64) -> Result { +// Ok(Number(value as f64)) +// } +// fn visit_f32(self, value: f32) -> Result { +// Ok(Number(value.into())) +// } +// fn visit_f64(self, value: f64) -> Result { +// Ok(Number(value)) +// } +// } +// deserializer.deserialize_any(NumberVisitor) +// } +// } impl DefaultableWith for ValueSpecNumber { type DefaultSpec = Option; type Error = crate::util::Never; @@ -837,11 +936,14 @@ impl DefaultableWith for ValueSpecNumber { _rng: &mut R, _timeout: &Option, ) -> Result { - Ok(spec.map(|s| Value::Number(s.0)).unwrap_or(Value::Null)) + Ok(spec + .clone() + .map(|s| Value::Number(s)) + .unwrap_or(Value::Null)) } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ValueSpecObject { pub spec: ConfigSpec, @@ -863,19 +965,34 @@ impl ValueSpec for ValueSpecObject { ))), } } - fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { self.spec.validate(manifest) } - async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + async fn update( + &self, + db: &mut Db, + config_overrides: &IndexMap, + value: &mut Value, + ) -> Result<(), ConfigurationError> { if let Value::Object(o) = value { - self.spec.update(o).await + self.spec.update(db, config_overrides, o).await } else { Err(ConfigurationError::NoMatch(NoMatchWithPath::new( MatchError::InvalidType("object", value.type_of()), ))) } } - fn requires(&self, id: &str, value: &Value) -> bool { + fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { + if let Value::Object(o) = value { + self.spec.pointers(o) + } else { + Err(NoMatchWithPath::new(MatchError::InvalidType( + "object", + value.type_of(), + ))) + } + } + fn requires(&self, id: &PackageId, value: &Value) -> bool { if let Value::Object(o) = value { self.spec.requires(id, o) } else { @@ -922,12 +1039,12 @@ impl Defaultable for ValueSpecObject { } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct ConfigSpec(pub LinearMap); +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct ConfigSpec(pub IndexMap); impl ConfigSpec { pub fn matches(&self, value: &Config) -> Result<(), NoMatchWithPath> { for (key, val) in self.0.iter() { - if let Some(v) = value.0.get(key) { + if let Some(v) = value.get(key) { val.matches(v).map_err(|e| e.prepend(key.clone()))?; } else { val.matches(&Value::Null) @@ -942,31 +1059,35 @@ impl ConfigSpec { rng: &mut R, timeout: &Option, ) -> Result { - let mut res = LinearMap::new(); + let mut res = Config::new(); for (key, val) in self.0.iter() { res.insert(key.clone(), val.gen(rng, timeout)?); } - Ok(Config(res)) + Ok(res) } - pub fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + pub fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { for (name, val) in &self.0 { - if let Err(_) = super::rules::validate_key(&name) { - return Err(NoMatchWithPath::new(MatchError::InvalidKey( - name.to_owned(), - ))); - } val.validate(manifest) .map_err(|e| e.prepend(name.clone()))?; } Ok(()) } - pub async fn update(&self, cfg: &mut Config) -> Result<(), ConfigurationError> { - for (k, v) in cfg.0.iter_mut() { - match self.0.get(k) { - None => (), - Some(vs) => match vs.update(v).await { + pub async fn update( + &self, + db: &mut Db, + config_overrides: &IndexMap, + cfg: &mut Config, + ) -> Result<(), ConfigurationError> { + for (k, vs) in self.0.iter() { + match cfg.get_mut(k) { + None => { + let mut v = Value::Null; + vs.update(db, config_overrides, &mut v).await?; + cfg.insert(k.clone(), v); + } + Some(v) => match vs.update(db, config_overrides, v).await { Err(ConfigurationError::NoMatch(e)) => { Err(ConfigurationError::NoMatch(e.prepend(k.clone()))) } @@ -976,14 +1097,29 @@ impl ConfigSpec { } Ok(()) } - pub fn requires(&self, id: &str, cfg: &Config) -> bool { + + pub fn pointers(&self, cfg: &Config) -> Result, NoMatchWithPath> { + let mut res = Vec::new(); + for (k, v) in cfg.iter() { + match self.0.get(k) { + None => (), + Some(vs) => match vs.pointers(v) { + Err(e) => return Err(e.prepend(k.clone())), + Ok(mut a) => res.append(&mut a), + }, + }; + } + Ok(res) + } + + pub fn requires(&self, id: &PackageId, cfg: &Config) -> bool { self.0 .iter() - .any(|(k, v)| v.requires(id, cfg.0.get(k).unwrap_or(&STATIC_NULL))) + .any(|(k, v)| v.requires(id, cfg.get(k).unwrap_or(&STATIC_NULL))) } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Pattern { #[serde(with = "util::serde_regex")] @@ -991,7 +1127,7 @@ pub struct Pattern { pub pattern_description: String, } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct ValueSpecString { #[serde(flatten)] pub pattern: Option, @@ -1025,13 +1161,21 @@ impl ValueSpec for ValueSpecString { ))), } } - fn validate(&self, _manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, _manifest: &Manifest) -> Result<(), NoMatchWithPath> { Ok(()) } - async fn update(&self, _value: &mut Value) -> Result<(), ConfigurationError> { + async fn update( + &self, + _db: &mut Db, + _config_overrides: &IndexMap, + _value: &mut Value, + ) -> Result<(), ConfigurationError> { Ok(()) } - fn requires(&self, _id: &str, _value: &Value) -> bool { + fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { + Ok(Vec::new()) + } + fn requires(&self, _id: &PackageId, _value: &Value) -> bool { false } fn eq(&self, lhs: &Value, rhs: &Value) -> bool { @@ -1077,7 +1221,7 @@ impl DefaultableWith for ValueSpecString { } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum DefaultString { Literal(String), @@ -1092,7 +1236,7 @@ impl DefaultString { } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Entropy { pub charset: Option, pub len: usize, @@ -1109,51 +1253,51 @@ impl Entropy { } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UnionTag { pub id: String, pub name: String, pub description: Option, - pub variant_names: LinearMap, + pub variant_names: IndexMap, } -#[derive(Clone, Debug, serde::Serialize)] +#[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ValueSpecUnion { pub tag: UnionTag, - pub variants: LinearMap, + pub variants: IndexMap, pub display_as: Option, pub unique_by: UniqueBy, } impl<'de> serde::de::Deserialize<'de> for ValueSpecUnion { fn deserialize>(deserializer: D) -> Result { - #[derive(serde::Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] #[serde(untagged)] pub enum _UnionTag { Old(String), New(UnionTag), } - #[derive(serde::Deserialize)] + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct _ValueSpecUnion { - pub variants: LinearMap, + pub variants: IndexMap, pub tag: _UnionTag, pub display_as: Option, #[serde(default)] pub unique_by: UniqueBy, } - let union = _ValueSpecUnion::deserialize(deserializer)?; + let u = _ValueSpecUnion::deserialize(deserializer)?; Ok(ValueSpecUnion { - tag: match union.tag { + tag: match u.tag { _UnionTag::Old(id) => UnionTag { id: id.clone(), name: id, description: None, - variant_names: union + variant_names: u .variants .keys() .map(|k| (k.to_owned(), k.to_owned())) @@ -1169,8 +1313,8 @@ impl<'de> serde::de::Deserialize<'de> for ValueSpecUnion { name, description, variant_names: { - let mut iter = union.variants.keys(); - while variant_names.len() < union.variants.len() { + let mut iter = u.variants.keys(); + while variant_names.len() < u.variants.len() { if let Some(variant) = iter.next() { variant_names.insert(variant.to_owned(), variant.to_owned()); } else { @@ -1181,9 +1325,9 @@ impl<'de> serde::de::Deserialize<'de> for ValueSpecUnion { }, }, }, - variants: union.variants, - display_as: union.display_as, - unique_by: union.unique_by, + variants: u.variants, + display_as: u.display_as, + unique_by: u.unique_by, }) } } @@ -1193,10 +1337,10 @@ impl ValueSpec for ValueSpecUnion { fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { match value { Value::Object(o) => { - if let Some(Value::String(ref tag)) = o.0.get(&self.tag.id) { + if let Some(Value::String(ref tag)) = o.get(&self.tag.id) { if let Some(obj_spec) = self.variants.get(tag) { let mut without_tag = o.clone(); - without_tag.0.remove(&self.tag.id); + without_tag.remove(&self.tag.id); obj_spec.matches(&without_tag) } else { Err(NoMatchWithPath::new(MatchError::Union( @@ -1217,7 +1361,7 @@ impl ValueSpec for ValueSpecUnion { ))), } } - fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { for (name, variant) in &self.variants { if variant.0.get(&self.tag.id).is_some() { return Err(NoMatchWithPath::new(MatchError::PropertyMatchesUnionTag( @@ -1229,15 +1373,22 @@ impl ValueSpec for ValueSpecUnion { } Ok(()) } - async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + async fn update( + &self, + db: &mut Db, + config_overrides: &IndexMap, + value: &mut Value, + ) -> Result<(), ConfigurationError> { if let Value::Object(o) = value { - match o.0.get(&self.tag.id) { + match o.get(&self.tag.id) { None => Err(ConfigurationError::NoMatch(NoMatchWithPath::new( MatchError::MissingTag(self.tag.id.clone()), ))), Some(Value::String(tag)) => match self.variants.get(tag) { - None => Err(ConfigurationError::InvalidVariant(tag.clone())), - Some(spec) => spec.update(o).await, + None => Err(ConfigurationError::NoMatch(NoMatchWithPath::new( + MatchError::Union(tag.clone(), self.variants.keys().cloned().collect()), + ))), + Some(spec) => spec.update(db, config_overrides, o).await, }, Some(other) => Err(ConfigurationError::NoMatch( NoMatchWithPath::new(MatchError::InvalidType("string", other.type_of())) @@ -1250,9 +1401,35 @@ impl ValueSpec for ValueSpecUnion { ))) } } - fn requires(&self, id: &str, value: &Value) -> bool { + fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { if let Value::Object(o) = value { - match o.0.get(&self.tag.id) { + match o.get(&self.tag.id) { + None => Err(NoMatchWithPath::new(MatchError::MissingTag( + self.tag.id.clone(), + ))), + Some(Value::String(tag)) => match self.variants.get(tag) { + None => Err(NoMatchWithPath::new(MatchError::Union( + tag.clone(), + self.variants.keys().cloned().collect(), + ))), + Some(spec) => spec.pointers(o), + }, + Some(other) => Err(NoMatchWithPath::new(MatchError::InvalidType( + "string", + other.type_of(), + )) + .prepend(self.tag.id.clone())), + } + } else { + Err(NoMatchWithPath::new(MatchError::InvalidType( + "object", + value.type_of(), + ))) + } + } + fn requires(&self, id: &PackageId, value: &Value) -> bool { + if let Value::Object(o) = value { + match o.get(&self.tag.id) { Some(Value::String(tag)) => match self.variants.get(tag) { None => false, Some(spec) => spec.requires(id, o), @@ -1283,29 +1460,31 @@ impl DefaultableWith for ValueSpecUnion { let variant = if let Some(v) = self.variants.get(spec) { v } else { - return Err(ConfigurationError::InvalidVariant(spec.clone())); + return Err(ConfigurationError::NoMatch(NoMatchWithPath::new( + MatchError::Union(spec.clone(), self.variants.keys().cloned().collect()), + ))); }; let cfg_res = variant.gen(rng, timeout)?; - let mut tagged_cfg = LinearMap::new(); + let mut tagged_cfg = Config::new(); tagged_cfg.insert(self.tag.id.clone(), Value::String(spec.clone())); - tagged_cfg.extend(cfg_res.0.into_iter()); + tagged_cfg.extend(cfg_res.into_iter()); - Ok(Value::Object(Config(tagged_cfg))) + Ok(Value::Object(tagged_cfg)) } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "subtype")] #[serde(rename_all = "kebab-case")] pub enum ValueSpecPointer { - App(AppPointerSpec), + Package(PackagePointerSpec), System(SystemPointerSpec), } impl fmt::Display for ValueSpecPointer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ValueSpecPointer::App(p) => write!(f, "{}", p), + ValueSpecPointer::Package(p) => write!(f, "{}", p), ValueSpecPointer::System(p) => write!(f, "{}", p), } } @@ -1324,25 +1503,33 @@ impl Defaultable for ValueSpecPointer { impl ValueSpec for ValueSpecPointer { fn matches(&self, value: &Value) -> Result<(), NoMatchWithPath> { match self { - ValueSpecPointer::App(a) => a.matches(value), + ValueSpecPointer::Package(a) => a.matches(value), ValueSpecPointer::System(a) => a.matches(value), } } - fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { match self { - ValueSpecPointer::App(a) => a.validate(manifest), + ValueSpecPointer::Package(a) => a.validate(manifest), ValueSpecPointer::System(a) => a.validate(manifest), } } - async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { + async fn update( + &self, + db: &mut Db, + config_overrides: &IndexMap, + value: &mut Value, + ) -> Result<(), ConfigurationError> { match self { - ValueSpecPointer::App(a) => a.update(value).await, - ValueSpecPointer::System(a) => a.update(value).await, + ValueSpecPointer::Package(a) => a.update(db, config_overrides, value).await, + ValueSpecPointer::System(a) => a.update(db, config_overrides, value).await, } } - fn requires(&self, id: &str, value: &Value) -> bool { + fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { + Ok(vec![self.clone()]) + } + fn requires(&self, id: &PackageId, value: &Value) -> bool { match self { - ValueSpecPointer::App(a) => a.requires(id, value), + ValueSpecPointer::Package(a) => a.requires(id, value), ValueSpecPointer::System(a) => a.requires(id, value), } } @@ -1351,88 +1538,107 @@ impl ValueSpec for ValueSpecPointer { } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] -pub struct AppPointerSpec { - pub app_id: String, +pub struct PackagePointerSpec { + pub package_id: PackageId, #[serde(flatten)] - pub target: AppPointerSpecVariants, + pub target: PackagePointerSpecVariant, } -impl fmt::Display for AppPointerSpec { +impl fmt::Display for PackagePointerSpec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "[{}].{}", self.app_id, self.target) + write!(f, "{}: {}", self.package_id, self.target) } } -impl AppPointerSpec { - async fn deref(&self) -> Result { - match self.target { - AppPointerSpecVariants::TorAddress => { - let mut apps = crate::apps::list_info() +impl PackagePointerSpec { + async fn deref( + &self, + db: &mut Db, + config_overrides: &IndexMap, + ) -> Result { + match &self.target { + PackagePointerSpecVariant::TorAddress { interface } => { + let addr = crate::db::DatabaseModel::new() + .package_data() + .idx_model(&self.package_id) + .and_then(|pde| pde.installed()) + .and_then(|installed| { + installed.interface_info().addresses().idx_model(interface) + }) + .and_then(|addresses| addresses.tor_address()) + .get(db) .await - .map_err(ConfigurationError::SystemError)?; - let info = apps.remove(&self.app_id); - Ok(info - .and_then(|info| info.tor_address) - .map(Value::String) - .unwrap_or(Value::Null)) + .map_err(|e| ConfigurationError::SystemError(Error::from(e)))?; + Ok(addr.to_owned().map(Value::String).unwrap_or(Value::Null)) } - AppPointerSpecVariants::TorKey => { - let services_path = PersistencePath::from_ref(crate::SERVICES_YAML); - let service_map = crate::tor::services_map(&services_path) + PackagePointerSpecVariant::LanAddress { interface } => { + let addr = crate::db::DatabaseModel::new() + .package_data() + .idx_model(&self.package_id) + .and_then(|pde| pde.installed()) + .and_then(|installed| { + installed.interface_info().addresses().idx_model(interface) + }) + .and_then(|addresses| addresses.lan_address()) + .get(db) .await - .map_err(ConfigurationError::SystemError)?; - let service = - service_map - .map - .get(&self.app_id) - .ok_or(ConfigurationError::SystemError(crate::Error::new( - failure::format_err!("App Not Found"), - Some(crate::error::NOT_FOUND), - )))?; - Ok( - crate::tor::read_tor_key(&self.app_id, service.hidden_service_version, None) - .await - .map(Value::String) - .unwrap_or(Value::Null), - ) + .map_err(|e| ConfigurationError::SystemError(Error::from(e)))?; + Ok(addr.to_owned().map(Value::String).unwrap_or(Value::Null)) } - AppPointerSpecVariants::LanAddress => { - let services_path = PersistencePath::from_ref(crate::SERVICES_YAML); - let mut service_map = crate::tor::services_map(&services_path) - .await - .map_err(ConfigurationError::SystemError)?; - let service = service_map.map.remove(&self.app_id); - Ok(service - .map(|service| Value::String(format!("{}", service.ip))) - .unwrap_or(Value::Null)) - } - AppPointerSpecVariants::Config { ref index } => { - // check if the app exists - if !crate::apps::list_info() - .await - .map_err(ConfigurationError::SystemError)? - .contains_key(&self.app_id) - { - return Ok(Value::Null); - } - // fetch the config of the pointer target - let app_config = crate::apps::config(&self.app_id) - .await - .map_err(ConfigurationError::SystemError)?; - let cfg = if let Some(cfg) = app_config.config { - cfg + PackagePointerSpecVariant::Config { selector, multi } => { + if let Some(cfg) = config_overrides.get(&self.package_id) { + Ok(selector.select(*multi, &Value::Object(cfg.clone()))) } else { - return Ok(Value::Null); - }; - let mut cfgs = LinearMap::new(); - cfgs.insert(self.app_id.as_str(), Cow::Borrowed(&cfg)); - - Ok((index.compiled)(&cfg, &cfgs)) + let manifest_model: OptionModel<_> = crate::db::DatabaseModel::new() + .package_data() + .idx_model(&self.package_id) + .and_then(|pde| pde.installed()) + .map(|installed| installed.manifest()) + .into(); + let version = manifest_model + .clone() + .map(|manifest| manifest.version()) + .get(db) + .await + .map_err(|e| ConfigurationError::SystemError(Error::from(e)))?; + let cfg_actions = manifest_model + .clone() + .and_then(|manifest| manifest.config()) + .get(db) + .await + .map_err(|e| ConfigurationError::SystemError(Error::from(e)))?; + let volumes = manifest_model + .map(|manifest| manifest.volumes()) + .get(db) + .await + .map_err(|e| ConfigurationError::SystemError(Error::from(e)))?; + let hosts = crate::db::DatabaseModel::new() + .network() + .hosts() + .get(db) + .await + .map_err(|e| ConfigurationError::SystemError(Error::from(e)))?; + if let (Some(version), Some(cfg_actions), Some(volumes)) = + (&*version, &*cfg_actions, &*volumes) + { + let cfg_res = cfg_actions + .get(&self.package_id, version, volumes, &*hosts) + .await + .map_err(|e| ConfigurationError::SystemError(Error::from(e)))?; + if let Some(cfg) = cfg_res.config { + Ok(selector.select(*multi, &Value::Object(cfg))) + } else { + Ok(Value::Null) + } + } else { + Ok(Value::Null) + } + } } } } } -impl Defaultable for AppPointerSpec { +impl Defaultable for PackagePointerSpec { type Error = ConfigurationError; fn gen( &self, @@ -1443,116 +1649,117 @@ impl Defaultable for AppPointerSpec { } } #[async_trait] -impl ValueSpec for AppPointerSpec { +impl ValueSpec for PackagePointerSpec { fn matches(&self, _value: &Value) -> Result<(), NoMatchWithPath> { Ok(()) } - fn validate(&self, manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { - if manifest.id != self.app_id && !manifest.dependencies.0.contains_key(&self.app_id) { + fn validate(&self, manifest: &Manifest) -> Result<(), NoMatchWithPath> { + if manifest.id != self.package_id && !manifest.dependencies.0.contains_key(&self.package_id) + { return Err(NoMatchWithPath::new(MatchError::InvalidPointer( - ValueSpecPointer::App(self.clone()), + ValueSpecPointer::Package(self.clone()), ))); } match self.target { - AppPointerSpecVariants::TorKey if manifest.id != self.app_id => { - Err(NoMatchWithPath::new(MatchError::InvalidPointer( - ValueSpecPointer::App(self.clone()), - ))) - } _ => Ok(()), } } - async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { - *value = self.deref().await?; + async fn update( + &self, + db: &mut Db, + config_overrides: &IndexMap, + value: &mut Value, + ) -> Result<(), ConfigurationError> { + *value = self.deref(db, config_overrides).await?; Ok(()) } - fn requires(&self, id: &str, _value: &Value) -> bool { - self.app_id == id + fn pointers(&self, _value: &Value) -> Result, NoMatchWithPath> { + Ok(vec![ValueSpecPointer::Package(self.clone())]) + } + fn requires(&self, id: &PackageId, _value: &Value) -> bool { + &self.package_id == id } fn eq(&self, _lhs: &Value, _rhs: &Value) -> bool { false } } -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "target")] #[serde(rename_all = "kebab-case")] -pub enum AppPointerSpecVariants { - TorAddress, - TorKey, - LanAddress, - Config { index: Arc }, +pub enum PackagePointerSpecVariant { + TorAddress { + interface: InterfaceId, + }, + LanAddress { + interface: InterfaceId, + }, + Config { + selector: Arc, + multi: bool, + }, } -impl fmt::Display for AppPointerSpecVariants { +impl fmt::Display for PackagePointerSpecVariant { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::TorAddress => write!(f, "TOR_ADDRESS"), - Self::TorKey => write!(f, "TOR_KEY"), - Self::LanAddress => write!(f, "LAN_ADDRESS"), - Self::Config { index } => write!(f, "{}", index.src), + Self::TorAddress { interface } => write!(f, "tor-address: {}", interface), + Self::LanAddress { interface } => write!(f, "lan-address: {}", interface), + Self::Config { selector, .. } => write!(f, "config: {}", selector), } } } -#[derive(Clone)] -pub struct ConfigPointer { - pub src: String, - pub compiled: Arc>, +#[derive(Clone, Debug)] +pub struct ConfigSelector { + src: String, + compiled: CompiledJsonPath, } -impl std::fmt::Debug for ConfigPointer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ConfigPointer") - .field("src", &self.src) - .field("compiled", &"Fn(&Config, &Config) -> bool") - .finish() +impl ConfigSelector { + pub fn select(&self, multi: bool, val: &Value) -> Value { + let selected = self.compiled.select(&val).ok().unwrap_or_else(Vec::new); + if multi { + Value::Array(selected.into_iter().cloned().collect()) + } else { + selected.get(0).map(|v| (*v).clone()).unwrap_or(Value::Null) + } } } -impl<'de> serde::de::Deserialize<'de> for ConfigPointer { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - let src = String::deserialize(deserializer)?; - let compiled = super::rules::compile_expr(&src).map_err(serde::de::Error::custom)?; - Ok(ConfigPointer { - src, - compiled: Arc::new(compiled), - }) +impl fmt::Display for ConfigSelector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.src) } } -impl serde::ser::Serialize for ConfigPointer { +impl Serialize for ConfigSelector { fn serialize(&self, serializer: S) -> Result where - S: serde::ser::Serializer, + S: Serializer, { serializer.serialize_str(&self.src) } } +impl<'de> Deserialize<'de> for ConfigSelector { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let src: String = Deserialize::deserialize(deserializer)?; + let compiled = CompiledJsonPath::compile(&src).map_err(serde::de::Error::custom)?; + Ok(Self { src, compiled }) + } +} -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] #[serde(tag = "target")] -pub enum SystemPointerSpec { - HostIp, -} +pub enum SystemPointerSpec {} impl fmt::Display for SystemPointerSpec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "[SYSTEM].{}", - match self { - SystemPointerSpec::HostIp => "HOST_IP", - } - ) + write!(f, "SYSTEM: {}", match *self {}) } } impl SystemPointerSpec { - async fn deref(&self) -> Result { - Ok(match self { - SystemPointerSpec::HostIp => { - Value::String(format!("{}", std::net::Ipv4Addr::from(crate::HOST_IP))) - } - }) + async fn deref(&self, db: &mut Db) -> Result { + Ok(match *self {}) } } impl Defaultable for SystemPointerSpec { @@ -1570,14 +1777,22 @@ impl ValueSpec for SystemPointerSpec { fn matches(&self, _value: &Value) -> Result<(), NoMatchWithPath> { Ok(()) } - fn validate(&self, _manifest: &ManifestLatest) -> Result<(), NoMatchWithPath> { + fn validate(&self, _manifest: &Manifest) -> Result<(), NoMatchWithPath> { Ok(()) } - async fn update(&self, value: &mut Value) -> Result<(), ConfigurationError> { - *value = self.deref().await?; + async fn update( + &self, + db: &mut Db, + _config_overrides: &IndexMap, + value: &mut Value, + ) -> Result<(), ConfigurationError> { + *value = self.deref(db).await?; Ok(()) } - fn requires(&self, _id: &str, _value: &Value) -> bool { + fn pointers(&self, value: &Value) -> Result, NoMatchWithPath> { + Ok(vec![ValueSpecPointer::System(self.clone())]) + } + fn requires(&self, _id: &PackageId, _value: &Value) -> bool { false } fn eq(&self, _lhs: &Value, _rhs: &Value) -> bool { @@ -1761,7 +1976,7 @@ mod test { "name": "Type", "variantNames": {} }, - "variants": { + "Variant": { "internal": { "lan-address": { "name": "LAN Address", @@ -1830,45 +2045,7 @@ mod test { } }); let spec: ConfigSpec = serde_json::from_value(spec).unwrap(); - let mut deps = crate::dependencies::Dependencies::default(); - deps.0.insert( - "bitcoind".to_owned(), - crate::dependencies::DepInfo { - version: "^0.20.0".parse().unwrap(), - description: None, - mount_public: false, - mount_shared: false, - optional: Some("Could be external.".to_owned()), - config: Vec::new(), - }, - ); - spec.validate(&crate::manifest::ManifestV0 { - id: "test-app".to_owned(), - version: "0.1.0".parse().unwrap(), - title: "Test App".to_owned(), - description: crate::manifest::Description { - short: "A test app.".to_owned(), - long: "A super cool test app for testing".to_owned(), - }, - release_notes: "Some things changed".to_owned(), - ports: Vec::new(), - image: crate::manifest::ImageConfig::Tar, - shm_size_mb: None, - mount: "/root".parse().unwrap(), - public: None, - shared: None, - has_instructions: false, - os_version_required: ">=0.2.5".parse().unwrap(), - os_version_recommended: ">=0.2.5".parse().unwrap(), - assets: Vec::new(), - hidden_service_version: crate::tor::HiddenServiceVersion::V3, - dependencies: deps, - extra: LinearMap::new(), - install_alert: None, - restore_alert: None, - uninstall_alert: None, - }) - .unwrap(); + spec.validate(todo!()).unwrap(); let config = spec .gen(&mut rand::rngs::StdRng::from_entropy(), &None) .unwrap(); diff --git a/appmgr/src/config/util.rs b/appmgr/src/config/util.rs index d7595e52d..0b56105ac 100644 --- a/appmgr/src/config/util.rs +++ b/appmgr/src/config/util.rs @@ -1,12 +1,12 @@ -use std::ops::Bound; -use std::ops::RangeBounds; -use std::ops::RangeInclusive; +use std::ops::{Bound, RangeBounds, RangeInclusive}; -use rand::{distributions::Distribution, Rng}; +use rand::distributions::Distribution; +use rand::Rng; +use serde_json::Value; -use super::value::Config; +use super::Config; -pub const STATIC_NULL: super::value::Value = super::value::Value::Null; +pub const STATIC_NULL: Value = Value::Null; #[derive(Clone, Debug)] pub struct CharSet(pub Vec<(RangeInclusive, usize)>, usize); @@ -15,7 +15,7 @@ impl CharSet { self.0.iter().any(|r| r.0.contains(c)) } pub fn gen(&self, rng: &mut R) -> char { - let mut idx = rng.gen_range(0, self.1); + let mut idx = rng.gen_range(0..self.1); for r in &self.0 { if idx < r.1 { return std::convert::TryFrom::try_from( @@ -282,7 +282,7 @@ impl UniqueBy { match self { UniqueBy::Any(any) => any.iter().any(|u| u.eq(lhs, rhs)), UniqueBy::All(all) => all.iter().all(|u| u.eq(lhs, rhs)), - UniqueBy::Exactly(key) => lhs.0.get(key) == rhs.0.get(key), + UniqueBy::Exactly(key) => lhs.get(key) == rhs.get(key), UniqueBy::NotUnique => false, } } diff --git a/appmgr/src/config/value.rs b/appmgr/src/config/value.rs deleted file mode 100644 index deb8439a9..000000000 --- a/appmgr/src/config/value.rs +++ /dev/null @@ -1,66 +0,0 @@ -use linear_map::LinearMap; - -#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct Config(pub LinearMap); - -impl Config { - pub fn merge_with(&mut self, other: Config) { - for (key, val) in other.0.into_iter() { - match (self.0.get_mut(&key), &val) { - (Some(Value::Object(l_obj)), Value::Object(_)) => { - // gross, I know. https://github.com/rust-lang/rust/issues/45600 - let r_obj = match val { - Value::Object(r_obj) => r_obj, - _ => unreachable!(), - }; - l_obj.merge_with(r_obj) - } - (Some(Value::List(l_vec)), Value::List(_)) => { - let mut r_vec = match val { - Value::List(r_vec) => r_vec, - _ => unreachable!(), - }; - l_vec.append(&mut r_vec); - } - _ => { - self.0.insert(key, val); - } - } - } - } -} - -fn serialize_num(num: &f64, serializer: S) -> Result { - if *num < (1_i64 << f64::MANTISSA_DIGITS) as f64 - && *num > -(1_i64 << f64::MANTISSA_DIGITS) as f64 - && num.trunc() == *num - { - serializer.serialize_i64(*num as i64) - } else { - serializer.serialize_f64(*num) - } -} - -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -#[serde(untagged)] -pub enum Value { - String(String), - #[serde(serialize_with = "serialize_num")] - Number(f64), - Bool(bool), - List(Vec), - Object(Config), - Null, -} -impl Value { - pub fn type_of(&self) -> &'static str { - match self { - Value::String(_) => "string", - Value::Number(_) => "number", - Value::Bool(_) => "boolean", - Value::List(_) => "list", - Value::Object(_) => "object", - Value::Null => "null", - } - } -} diff --git a/appmgr/src/context/cli.rs b/appmgr/src/context/cli.rs new file mode 100644 index 000000000..2dd16d96e --- /dev/null +++ b/appmgr/src/context/cli.rs @@ -0,0 +1,135 @@ +use std::fs::File; +use std::net::IpAddr; +use std::path::Path; +use std::sync::Arc; + +use clap::ArgMatches; +use reqwest::Proxy; +use rpc_toolkit::reqwest::{Client, Url}; +use rpc_toolkit::url::Host; +use rpc_toolkit::Context; +use serde::Deserialize; + +use super::rpc::RpcContextConfig; +use crate::ResultExt; + +#[derive(Debug, Default, Deserialize)] +pub struct CliContextConfig { + #[serde(deserialize_with = "deserialize_host")] + pub host: Option, + pub port: Option, + #[serde(deserialize_with = "crate::util::deserialize_from_str_opt")] + pub proxy: Option, + #[serde(flatten)] + pub server_config: RpcContextConfig, +} + +#[derive(Debug)] +pub struct CliContextSeed { + pub host: Host, + pub port: u16, + pub client: Client, +} + +#[derive(Debug, Clone)] +pub struct CliContext(Arc); +impl CliContext { + pub fn init(matches: &ArgMatches) -> Result { + let cfg_path = Path::new(crate::CONFIG_PATH); + let mut base = if cfg_path.exists() { + serde_yaml::from_reader( + File::open(cfg_path) + .with_ctx(|_| (crate::ErrorKind::Filesystem, cfg_path.display().to_string()))?, + ) + .with_kind(crate::ErrorKind::Deserialization)? + } else { + CliContextConfig::default() + }; + if let Some(bind) = base.server_config.bind { + if base.host.is_none() { + base.host = Some(match bind.ip() { + IpAddr::V4(a) => Host::Ipv4(a), + IpAddr::V6(a) => Host::Ipv6(a), + }); + } + if base.port.is_none() { + base.port = Some(bind.port()) + } + } + if let Some(host) = matches.value_of("host") { + base.host = Some(Host::parse(host).with_kind(crate::ErrorKind::ParseUrl)?); + } + if let Some(port) = matches.value_of("port") { + base.port = Some(port.parse()?); + } + if let Some(proxy) = matches.value_of("proxy") { + base.proxy = Some(proxy.parse()?); + } + Ok(CliContext(Arc::new(CliContextSeed { + host: base.host.unwrap_or(Host::Ipv4([127, 0, 0, 1].into())), + port: base.port.unwrap_or(5959), + client: if let Some(proxy) = base.proxy { + Client::builder() + .proxy(Proxy::all(proxy).with_kind(crate::ErrorKind::ParseUrl)?) + .build() + .expect("cannot fail") + } else { + Client::new() + }, + }))) + } +} +impl Context for CliContext { + fn host(&self) -> Host<&str> { + match &self.0.host { + Host::Domain(a) => Host::Domain(a.as_str()), + Host::Ipv4(a) => Host::Ipv4(*a), + Host::Ipv6(a) => Host::Ipv6(*a), + } + } + fn port(&self) -> u16 { + self.0.port + } + fn client(&self) -> &Client { + &self.0.client + } +} + +fn deserialize_host<'de, D: serde::de::Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Option; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "a parsable string") + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Host::parse(v) + .map(Some) + .map_err(|e| serde::de::Error::custom(e)) + } + fn visit_some(self, deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + deserializer.deserialize_str(Visitor) + } + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + } + deserializer.deserialize_any(Visitor) +} diff --git a/appmgr/src/context/mod.rs b/appmgr/src/context/mod.rs new file mode 100644 index 000000000..6936538e1 --- /dev/null +++ b/appmgr/src/context/mod.rs @@ -0,0 +1,109 @@ +use rpc_toolkit::reqwest::Client; +use rpc_toolkit::url::{Host, Url}; +use rpc_toolkit::Context; + +mod cli; +mod rpc; + +pub use cli::CliContext; +pub use rpc::RpcContext; + +#[derive(Debug, Clone)] +pub struct ExtendedContext { + base: T, + extension: U, +} +impl ExtendedContext { + pub fn map V, V>(self, f: F) -> ExtendedContext { + ExtendedContext { + base: self.base, + extension: f(self.extension), + } + } + pub fn split(self) -> (T, U) { + (self.base, self.extension) + } + pub fn base(&self) -> &T { + &self.base + } + pub fn extension(&self) -> &U { + &self.extension + } +} +impl From for ExtendedContext { + fn from(base: T) -> Self { + ExtendedContext { + base, + extension: (), + } + } +} +impl Context for ExtendedContext { + fn host(&self) -> Host<&str> { + self.base.host() + } + fn port(&self) -> u16 { + self.base.port() + } + fn protocol(&self) -> &str { + self.base.protocol() + } + fn url(&self) -> Url { + self.base.url() + } + fn client(&self) -> &Client { + self.base.client() + } +} + +#[derive(Clone)] +pub enum EitherContext { + Cli(CliContext), + Rpc(RpcContext), +} +impl EitherContext { + pub fn as_cli(&self) -> Option<&CliContext> { + match self { + EitherContext::Cli(a) => Some(a), + _ => None, + } + } + pub fn as_rpc(&self) -> Option<&RpcContext> { + match self { + EitherContext::Rpc(a) => Some(a), + _ => None, + } + } +} +impl Context for EitherContext { + fn host(&self) -> Host<&str> { + match self { + EitherContext::Cli(a) => a.host(), + EitherContext::Rpc(b) => b.host(), + } + } + fn port(&self) -> u16 { + match self { + EitherContext::Cli(a) => a.port(), + EitherContext::Rpc(b) => b.port(), + } + } + fn protocol(&self) -> &str { + match self { + EitherContext::Cli(a) => a.protocol(), + EitherContext::Rpc(b) => b.protocol(), + } + } + fn url(&self) -> Url { + match self { + EitherContext::Cli(a) => a.url(), + EitherContext::Rpc(b) => b.url(), + } + } + fn client(&self) -> &Client { + match self { + EitherContext::Cli(a) => a.client(), + EitherContext::Rpc(b) => b.client(), + } + } +} diff --git a/appmgr/src/context/rpc.rs b/appmgr/src/context/rpc.rs new file mode 100644 index 000000000..1a5aa2612 --- /dev/null +++ b/appmgr/src/context/rpc.rs @@ -0,0 +1,79 @@ +use std::net::{IpAddr, SocketAddr}; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use bollard::Docker; +use patch_db::PatchDb; +use rpc_toolkit::url::Host; +use rpc_toolkit::Context; +use serde::Deserialize; +use sqlx::SqlitePool; +use tokio::fs::File; + +use crate::util::{from_yaml_async_reader, AsyncFileExt}; +use crate::{Error, ResultExt}; + +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct RpcContextConfig { + pub bind: Option, + pub db: Option, + pub secret_store: Option, +} +pub struct RpcContextSeed { + pub bind: SocketAddr, + pub db: PatchDb, + pub secret_store: SqlitePool, + pub docker: Docker, +} + +#[derive(Clone)] +pub struct RpcContext(Arc); +impl RpcContext { + pub async fn init() -> Result { + let cfg_path = Path::new(crate::CONFIG_PATH); + let base = if let Some(f) = File::maybe_open(cfg_path) + .await + .with_ctx(|_| (crate::ErrorKind::Filesystem, cfg_path.display().to_string()))? + { + from_yaml_async_reader(f).await? + } else { + RpcContextConfig::default() + }; + let seed = Arc::new(RpcContextSeed { + bind: base.bind.unwrap_or(([127, 0, 0, 1], 5959).into()), + db: PatchDb::open( + base.db + .unwrap_or_else(|| Path::new("/mnt/embassy-os/embassy.db").to_owned()), + ) + .await?, + secret_store: SqlitePool::connect(&format!( + "sqlite://{}", + base.secret_store + .unwrap_or_else(|| Path::new("/mnt/embassy-os/secrets.db").to_owned()) + .display() + )) + .await?, + docker: Docker::connect_with_unix_defaults()?, + }); + Ok(Self(seed)) + } +} +impl Context for RpcContext { + fn host(&self) -> Host<&str> { + match self.0.bind.ip() { + IpAddr::V4(a) => Host::Ipv4(a), + IpAddr::V6(a) => Host::Ipv6(a), + } + } + fn port(&self) -> u16 { + self.0.bind.port() + } +} +impl Deref for RpcContext { + type Target = RpcContextSeed; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} diff --git a/appmgr/src/control.rs b/appmgr/src/control.rs deleted file mode 100644 index 03778b8f7..000000000 --- a/appmgr/src/control.rs +++ /dev/null @@ -1,237 +0,0 @@ -use std::path::Path; - -use futures::future::{BoxFuture, FutureExt}; -use linear_map::{set::LinearSet, LinearMap}; - -use crate::dependencies::{DependencyError, TaggedDependencyError}; -use crate::util::{from_yaml_async_reader, PersistencePath, YamlUpdateHandle}; -use crate::Error; - -pub async fn start_app(name: &str, update_metadata: bool) -> Result<(), Error> { - let lock = crate::util::lock_file( - format!( - "{}", - Path::new(crate::PERSISTENCE_DIR) - .join("apps") - .join(name) - .join("control.lock") - .display() - ), - true, - ) - .await?; - let status = crate::apps::status(name, false).await?.status; - if status == crate::apps::DockerStatus::Stopped { - if update_metadata { - crate::config::configure(name, None, None, false).await?; - crate::dependencies::update_binds(name).await?; - } - crate::apps::set_needs_restart(name, false).await?; - let mut running = YamlUpdateHandle::>::new_or_default( - PersistencePath::from_ref("running.yaml"), - ) - .await?; - let output = tokio::process::Command::new("docker") - .args(&["start", name]) - .stdout(std::process::Stdio::null()) - .output() - .await?; - crate::ensure_code!( - output.status.success(), - crate::error::DOCKER_ERROR, - "Failed to Start Application: {}", - std::str::from_utf8(&output.stderr).unwrap_or("Unknown Error") - ); - running.insert(name.to_owned()); - running.commit().await?; - } else if status == crate::apps::DockerStatus::Paused { - resume_app(name).await?; - } - crate::util::unlock(lock).await?; - Ok(()) -} - -pub async fn stop_app( - name: &str, - cascade: bool, - dry_run: bool, -) -> Result, Error> { - let mut res = LinearMap::new(); - if cascade { - stop_dependents(name, dry_run, DependencyError::NotRunning, &mut res).await?; - } - if !dry_run { - let lock = crate::util::lock_file( - format!( - "{}", - Path::new(crate::PERSISTENCE_DIR) - .join("apps") - .join(name) - .join("control.lock") - .display() - ), - true, - ) - .await?; - let mut running = YamlUpdateHandle::>::new_or_default( - PersistencePath::from_ref("running.yaml"), - ) - .await?; - log::info!("Stopping {}", name); - let output = tokio::process::Command::new("docker") - .args(&["stop", "-t", "25", name]) - .stdout(std::process::Stdio::null()) - .output() - .await?; - crate::ensure_code!( - output.status.success(), - crate::error::DOCKER_ERROR, - "Failed to Stop Application: {}", - std::str::from_utf8(&output.stderr).unwrap_or("Unknown Error") - ); - running.remove(name); - running.commit().await?; - crate::util::unlock(lock).await?; - } - Ok(res) -} - -pub async fn stop_dependents( - name: &str, - dry_run: bool, - err: DependencyError, - res: &mut LinearMap, -) -> Result<(), Error> { - fn stop_dependents_rec<'a>( - name: &'a str, - dry_run: bool, - err: DependencyError, - res: &'a mut LinearMap, - ) -> BoxFuture<'a, Result<(), Error>> { - async move { - for dependent in crate::apps::dependents(name, false).await? { - if crate::apps::status(&dependent, false).await?.status - != crate::apps::DockerStatus::Stopped - { - stop_dependents_rec(&dependent, dry_run, DependencyError::NotRunning, res) - .await?; - stop_app(&dependent, false, dry_run).await?; - res.insert( - dependent, - TaggedDependencyError { - dependency: name.to_owned(), - error: err.clone(), - }, - ); - } - } - Ok(()) - } - .boxed() - } - stop_dependents_rec(name, dry_run, err, res).await -} - -pub async fn restart_app(name: &str) -> Result<(), Error> { - stop_app(name, false, false).await?; - if let Err(e) = start_app(name, true).await { - log::warn!("Stopping dependents"); - stop_dependents( - name, - false, - crate::dependencies::DependencyError::NotRunning, - &mut linear_map::LinearMap::new(), - ) - .await?; - return Err(e); - } - Ok(()) -} - -pub async fn pause_app(name: &str) -> Result<(), Error> { - let lock = crate::util::lock_file( - format!( - "{}", - Path::new(crate::PERSISTENCE_DIR) - .join("apps") - .join(name) - .join("control.lock") - .display() - ), - true, - ) - .await?; - let output = tokio::process::Command::new("docker") - .args(&["pause", name]) - .stdout(std::process::Stdio::null()) - .output() - .await?; - crate::ensure_code!( - output.status.success(), - crate::error::DOCKER_ERROR, - "Failed to Pause Application: {}", - std::str::from_utf8(&output.stderr).unwrap_or("Unknown Error") - ); - - crate::util::unlock(lock).await?; - Ok(()) -} - -pub async fn resume_app(name: &str) -> Result<(), Error> { - let lock = crate::util::lock_file( - format!( - "{}", - Path::new(crate::PERSISTENCE_DIR) - .join("apps") - .join(name) - .join("control.lock") - .display() - ), - true, - ) - .await?; - let output = tokio::process::Command::new("docker") - .args(&["unpause", name]) - .stdout(std::process::Stdio::null()) - .output() - .await?; - crate::ensure_code!( - output.status.success(), - crate::error::DOCKER_ERROR, - "Failed to Resume Application: {}", - std::str::from_utf8(&output.stderr).unwrap_or("Unknown Error") - ); - crate::util::unlock(lock).await?; - Ok(()) -} - -pub async fn repair_app_status() -> Result<(), Error> { - let mut running_file = PersistencePath::from_ref("running.yaml") - .maybe_read(false) - .await - .transpose()?; - let running: Vec = if let Some(f) = running_file.as_mut() { - from_yaml_async_reader::<_, &mut tokio::fs::File>(f).await? - } else { - Vec::new() - }; - for name in running { - let lock = crate::util::lock_file( - format!( - "{}", - Path::new(crate::PERSISTENCE_DIR) - .join("apps") - .join(&name) - .join("control.lock") - .display() - ), - true, - ) - .await?; - if crate::apps::status(&name, false).await?.status == crate::apps::DockerStatus::Stopped { - start_app(&name, true).await?; - } - crate::util::unlock(lock).await?; - } - Ok(()) -} diff --git a/appmgr/src/db/mod.rs b/appmgr/src/db/mod.rs new file mode 100644 index 000000000..88f72829d --- /dev/null +++ b/appmgr/src/db/mod.rs @@ -0,0 +1,4 @@ +pub mod model; +pub mod util; + +pub use model::DatabaseModel; diff --git a/appmgr/src/db/model.rs b/appmgr/src/db/model.rs new file mode 100644 index 000000000..9e77ae1a2 --- /dev/null +++ b/appmgr/src/db/model.rs @@ -0,0 +1,199 @@ +use std::net::Ipv4Addr; +use std::sync::Arc; + +use indexmap::{IndexMap, IndexSet}; +use patch_db::json_ptr::JsonPointer; +use patch_db::{HasModel, Map, MapModel, OptionModel}; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::config::spec::{PackagePointerSpecVariant, SystemPointerSpec}; +use crate::id::InterfaceId; +use crate::install::progress::InstallProgress; +use crate::net::Network; +use crate::s9pk::manifest::{Manifest, PackageId}; +use crate::status::health_check::HealthCheckId; +use crate::status::Status; +use crate::util::Version; +use crate::Error; + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +pub struct Database { + #[model] + pub server_info: ServerInfo, + #[model] + pub package_data: AllPackageData, + pub broken_packages: Vec, + #[model] + pub network: Network, + pub ui: Value, +} +impl Database { + pub fn init() -> Self { + // TODO + Database { + server_info: ServerInfo { + id: "c3ad21d8".to_owned(), + version: emver::Version::new(0, 3, 0, 0).into(), + lan_address: "https://start9-c3ad21d8.local".parse().unwrap(), + tor_address: + "http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion" + .parse() + .unwrap(), + updating: false, + registry: "https://registry.start9.com".parse().unwrap(), + unread_notification_count: 0, + }, + package_data: AllPackageData::default(), + broken_packages: Vec::new(), + network: Network::default(), + ui: Value::Object(Default::default()), + } + } +} +impl DatabaseModel { + pub fn new() -> Self { + Self::from(JsonPointer::default()) + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +pub struct ServerInfo { + id: String, + version: Version, + lan_address: Url, + tor_address: Url, + updating: bool, + registry: Url, + unread_notification_count: u64, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct AllPackageData(pub IndexMap); +impl Map for AllPackageData { + type Key = PackageId; + type Value = PackageDataEntry; + fn get(&self, key: &Self::Key) -> Option<&Self::Value> { + self.0.get(key) + } +} +impl HasModel for AllPackageData { + type Model = MapModel; +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct StaticFiles { + license: Url, + instructions: Url, + icon: Url, +} +impl StaticFiles { + pub fn local(id: &PackageId, version: &Version, icon_type: &str) -> Result { + Ok(StaticFiles { + license: format!("/public/package-data/{}/{}/LICENSE.md", id, version).parse()?, + instructions: format!("/public/package-data/{}/{}/INSTRUCTIONS.md", id, version) + .parse()?, + icon: format!("/public/package-data/{}/{}/icon.{}", id, version, icon_type).parse()?, + }) + } + pub fn remote(id: &PackageId, version: &Version, icon_type: &str) -> Result { + Ok(StaticFiles { + license: format!("/registry/packages/{}/{}/LICENSE.md", id, version).parse()?, + instructions: format!("/registry/packages/{}/{}/INSTRUCTIONS.md", id, version) + .parse()?, + icon: format!("/registry/packages/{}/{}/icon.{}", id, version, icon_type).parse()?, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(tag = "state")] +#[serde(rename_all = "kebab-case")] +pub enum PackageDataEntry { + #[serde(rename_all = "kebab-case")] + Installing { + static_files: StaticFiles, + temp_manifest: Manifest, + install_progress: Arc, + }, // { state: "installing", 'install-progress': InstallProgress } + #[serde(rename_all = "kebab-case")] + Updating { + static_files: StaticFiles, + temp_manifest: Manifest, + installed: InstalledPackageDataEntry, + install_progress: Arc, + }, + #[serde(rename_all = "kebab-case")] + Removing { + static_files: StaticFiles, + temp_manifest: Manifest, + }, + #[serde(rename_all = "kebab-case")] + Installed { + static_files: StaticFiles, + installed: InstalledPackageDataEntry, + }, +} +impl PackageDataEntryModel { + pub fn installed(self) -> OptionModel { + self.0.child("installed").into() + } + pub fn install_progress(self) -> OptionModel { + self.0.child("install-progress").into() + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +pub struct InstalledPackageDataEntry { + #[model] + pub manifest: Manifest, + #[model] + pub status: Status, + pub system_pointers: Vec, + #[model] + pub current_dependents: IndexMap, + #[model] + pub current_dependencies: IndexMap, + #[model] + pub interface_info: InterfaceInfo, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +pub struct CurrentDependencyInfo { + pub pointers: Vec, + pub health_checks: IndexSet, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +pub struct InterfaceInfo { + pub ip: Ipv4Addr, + #[model] + pub addresses: InterfaceAddressMap, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct InterfaceAddressMap(pub IndexMap); +impl Map for InterfaceAddressMap { + type Key = InterfaceId; + type Value = InterfaceAddresses; + fn get(&self, key: &Self::Key) -> Option<&Self::Value> { + self.0.get(key) + } +} +impl HasModel for InterfaceAddressMap { + type Model = MapModel; +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +pub struct InterfaceAddresses { + pub tor_address: Option, + pub lan_address: Option, +} diff --git a/appmgr/src/db/util.rs b/appmgr/src/db/util.rs new file mode 100644 index 000000000..606dff1d9 --- /dev/null +++ b/appmgr/src/db/util.rs @@ -0,0 +1,10 @@ +use std::sync::Arc; + +use patch_db::Revision; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WithRevision { + pub response: T, + pub revision: Arc, +} diff --git a/appmgr/src/dependencies.rs b/appmgr/src/dependencies.rs index f8c8b10a9..001f5d788 100644 --- a/appmgr/src/dependencies.rs +++ b/appmgr/src/dependencies.rs @@ -1,260 +1,245 @@ -use std::borrow::Cow; -use std::path::Path; +use std::collections::HashMap; -use emver::{Version, VersionRange}; -use linear_map::LinearMap; -use rand::SeedableRng; +use anyhow::anyhow; +use emver::VersionRange; +use indexmap::{IndexMap, IndexSet}; +use patch_db::{DbHandle, DiffPatch, HasModel, Map, MapModel}; +use serde::{Deserialize, Serialize}; -use crate::config::{Config, ConfigRuleEntryWithSuggestions, ConfigSpec}; -use crate::manifest::ManifestLatest; -use crate::Error; -use crate::ResultExt as _; +use crate::action::ActionImplementation; +use crate::config::{Config, ConfigSpec}; +use crate::id::InterfaceId; +use crate::net::host::Hosts; +use crate::s9pk::manifest::PackageId; +use crate::status::health_check::{HealthCheckId, HealthCheckResult, HealthCheckResultVariant}; +use crate::status::{DependencyErrors, MainStatus, Status}; +use crate::util::Version; +use crate::{Error, ResultExt as _}; -#[derive(Clone, Debug, Fail, serde::Serialize)] +#[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] +#[serde(tag = "type")] pub enum DependencyError { - NotInstalled, // "not-installed" - NotRunning, // "not-running" + NotInstalled, // { "type": "not-installed" } IncorrectVersion { expected: VersionRange, received: Version, - }, // { "incorrect-version": { "expected": "0.1.0", "received": "^0.2.0" } } - ConfigUnsatisfied(Vec), // { "config-unsatisfied": ["Bitcoin Core must have pruning set to manual."] } - PointerUpdateError(String), // { "pointer-update-error": "Bitcoin Core RPC Port must not be 18332" } - Other(String), // { "other": "Well fuck." } + }, // { "type": "incorrect-version", "expected": "0.1.0", "received": "^0.2.0" } + ConfigUnsatisfied { + error: String, + }, // { "type": "config-unsatisfied", "error": "Bitcoin Core must have pruning set to manual." } + NotRunning, // { "type": "not-running" } + HealthChecksFailed { + failures: IndexMap, + }, // { "type": "health-checks-failed", "checks": { "rpc": { "time": "2021-05-11T18:21:29Z", "result": "warming-up" } } } +} +impl DependencyError { + pub fn merge_with(self, other: DependencyError) -> DependencyError { + use DependencyError::*; + match (self, other) { + (NotInstalled, _) => NotInstalled, + (_, NotInstalled) => NotInstalled, + (IncorrectVersion { expected, received }, _) => IncorrectVersion { expected, received }, + (_, IncorrectVersion { expected, received }) => IncorrectVersion { expected, received }, + (ConfigUnsatisfied { error: e0 }, ConfigUnsatisfied { error: e1 }) => { + ConfigUnsatisfied { + error: e0 + "\n" + &e1, + } + } + (ConfigUnsatisfied { error }, _) => ConfigUnsatisfied { error }, + (_, ConfigUnsatisfied { error }) => ConfigUnsatisfied { error }, + (NotRunning, _) => NotRunning, + (_, NotRunning) => NotRunning, + (HealthChecksFailed { failures: f0 }, HealthChecksFailed { failures: f1 }) => { + HealthChecksFailed { + failures: f0.into_iter().chain(f1.into_iter()).collect(), + } + } + } + } } impl std::fmt::Display for DependencyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use DependencyError::*; match self { NotInstalled => write!(f, "Not Installed"), - NotRunning => write!(f, "Not Running"), IncorrectVersion { expected, received } => write!( f, "Incorrect Version: Expected {}, Received {}", - expected, received + expected, + received.as_str() ), - ConfigUnsatisfied(rules) => { - write!(f, "Configuration Rule(s) Violated: {}", rules.join(", ")) + ConfigUnsatisfied { error } => { + write!(f, "Configuration Requirements Not Satisfied: {}", error) + } + NotRunning => write!(f, "Not Running"), + HealthChecksFailed { failures } => { + write!(f, "Failed Health Check(s): ")?; + let mut comma = false; + for (check, res) in failures { + if !comma { + comma = true; + } else { + write!(f, ", "); + } + write!(f, "{} @ {} {}", check, res.time, res.result)?; + } + Ok(()) } - PointerUpdateError(e) => write!(f, "Pointer Update Caused {}", e), - Other(e) => write!(f, "System Error: {}", e), } } } -#[derive(Clone, Debug, serde::Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct TaggedDependencyError { - pub dependency: String, + pub dependency: PackageId, pub error: DependencyError, } -impl std::fmt::Display for TaggedDependencyError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}: {}", self.dependency, self.error) - } + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct BreakageRes { + pub patch: DiffPatch, + pub breakages: IndexMap, } -#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] -pub struct Dependencies(pub LinearMap); +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Dependencies(pub IndexMap); +impl Map for Dependencies { + type Key = PackageId; + type Value = DepInfo; + fn get(&self, key: &Self::Key) -> Option<&Self::Value> { + self.0.get(key) + } +} +impl HasModel for Dependencies { + type Model = MapModel; +} -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] #[serde(rename_all = "kebab-case")] pub struct DepInfo { pub version: VersionRange, pub optional: Option, pub description: Option, + pub critical: bool, #[serde(default)] - pub mount_public: bool, - #[serde(default)] - pub mount_shared: bool, - #[serde(default)] - pub config: Vec, + #[model] + pub config: Option, } impl DepInfo { - pub async fn satisfied( + pub async fn satisfied( &self, - dependency_id: &str, + db: &mut Db, + dependency_id: &PackageId, dependency_config: Option, // fetch if none - dependent_id: &str, + dependent_id: &PackageId, + dependent_version: &Version, dependent_config: &Config, ) -> Result, Error> { - let info = if let Some(info) = crate::apps::list_info().await?.remove(dependency_id) { + let dependency = crate::db::DatabaseModel::new() + .package_data() + .idx_model(dependency_id) + .and_then(|pde| pde.installed()) + .get(db) + .await?; + let info = if let Some(info) = &*dependency { info } else { return Ok(Err(DependencyError::NotInstalled)); }; - if !&info.version.satisfies(&self.version) { + if !&info.manifest.version.satisfies(&self.version) { return Ok(Err(DependencyError::IncorrectVersion { expected: self.version.clone(), - received: info.version.clone(), + received: info.manifest.version.clone(), })); } + let hosts = crate::db::DatabaseModel::new() + .network() + .hosts() + .get(db) + .await?; let dependency_config = if let Some(cfg) = dependency_config { cfg + } else if let Some(cfg_info) = &info.manifest.config { + cfg_info + .get( + dependency_id, + &info.manifest.version, + &info.manifest.volumes, + &hosts, + ) + .await? + .config + .unwrap_or_default() } else { - let app_config = crate::apps::config(dependency_id).await?; - if let Some(cfg) = app_config.config { - cfg - } else { - app_config - .spec - .gen(&mut rand::rngs::StdRng::from_entropy(), &None) - .unwrap_or_default() - } + Config::default() }; - let mut errors = Vec::new(); - let mut cfgs = LinearMap::with_capacity(2); - cfgs.insert(dependency_id, Cow::Borrowed(&dependency_config)); - cfgs.insert(dependent_id, Cow::Borrowed(dependent_config)); - for rule in self.config.iter() { - if !(rule.entry.rule.compiled)(&dependency_config, &cfgs) { - errors.push(rule.entry.description.clone()); + if let Some(cfg_req) = &self.config { + if let Err(e) = cfg_req + .check(dependent_id, dependent_version, dependent_config) + .await + { + if e.kind == crate::ErrorKind::ConfigRulesViolation { + return Ok(Err(DependencyError::ConfigUnsatisfied { + error: format!("{}", e), + })); + } else { + return Err(e); + } } } - if !errors.is_empty() { - return Ok(Err(DependencyError::ConfigUnsatisfied(errors))); - } - if crate::apps::status(dependency_id, false).await?.status - != crate::apps::DockerStatus::Running - { - return Ok(Err(DependencyError::NotRunning)); + match &info.status.main { + MainStatus::BackingUp { + started: Some(_), + health, + } + | MainStatus::Running { health, .. } => { + let mut failures = IndexMap::with_capacity(health.len()); + for (check, res) in health { + if !matches!(res.result, HealthCheckResultVariant::Success) { + failures.insert(check.clone(), res.clone()); + } + } + if !failures.is_empty() { + return Ok(Err(DependencyError::HealthChecksFailed { failures })); + } + } + _ => return Ok(Err(DependencyError::NotRunning)), } Ok(Ok(())) } } -#[derive(Debug, serde::Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] #[serde(rename_all = "kebab-case")] -pub struct AppDepInfo { - #[serde(flatten)] - pub info: DepInfo, - pub required: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, +pub struct DependencyConfig { + check: ActionImplementation, + auto_configure: ActionImplementation, } - -#[derive(Debug, Default, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct AppDependencies(pub LinearMap); - -pub async fn check_dependencies( - manifest: ManifestLatest, - dependent_config: &Config, - dependent_config_spec: &ConfigSpec, -) -> Result { - let mut deps = AppDependencies::default(); - for (dependency_id, dependency_info) in manifest.dependencies.0.into_iter() { - let required = dependency_info.optional.is_none() - || dependent_config_spec.requires(&dependency_id, dependent_config); - let error = dependency_info - .satisfied(&dependency_id, None, &manifest.id, dependent_config) +impl DependencyConfig { + pub async fn check( + &self, + dependent_id: &PackageId, + dependent_version: &Version, + dependency_config: &Config, + ) -> Result, Error> { + Ok(self + .check + .sandboxed(dependent_id, dependent_version, Some(dependency_config)) .await? - .err(); - let app_dep_info = AppDepInfo { - error, - required, - info: dependency_info, - }; - deps.0.insert(dependency_id, app_dep_info); + .map_err(|(_, e)| e)) } - Ok(deps) -} - -pub async fn auto_configure( - dependent: &str, - dependency: &str, - dry_run: bool, -) -> Result { - let (dependent_config, mut dependency_config, manifest) = futures::try_join!( - crate::apps::config_or_default(dependent), - crate::apps::config_or_default(dependency), - crate::apps::manifest(dependent) - )?; - let mut cfgs = LinearMap::new(); - cfgs.insert(dependent, Cow::Borrowed(&dependent_config)); - cfgs.insert(dependency, Cow::Owned(dependency_config.clone())); - let dep_info = manifest - .dependencies - .0 - .get(dependency) - .ok_or_else(|| failure::format_err!("{} Does Not Depend On {}", dependent, dependency)) - .no_code()?; - for rule in &dep_info.config { - if let Err(e) = rule.apply(dependency, &mut dependency_config, &mut cfgs) { - log::warn!("Rule Unsatisfied After Applying Suggestions: {}", e); - } + pub async fn auto_configure( + &self, + dependent_id: &PackageId, + dependent_version: &Version, + old: &Config, + ) -> Result { + self.auto_configure + .sandboxed(dependent_id, dependent_version, Some(old)) + .await? + .map_err(|e| Error::new(anyhow!("{}", e.1), crate::ErrorKind::AutoConfigure)) } - crate::config::configure(dependency, Some(dependency_config), None, dry_run).await -} - -pub async fn update_binds(dependent_id: &str) -> Result<(), Error> { - let dependent_manifest = crate::apps::manifest(dependent_id).await?; - let dependency_manifests = futures::future::try_join_all( - dependent_manifest - .dependencies - .0 - .into_iter() - .filter(|(_, info)| info.mount_public || info.mount_shared) - .map(|(id, info)| async { - Ok::<_, Error>(if crate::apps::list_info().await?.contains_key(&id) { - let man = crate::apps::manifest(&id).await?; - Some((id, info, man)) - } else { - None - }) - }), - ) - .await?; - // i just have a gut feeling this shouldn't be concurrent - for (dependency_id, info, dependency_manifest) in - dependency_manifests.into_iter().filter_map(|a| a) - { - match (dependency_manifest.public, info.mount_public) { - (Some(public), true) => { - let public_path = Path::new(crate::VOLUMES).join(&dependency_id).join(public); - if let Ok(metadata) = tokio::fs::metadata(&public_path).await { - if metadata.is_dir() { - crate::disks::bind( - public_path, - Path::new(crate::VOLUMES) - .join(&dependent_id) - .join("start9") - .join("public") - .join(&dependency_id), - true, - ) - .await? - } - } - } - _ => (), - } - match (dependency_manifest.shared, info.mount_shared) { - (Some(shared), true) => { - let shared_path = Path::new(crate::VOLUMES) - .join(&dependency_id) - .join(shared) - .join(dependent_id); // namespaced by dependent - tokio::fs::create_dir_all(&shared_path).await?; - if let Ok(metadata) = tokio::fs::metadata(&shared_path).await { - if metadata.is_dir() { - crate::disks::bind( - shared_path, - Path::new(crate::VOLUMES) - .join(&dependent_id) - .join("start9") - .join("shared") - .join(&dependency_id), - false, - ) - .await? - } - } - } - _ => (), - } - } - - Ok(()) } diff --git a/appmgr/src/disks.rs b/appmgr/src/disks.rs deleted file mode 100644 index 38e4dc709..000000000 --- a/appmgr/src/disks.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::path::Path; - -use failure::ResultExt as _; -use futures::future::try_join_all; - -use crate::util::Invoke; -use crate::Error; -use crate::ResultExt as _; - -pub const FSTAB: &'static str = "/etc/fstab"; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct DiskInfo { - pub logicalname: String, - pub size: String, - pub description: Option, -} - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct PartitionInfo { - pub logicalname: String, - pub is_mounted: bool, - pub size: Option, - pub label: Option, -} - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct Disk { - #[serde(flatten)] - pub info: DiskInfo, - pub partitions: Vec, -} - -pub async fn list() -> Result, Error> { - let output = tokio::process::Command::new("parted") - .arg("-lm") - .invoke("GNU Parted") - .await?; - let output_str = std::str::from_utf8(&output).no_code()?; - let disks = output_str.split("\n\n").filter_map(|s| -> Option { - let mut lines = s.split("\n"); - let has_size = lines.next()? == "BYT;"; - let disk_info_line = lines.next()?; - let mut disk_info_iter = disk_info_line.split(":"); - let logicalname = disk_info_iter.next()?.to_owned(); - let partition_prefix = if logicalname.ends_with(|c: char| c.is_digit(10)) { - logicalname.clone() + "p" - } else { - logicalname.clone() - }; - let size = disk_info_iter.next()?.to_owned(); - disk_info_iter.next()?; // transport-type - disk_info_iter.next()?; // logical-sector-size - disk_info_iter.next()?; // physical-sector-size - disk_info_iter.next()?; // partition-table-type - let description = disk_info_iter.next()?; - let description = if description.is_empty() { - None - } else { - Some(description.to_owned()) - }; - let info = DiskInfo { - logicalname, - size, - description, - }; - let partitions = lines - .filter_map(|partition_info_line| -> Option { - let mut partition_info_iter = partition_info_line.split(":"); - let partition_idx = partition_info_iter.next()?; - let logicalname = partition_prefix.clone() + partition_idx; - let size = if has_size { - partition_info_iter.next()?; // begin - partition_info_iter.next()?; // end - Some(partition_info_iter.next()?.to_owned()) - } else { - None - }; - Some(PartitionInfo { - logicalname, - is_mounted: false, - size, - label: None, - }) - }) - .collect(); - Some(Disk { info, partitions }) - }); - try_join_all(disks.map(|disk| async move { - Ok(Disk { - info: disk.info, - partitions: try_join_all(disk.partitions.into_iter().map(|mut partition| async move { - let mut blkid_command = tokio::process::Command::new("blkid"); - let (blkid_res, findmnt_status) = futures::join!( - blkid_command - .arg(&partition.logicalname) - .arg("-s") - .arg("LABEL") - .arg("-o") - .arg("value") - .invoke("BLKID"), - tokio::process::Command::new("findmnt") - .arg(&partition.logicalname) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - ); - let blkid_output = blkid_res?; - let label = std::str::from_utf8(&blkid_output).no_code()?.trim(); - if !label.is_empty() { - partition.label = Some(label.to_owned()); - } - if findmnt_status?.success() { - partition.is_mounted = true; - } - Ok::<_, Error>(partition) - })) - .await?, - }) - })) - .await -} - -pub async fn mount>(logicalname: &str, mount_point: P) -> Result<(), Error> { - let is_mountpoint = tokio::process::Command::new("mountpoint") - .arg(mount_point.as_ref()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - .await?; - if is_mountpoint.success() { - unmount(mount_point.as_ref()).await?; - } - tokio::fs::create_dir_all(&mount_point).await?; - let mount_output = tokio::process::Command::new("mount") - .arg(logicalname) - .arg(mount_point.as_ref()) - .output() - .await?; - crate::ensure_code!( - mount_output.status.success(), - crate::error::FILESYSTEM_ERROR, - "Error Mounting Drive: {}", - std::str::from_utf8(&mount_output.stderr).unwrap_or("Unknown Error") - ); - Ok(()) -} - -pub async fn bind, P1: AsRef>( - src: P0, - dst: P1, - read_only: bool, -) -> Result<(), Error> { - log::info!( - "Binding {} to {}", - src.as_ref().display(), - dst.as_ref().display() - ); - let is_mountpoint = tokio::process::Command::new("mountpoint") - .arg(dst.as_ref()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - .await?; - if is_mountpoint.success() { - unmount(dst.as_ref()).await?; - } - tokio::fs::create_dir_all(&dst).await?; - let mut mount_cmd = tokio::process::Command::new("mount"); - mount_cmd.arg("--bind"); - if read_only { - mount_cmd.arg("-o").arg("ro"); - } - let mount_output = mount_cmd - .arg(src.as_ref()) - .arg(dst.as_ref()) - .output() - .await?; - crate::ensure_code!( - mount_output.status.success(), - crate::error::FILESYSTEM_ERROR, - "Error Binding {} to {}: {}", - src.as_ref().display(), - dst.as_ref().display(), - std::str::from_utf8(&mount_output.stderr).unwrap_or("Unknown Error") - ); - Ok(()) -} - -pub async fn unmount>(mount_point: P) -> Result<(), Error> { - log::info!("Unmounting {}.", mount_point.as_ref().display()); - let umount_output = tokio::process::Command::new("umount") - .arg(mount_point.as_ref()) - .output() - .await?; - crate::ensure_code!( - umount_output.status.success(), - crate::error::FILESYSTEM_ERROR, - "Error Unmounting Drive: {}: {}", - mount_point.as_ref().display(), - std::str::from_utf8(&umount_output.stderr).unwrap_or("Unknown Error") - ); - tokio::fs::remove_dir_all(mount_point.as_ref()) - .await - .with_context(|e| format!("rm {}: {}", mount_point.as_ref().display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - Ok(()) -} - -#[must_use] -pub struct MountGuard> { - path: Option

, -} -impl> MountGuard

{ - pub async fn new(logicalname: &str, mount_point: P) -> Result { - mount(logicalname, mount_point.as_ref()).await?; - Ok(Self { - path: Some(mount_point), - }) - } - pub async fn unmount(mut self) -> Result<(), Error> { - if let Some(ref path) = self.path { - unmount(path).await?; - self.path = None; - } - Ok(()) - } -} -impl> Drop for MountGuard

{ - fn drop(&mut self) { - if let Some(ref path) = self.path { - tokio::runtime::Runtime::new() - .unwrap() - .block_on(unmount(path)) - .unwrap() - } - } -} diff --git a/appmgr/src/error.rs b/appmgr/src/error.rs index 6b3f12c1b..4120a95d6 100644 --- a/appmgr/src/error.rs +++ b/appmgr/src/error.rs @@ -1,107 +1,245 @@ use std::fmt::Display; -pub const GENERAL_ERROR: i32 = 1; -pub const FILESYSTEM_ERROR: i32 = 2; -pub const DOCKER_ERROR: i32 = 3; -pub const CFG_SPEC_VIOLATION: i32 = 4; -pub const CFG_RULES_VIOLATION: i32 = 5; -pub const NOT_FOUND: i32 = 6; -pub const INVALID_BACKUP_PASSWORD: i32 = 7; -pub const VERSION_INCOMPATIBLE: i32 = 8; -pub const NETWORK_ERROR: i32 = 9; -pub const REGISTRY_ERROR: i32 = 10; -pub const SERDE_ERROR: i32 = 11; +use anyhow::anyhow; +use patch_db::Revision; +use rpc_toolkit::yajrc::RpcError; -#[derive(Debug, Fail)] -#[fail(display = "{}", _0)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorKind { + Unknown = 1, + Filesystem = 2, + Docker = 3, + ConfigSpecViolation = 4, + ConfigRulesViolation = 5, + NotFound = 6, + InvalidPassword = 7, + VersionIncompatible = 8, + Network = 9, + Registry = 10, + Serialization = 11, + Deserialization = 12, + Utf8 = 13, + ParseVersion = 14, + Duplicity = 15, + Nginx = 16, + Dependency = 17, + ParseS9pk = 18, + ParseUrl = 19, + GParted = 20, + Blkid = 21, + InvalidOnionAddress = 22, + Pack = 23, + ValidateS9pk = 24, + OpenSSL = 25, + Tor = 26, + ConfigGen = 27, + ParseNumber = 28, + Database = 29, + InvalidPackageId = 30, + InvalidSignature = 31, + Backup = 32, + Restore = 33, + Authorization = 34, + AutoConfigure = 35, + Action = 36, + RateLimited = 37, + InvalidRequest = 38, + MigrationFailed = 39, +} +impl ErrorKind { + pub fn as_str(&self) -> &'static str { + use ErrorKind::*; + match self { + Unknown => "Unknown Error", + Filesystem => "Filesystem I/O Error", + Docker => "Docker Error", + ConfigSpecViolation => "Config Spec Violation", + ConfigRulesViolation => "Config Rules Violation", + NotFound => "Not Found", + InvalidPassword => "Invalid Password", + VersionIncompatible => "Version Incompatible", + Network => "Network Error", + Registry => "Registry Error", + Serialization => "Serialization Error", + Deserialization => "Deserialization Error", + Utf8 => "UTF-8 Parse Error", + ParseVersion => "Version Parsing Error", + Duplicity => "Duplicity Error", + Nginx => "Nginx Error", + Dependency => "Dependency Error", + ParseS9pk => "S9PK Parsing Error", + ParseUrl => "URL Parsing Error", + GParted => "GNU Parted Error", + Blkid => "BLKID Error", + InvalidOnionAddress => "Invalid Onion Address", + Pack => "Pack Error", + ValidateS9pk => "S9PK Validation Error", + OpenSSL => "OpenSSL Error", + Tor => "Tor Daemon Error", + ConfigGen => "Config Generation Error", + ParseNumber => "Number Parsing Error", + Database => "Database Error", + InvalidPackageId => "Invalid Package ID", + InvalidSignature => "Invalid Signature", + Backup => "Backup Error", + Restore => "Restore Error", + Authorization => "Unauthorized", + AutoConfigure => "Auto-Configure Error", + Action => "Action Failed", + RateLimited => "Rate Limited", + InvalidRequest => "Invalid Request", + MigrationFailed => "Migration Failed", + } + } +} +impl Display for ErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[derive(Debug)] pub struct Error { - pub failure: failure::Error, - pub code: Option, + pub source: anyhow::Error, + pub kind: ErrorKind, + pub revision: Option, +} +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.kind.as_str(), self.source) + } } impl Error { - pub fn new>(e: E, code: Option) -> Self { + pub fn new>(source: E, kind: ErrorKind) -> Self { Error { - failure: e.into(), - code, - } - } - pub fn from>(e: E) -> Self { - Error { - failure: e.into(), - code: None, - } - } -} -impl From for Error { - fn from(e: failure::Error) -> Self { - Error { - failure: e, - code: None, + source: source.into(), + kind, + revision: None, } } } impl From for Error { fn from(e: std::io::Error) -> Self { - Error { - failure: e.into(), - code: Some(2), + Error::new(e, ErrorKind::Filesystem) + } +} +impl From for Error { + fn from(e: std::str::Utf8Error) -> Self { + Error::new(e, ErrorKind::Utf8) + } +} +impl From for Error { + fn from(e: std::string::FromUtf8Error) -> Self { + Error::new(e, ErrorKind::Utf8) + } +} +impl From for Error { + fn from(e: emver::ParseError) -> Self { + Error::new(e, ErrorKind::ParseVersion) + } +} +impl From for Error { + fn from(e: rpc_toolkit::url::ParseError) -> Self { + Error::new(e, ErrorKind::ParseUrl) + } +} +impl From for Error { + fn from(e: std::num::ParseIntError) -> Self { + Error::new(e, ErrorKind::ParseNumber) + } +} +impl From for Error { + fn from(e: std::num::ParseFloatError) -> Self { + Error::new(e, ErrorKind::ParseNumber) + } +} +impl From for Error { + fn from(e: patch_db::Error) -> Self { + Error::new(e, ErrorKind::Database) + } +} +impl From for Error { + fn from(e: sqlx::Error) -> Self { + Error::new(e, ErrorKind::Database) + } +} +impl From for Error { + fn from(e: ed25519_dalek::SignatureError) -> Self { + Error::new(e, ErrorKind::InvalidSignature) + } +} +impl From for Error { + fn from(e: bollard::errors::Error) -> Self { + Error::new(e, ErrorKind::Docker) + } +} +impl From for RpcError { + fn from(e: Error) -> Self { + let mut data_object = serde_json::Map::with_capacity(2); + data_object.insert("message".to_owned(), format!("{}", e).into()); + data_object.insert( + "revision".to_owned(), + match serde_json::to_value(&e.revision) { + Ok(a) => a, + Err(e) => { + log::warn!("Error serializing revision for Error object: {}", e); + serde_json::Value::Null + } + }, + ); + RpcError { + code: e.kind as i32, + message: e.kind.as_str().into(), + data: Some(data_object.into()), } } } + pub trait ResultExt where Self: Sized, { - fn with_code(self, code: i32) -> Result; - fn with_ctx (Option, D), D: Display + Send + Sync + 'static>( + fn with_kind(self, kind: ErrorKind) -> Result; + fn with_ctx (ErrorKind, D), D: Display + Send + Sync + 'static>( self, f: F, ) -> Result; - fn no_code(self) -> Result; } impl ResultExt for Result where - failure::Error: From, + anyhow::Error: From, { - fn with_code(self, code: i32) -> Result { - #[cfg(not(feature = "production"))] - assert!(code != 0); + fn with_kind(self, kind: ErrorKind) -> Result { self.map_err(|e| Error { - failure: e.into(), - code: Some(code), + source: e.into(), + kind, + revision: None, }) } - fn with_ctx (Option, D), D: Display + Send + Sync + 'static>( + fn with_ctx (ErrorKind, D), D: Display + Send + Sync + 'static>( self, f: F, ) -> Result { self.map_err(|e| { - let (code, ctx) = f(&e); - let failure = failure::Error::from(e).context(ctx); + let (kind, ctx) = f(&e); + let source = anyhow::Error::from(e); + let ctx = format!("{}: {}", ctx, source); + let source = source.context(ctx); Error { - code, - failure: failure.into(), + kind, + source: source.into(), + revision: None, } }) } - - fn no_code(self) -> Result { - self.map_err(|e| Error { - failure: e.into(), - code: None, - }) - } } #[macro_export] macro_rules! ensure_code { ($x:expr, $c:expr, $fmt:expr $(, $arg:expr)*) => { if !($x) { - return Err(crate::Error { - failure: format_err!($fmt, $($arg, )*), - code: Some($c), - }); + return Err(crate::Error::new(anyhow!($fmt, $($arg, )*), $c)); } }; } diff --git a/appmgr/src/id.rs b/appmgr/src/id.rs new file mode 100644 index 000000000..3a4e8dca0 --- /dev/null +++ b/appmgr/src/id.rs @@ -0,0 +1,192 @@ +use std::borrow::{Borrow, Cow}; +use std::fmt::Debug; +use std::path::Path; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::util::Version; +use crate::Error; + +pub const SYSTEM_ID: Id<&'static str> = Id("SYSTEM"); + +#[derive(Debug, thiserror::Error)] +#[error("Invalid ID")] +pub struct InvalidId; +impl From for Error { + fn from(err: InvalidId) -> Self { + Error::new(err, crate::error::ErrorKind::InvalidPackageId) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct IdUnchecked>(pub S); +impl<'de> Deserialize<'de> for IdUnchecked> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = IdUnchecked>; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a valid ID") + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(IdUnchecked(Cow::Owned(v.to_owned()))) + } + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + Ok(IdUnchecked(Cow::Owned(v))) + } + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + Ok(IdUnchecked(Cow::Borrowed(v))) + } + } + deserializer.deserialize_any(Visitor) + } +} +impl<'de> Deserialize<'de> for IdUnchecked { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(IdUnchecked(String::deserialize(deserializer)?)) + } +} +impl<'de> Deserialize<'de> for IdUnchecked<&'de str> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(IdUnchecked(<&'de str>::deserialize(deserializer)?)) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Id = String>(S); +impl> Id { + pub fn try_from(value: S) -> Result { + if value + .as_ref() + .chars() + .all(|c| c.is_ascii_lowercase() || c == '-') + { + Ok(Id(value)) + } else { + Err(InvalidId) + } + } +} +impl<'a> Id<&'a str> { + pub fn owned(&self) -> Id { + Id(self.0.to_owned()) + } +} +impl> std::fmt::Display for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.as_ref()) + } +} +impl> AsRef for Id { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} +impl> Borrow for Id { + fn borrow(&self) -> &str { + self.0.as_ref() + } +} +impl<'de, S> Deserialize<'de> for Id +where + S: AsRef, + IdUnchecked: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let unchecked: IdUnchecked = Deserialize::deserialize(deserializer)?; + Id::try_from(unchecked.0).map_err(serde::de::Error::custom) + } +} +impl> Serialize for Id { + fn serialize(&self, serializer: Ser) -> Result + where + Ser: Serializer, + { + serializer.serialize_str(self.as_ref()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] +pub struct ImageId = String>(Id); +impl> std::fmt::Display for ImageId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } +} +impl> ImageId { + pub fn for_package>, S0: AsRef>( + &self, + pkg_id: PkgId, + pkg_version: Option<&Version>, + ) -> String { + format!( + "start9/{}/{}:{}", + pkg_id.as_ref(), + self.0, + pkg_version.map(|v| { v.as_str() }).unwrap_or("latest") + ) + } +} +impl<'de, S> Deserialize<'de> for ImageId +where + S: AsRef, + Id: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(ImageId(Deserialize::deserialize(deserializer)?)) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] +pub struct InterfaceId = String>(Id); +impl> std::fmt::Display for InterfaceId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } +} +impl> AsRef for InterfaceId { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} +impl<'de, S> Deserialize<'de> for InterfaceId +where + S: AsRef, + Id: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(InterfaceId(Deserialize::deserialize(deserializer)?)) + } +} +impl> AsRef for InterfaceId { + fn as_ref(&self) -> &Path { + self.0.as_ref().as_ref() + } +} diff --git a/appmgr/src/index.rs b/appmgr/src/index.rs deleted file mode 100644 index d8d0ea130..000000000 --- a/appmgr/src/index.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::cmp::Ord; -use std::ffi::OsStr; -use std::iter::FromIterator; -use std::path::Path; - -use emver::{Version, VersionRange}; -use futures::future::{BoxFuture, FutureExt}; -use linear_map::LinearMap; - -use crate::inspect::info_full; -use crate::manifest::{Description, ManifestLatest}; -use crate::{Error, ResultExt}; - -#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] -pub struct AppIndex(pub LinearMap); - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct IndexInfo { - pub title: String, - pub description: Description, - pub version_info: Vec, - pub icon_type: String, -} - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct VersionInfo { - pub version: Version, - pub release_notes: String, - pub os_version_required: VersionRange, - pub os_version_recommended: VersionRange, - pub install_alert: Option, -} - -const NULL_VERSION: Version = Version::new(0, 0, 0, 0); - -impl AppIndex { - fn add(&mut self, manifest: ManifestLatest) { - if let Some(ref mut entry) = self.0.get_mut(&manifest.id) { - if entry - .version_info - .get(0) - .map(|i| &i.version) - .unwrap_or(&NULL_VERSION) - <= &manifest.version - { - entry.title = manifest.title; - entry.description = manifest.description; - } - entry.version_info.push(VersionInfo { - version: manifest.version, - release_notes: manifest.release_notes, - os_version_required: manifest.os_version_required, - os_version_recommended: manifest.os_version_recommended, - install_alert: manifest.install_alert, - }); - entry - .version_info - .sort_unstable_by(|a, b| b.version.cmp(&a.version)); - entry.version_info.dedup_by(|a, b| a.version == b.version); - } else { - self.0.insert( - manifest.id, - IndexInfo { - title: manifest.title, - description: manifest.description, - version_info: vec![VersionInfo { - version: manifest.version, - release_notes: manifest.release_notes, - os_version_required: manifest.os_version_required, - os_version_recommended: manifest.os_version_recommended, - install_alert: manifest.install_alert, - }], - icon_type: "png".to_owned(), // TODO - }, - ); - } - } -} - -impl Extend for AppIndex { - fn extend>(&mut self, iter: I) { - for manifest in iter { - self.add(manifest); - } - } -} - -impl FromIterator for AppIndex { - fn from_iter>(iter: I) -> Self { - let mut res = Self::default(); - res.extend(iter); - res - } -} - -pub async fn index>(dir: P) -> Result { - let dir_path = dir.as_ref(); - let mut idx = AppIndex::default(); - fn index_rec<'a, P: AsRef + Send + Sync + 'a>( - idx: &'a mut AppIndex, - dir: P, - ) -> BoxFuture<'a, Result<(), Error>> { - async move { - let dir_path = dir.as_ref(); - if let Ok(_) = tokio::fs::metadata(dir_path.join(".ignore")).await { - log::info!("Skipping {}", dir_path.display()); - return Ok(()); - } - let mut entry_stream = tokio::fs::read_dir(dir_path).await?; - while let Some(entry) = entry_stream.next_entry().await? { - let path = entry.path(); - let metadata = entry.metadata().await?; - if metadata.is_file() { - let ext = path.extension(); - if ext == Some(OsStr::new("s9pk")) { - let info = info_full(&path, true, false) - .await - .with_ctx(|e| (e.code.clone(), format!("{}: {}", path.display(), e)))?; - idx.add(info.manifest.unwrap()); - } - } else if metadata.is_dir() { - index_rec(idx, &path).await?; - } - } - Ok(()) - } - .boxed() - } - index_rec(&mut idx, dir_path).await?; - Ok(idx) -} diff --git a/appmgr/src/inspect.rs b/appmgr/src/inspect.rs deleted file mode 100644 index 6ef9c5b63..000000000 --- a/appmgr/src/inspect.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::path::Path; - -use failure::ResultExt as _; -use futures::stream::StreamExt; -use tokio_tar as tar; - -use crate::config::{ConfigRuleEntry, ConfigSpec}; -use crate::manifest::{Manifest, ManifestLatest}; -use crate::util::from_cbor_async_reader; -use crate::version::VersionT; -use crate::Error; -use crate::ResultExt as _; - -#[derive(Debug, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct AppInfoFull { - #[serde(flatten)] - pub info: AppInfo, - #[serde(skip_serializing_if = "Option::is_none")] - pub manifest: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub config: Option, -} - -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct AppInfo { - pub title: String, - pub version: emver::Version, -} - -#[derive(Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct AppConfig { - pub spec: ConfigSpec, - pub rules: Vec, -} - -pub async fn info_full>( - path: P, - with_manifest: bool, - with_config: bool, -) -> Result { - let p = path.as_ref(); - log::info!("Opening file."); - let r = tokio::fs::File::open(p) - .await - .with_context(|e| format!("{}: {}", p.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - log::info!("Extracting archive."); - let mut pkg = tar::Archive::new(r); - let mut entries = pkg.entries()?; - log::info!("Opening manifest from archive."); - let manifest = entries - .next() - .await - .ok_or(crate::install::Error::CorruptedPkgFile("missing manifest")) - .no_code()??; - crate::ensure_code!( - manifest.path()?.to_str() == Some("manifest.cbor"), - crate::error::GENERAL_ERROR, - "Package File Invalid or Corrupted" - ); - log::trace!("Deserializing manifest."); - let manifest: Manifest = from_cbor_async_reader(manifest).await?; - let manifest = manifest.into_latest(); - crate::ensure_code!( - crate::version::Current::new() - .semver() - .satisfies(&manifest.os_version_required), - crate::error::VERSION_INCOMPATIBLE, - "AppMgr Version Not Compatible: needs {}", - manifest.os_version_required - ); - Ok(AppInfoFull { - info: AppInfo { - title: manifest.title.clone(), - version: manifest.version.clone(), - }, - manifest: if with_manifest { Some(manifest) } else { None }, - config: if with_config { - log::info!("Opening config spec from archive."); - let spec = entries - .next() - .await - .ok_or(crate::install::Error::CorruptedPkgFile( - "missing config spec", - )) - .no_code()??; - crate::ensure_code!( - spec.path()?.to_str() == Some("config_spec.cbor"), - crate::error::GENERAL_ERROR, - "Package File Invalid or Corrupted" - ); - log::trace!("Deserializing config spec."); - let spec = from_cbor_async_reader(spec).await?; - log::info!("Opening config rules from archive."); - let rules = entries - .next() - .await - .ok_or(crate::install::Error::CorruptedPkgFile( - "missing config rules", - )) - .no_code()??; - crate::ensure_code!( - rules.path()?.to_str() == Some("config_rules.cbor"), - crate::error::GENERAL_ERROR, - "Package File Invalid or Corrupted" - ); - log::trace!("Deserializing config rules."); - let rules = from_cbor_async_reader(rules).await?; - Some(AppConfig { spec, rules }) - } else { - None - }, - }) -} - -pub async fn print_instructions>(path: P) -> Result<(), Error> { - let p = path.as_ref(); - log::info!("Opening file."); - let r = tokio::fs::File::open(p) - .await - .with_context(|e| format!("{}: {}", p.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - log::info!("Extracting archive."); - let mut pkg = tar::Archive::new(r); - let mut entries = pkg.entries()?; - log::info!("Opening manifest from archive."); - let manifest = entries - .next() - .await - .ok_or(crate::install::Error::CorruptedPkgFile("missing manifest")) - .no_code()??; - crate::ensure_code!( - manifest.path()?.to_str() == Some("manifest.cbor"), - crate::error::GENERAL_ERROR, - "Package File Invalid or Corrupted" - ); - log::trace!("Deserializing manifest."); - let manifest: Manifest = from_cbor_async_reader(manifest).await?; - let manifest = manifest.into_latest(); - crate::ensure_code!( - crate::version::Current::new() - .semver() - .satisfies(&manifest.os_version_required), - crate::error::VERSION_INCOMPATIBLE, - "AppMgr Version Not Compatible: needs {}", - manifest.os_version_required - ); - entries - .next() - .await - .ok_or(crate::install::Error::CorruptedPkgFile( - "missing config spec", - )) - .no_code()??; - entries - .next() - .await - .ok_or(crate::install::Error::CorruptedPkgFile( - "missing config rules", - )) - .no_code()??; - - if manifest.has_instructions { - use tokio::io::AsyncWriteExt; - - let mut instructions = entries - .next() - .await - .ok_or(crate::install::Error::CorruptedPkgFile( - "missing instructions", - )) - .no_code()??; - - let mut stdout = tokio::io::stdout(); - tokio::io::copy(&mut instructions, &mut stdout) - .await - .with_code(crate::error::FILESYSTEM_ERROR)?; - stdout - .flush() - .await - .with_code(crate::error::FILESYSTEM_ERROR)?; - stdout - .shutdown() - .await - .with_code(crate::error::FILESYSTEM_ERROR)?; - } else { - return Err(failure::format_err!("No instructions for {}", p.display())) - .with_code(crate::error::NOT_FOUND); - } - - Ok(()) -} diff --git a/appmgr/src/install.rs b/appmgr/src/install.rs deleted file mode 100644 index 34306758b..000000000 --- a/appmgr/src/install.rs +++ /dev/null @@ -1,579 +0,0 @@ -use std::borrow::Cow; -use std::ffi::{OsStr, OsString}; -use std::marker::Unpin; -use std::path::{Path, PathBuf}; -use std::pin::Pin; -use std::sync::{ - atomic::{self, AtomicBool, AtomicU64}, - Arc, -}; -use std::task::Context; -use std::task::Poll; -use std::time::Duration; - -use failure::ResultExt as _; -use futures::stream::StreamExt; -use futures::stream::TryStreamExt; -use tokio::io::AsyncWriteExt; -use tokio::io::{AsyncRead, ReadBuf}; -use tokio_compat_02::FutureExt; -use tokio_tar as tar; - -use crate::config::{ConfigRuleEntry, ConfigSpec}; -use crate::manifest::{ImageConfig, Manifest, ManifestV0}; -use crate::util::{from_cbor_async_reader, to_yaml_async_writer, AsyncCompat, PersistencePath}; -use crate::version::VersionT; -use crate::ResultExt as _; - -#[derive(Fail, Debug, Clone)] -pub enum Error { - #[fail(display = "Package File Invalid or Corrupted: {}", _0)] - CorruptedPkgFile(&'static str), - #[fail(display = "Invalid File Name")] - InvalidFileName, -} - -pub async fn install_name(name_version: &str, use_cache: bool) -> Result<(), crate::Error> { - let name = name_version.split("@").next().unwrap(); - let tmp_path = Path::new(crate::TMP_DIR).join(format!("{}.s9pk", name)); - if !use_cache || !tmp_path.exists() { - download_name(name_version).await?; - } - install_path( - &tmp_path - .as_os_str() - .to_str() - .ok_or(Error::InvalidFileName) - .with_code(crate::error::FILESYSTEM_ERROR)?, - Some(name), - ) - .await?; - tokio::fs::remove_file(&tmp_path) - .await - .with_context(|e| format!("{}: {}", tmp_path.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - Ok(()) -} - -struct CountingReader(pub R, pub Arc); -impl AsyncRead for CountingReader -where - R: AsyncRead, -{ - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf, - ) -> Poll> { - let atomic = self.as_ref().1.clone(); // TODO: not efficient - match unsafe { self.map_unchecked_mut(|a| &mut a.0) }.poll_read(cx, buf) { - Poll::Ready(Ok(())) => { - atomic.fetch_add(buf.filled().len() as u64, atomic::Ordering::SeqCst); - Poll::Ready(Ok(())) - } - a => a, - } - } -} - -pub async fn download_name(name_version: &str) -> Result { - let mut split = name_version.split("@"); - let name = split.next().unwrap(); - let req: Option = split.next().map(|a| a.parse()).transpose().no_code()?; - if let Some(req) = req { - download( - &format!("{}/{}.s9pk?spec={}", &*crate::APP_REGISTRY_URL, name, req), - Some(name), - ) - .await - } else { - download( - &format!("{}/{}.s9pk", &*crate::APP_REGISTRY_URL, name), - Some(name), - ) - .await - } -} - -pub async fn download(url: &str, name: Option<&str>) -> Result { - let url = reqwest::Url::parse(url).no_code()?; - log::info!("Downloading {}.", url.as_str()); - let response = reqwest::get(url) - .compat() - .await - .with_code(crate::error::NETWORK_ERROR)? - .error_for_status() - .with_code(crate::error::REGISTRY_ERROR)?; - tokio::fs::create_dir_all(crate::TMP_DIR).await?; - let tmp_file_path = - Path::new(crate::TMP_DIR).join(&format!("{}.s9pk", name.unwrap_or("download"))); - let mut f = tokio::fs::File::create(&tmp_file_path).await?; - let len: Option = response.content_length().map(|a| { - log::info!("{}KiB to download.", a / 1024); - a - }); - let done = Arc::new(AtomicBool::new(false)); - let counter = Arc::new(AtomicU64::new(0)); - let mut reader = CountingReader( - AsyncCompat( - response - .bytes_stream() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) - .into_async_read(), - ), - counter.clone(), - ); - let done_handle = done.clone(); - let download_handle = tokio::spawn(async move { - let res = tokio::io::copy(&mut reader, &mut f).await; - done_handle.store(true, atomic::Ordering::SeqCst); - res - }); - let poll_handle = tokio::spawn(async move { - loop { - let is_done = done.load(atomic::Ordering::SeqCst); - let downloaded_bytes = counter.load(atomic::Ordering::SeqCst); - if !*crate::QUIET.read().await { - if let Some(len) = len { - print!("\rDownloading... {}%", downloaded_bytes * 100 / len); - } else { - print!("\rDownloading... {}KiB", downloaded_bytes / 1024); - } - } - if is_done { - break; - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - if !*crate::QUIET.read().await { - println!("\rDownloading... 100%"); - } - }); - download_handle.await.unwrap()?; - poll_handle.await.unwrap(); - Ok(tmp_file_path) -} - -pub async fn install_url(url: &str, name: Option<&str>) -> Result<(), crate::Error> { - let tmp_file_path = download(url, name).await?; - install_path(&tmp_file_path, name).await?; - tokio::fs::remove_file(&tmp_file_path) - .await - .with_context(|e| format!("{}: {}", tmp_file_path.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - Ok(()) -} - -pub async fn install_path>(p: P, name: Option<&str>) -> Result<(), crate::Error> { - let path = p.as_ref(); - log::info!( - "Starting install of {}.", - path.file_name() - .and_then(|a| a.to_str()) - .ok_or(Error::InvalidFileName) - .no_code()? - ); - let file = tokio::fs::File::open(&path) - .await - .with_context(|e| format!("{}: {}", path.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - let len = file.metadata().await?.len(); - let done = Arc::new(AtomicBool::new(false)); - let counter = Arc::new(AtomicU64::new(0)); - let done_handle = done.clone(); - let name_clone = name.map(|a| a.to_owned()); - let counter_clone = counter.clone(); - let poll_handle = tokio::spawn(async move { - loop { - let is_done = done.load(atomic::Ordering::SeqCst); - let installed_bytes = counter.load(atomic::Ordering::SeqCst); - if !*crate::QUIET.read().await { - print!("\rInstalling... {}%", installed_bytes * 100 / len); - } - if is_done { - break; - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - if !*crate::QUIET.read().await { - println!("\rInstalling... 100%"); - } - }); - let reader = CountingReader(file, counter_clone); - let res = install(reader, name_clone.as_ref().map(|a| a.as_str())).await; - done_handle.store(true, atomic::Ordering::SeqCst); - res?; - poll_handle.await.unwrap(); - if !*crate::QUIET.read().await { - println!("Complete."); - } - Ok(()) -} - -pub async fn install( - r: R, - name: Option<&str>, -) -> Result<(), crate::Error> { - log::info!("Extracting archive."); - let mut pkg = tar::Archive::new(r); - let mut entries = pkg.entries()?; - log::info!("Opening manifest from archive."); - let manifest = entries - .next() - .await - .ok_or(Error::CorruptedPkgFile("missing manifest")) - .no_code()??; - crate::ensure_code!( - manifest.path()?.to_str() == Some("manifest.cbor"), - crate::error::GENERAL_ERROR, - "Package File Invalid or Corrupted" - ); - log::trace!("Deserializing manifest."); - let manifest: Manifest = from_cbor_async_reader(manifest).await.no_code()?; - match manifest { - Manifest::V0(m) => install_v0(m, entries, name).await?, - }; - Ok(()) -} - -pub async fn install_v0( - manifest: ManifestV0, - mut entries: tar::Entries, - name: Option<&str>, -) -> Result<(), crate::Error> { - crate::ensure_code!( - crate::version::Current::new() - .semver() - .satisfies(&manifest.os_version_required), - crate::error::VERSION_INCOMPATIBLE, - "OS Version Not Compatible: need {}", - manifest.os_version_required - ); - if let Some(name) = name { - crate::ensure_code!( - manifest.id == name, - crate::error::GENERAL_ERROR, - "Package Name Does Not Match Expected" - ); - } - - log::info!( - "Creating metadata directory: {}/apps/{}", - crate::PERSISTENCE_DIR, - manifest.id - ); - let app_dir = PersistencePath::from_ref("apps").join(&manifest.id); - let app_dir_path = app_dir.path(); - if app_dir_path.exists() { - tokio::fs::remove_dir_all(&app_dir_path).await?; - } - tokio::fs::create_dir_all(&app_dir_path).await?; - - let (ip, tor_addr, tor_key) = crate::tor::set_svc( - &manifest.id, - crate::tor::NewService { - ports: manifest.ports.clone(), - hidden_service_version: manifest.hidden_service_version, - }, - ) - .await?; - - let recoverable = Path::new(crate::VOLUMES).join(&manifest.id).exists(); - - log::info!("Creating volume {}/{}.", crate::VOLUMES, manifest.id); - tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id)).await?; - - let _lock = app_dir.lock(true).await?; - log::info!("Saving manifest."); - let mut manifest_out = app_dir.join("manifest.yaml").write(None).await?; - to_yaml_async_writer(&mut *manifest_out, &Manifest::V0(manifest.clone())).await?; - manifest_out.commit().await?; - log::info!("Opening config spec from archive."); - let config_spec = entries - .next() - .await - .ok_or(Error::CorruptedPkgFile("missing config spec")) - .no_code()??; - crate::ensure_code!( - config_spec.path()?.to_str() == Some("config_spec.cbor"), - crate::error::GENERAL_ERROR, - "Package File Invalid or Corrupted" - ); - log::trace!("Deserializing config spec."); - let config_spec: ConfigSpec = from_cbor_async_reader(config_spec).await?; - log::info!("Saving config spec."); - let mut config_spec_out = app_dir.join("config_spec.yaml").write(None).await?; - to_yaml_async_writer(&mut *config_spec_out, &config_spec).await?; - config_spec_out.commit().await?; - log::info!("Opening config rules from archive."); - let config_rules = entries - .next() - .await - .ok_or(Error::CorruptedPkgFile("missing config rules")) - .no_code()??; - crate::ensure_code!( - config_rules.path()?.to_str() == Some("config_rules.cbor"), - crate::error::GENERAL_ERROR, - "Package File Invalid or Corrupted" - ); - log::trace!("Deserializing config rules."); - let config_rules: Vec = from_cbor_async_reader(config_rules).await?; - log::info!("Saving config rules."); - let mut config_rules_out = app_dir.join("config_rules.yaml").write(None).await?; - to_yaml_async_writer(&mut *config_rules_out, &config_rules).await?; - config_rules_out.commit().await?; - if manifest.has_instructions { - log::info!("Opening instructions from archive."); - let mut instructions = entries - .next() - .await - .ok_or(Error::CorruptedPkgFile("missing config rules")) - .no_code()??; - crate::ensure_code!( - instructions.path()?.to_str() == Some("instructions.md"), - crate::error::GENERAL_ERROR, - "Package File Invalid or Corrupted" - ); - log::info!("Saving instructions."); - let mut instructions_out = app_dir.join("instructions.md").write(None).await?; - tokio::io::copy(&mut instructions, &mut *instructions_out) - .await - .with_code(crate::error::FILESYSTEM_ERROR)?; - instructions_out.commit().await?; - } - - log::info!("Copying over assets."); - for asset in manifest.assets.iter() { - let dst_path = Path::new(crate::VOLUMES) - .join(&manifest.id) - .join(&asset.dst); - log::info!("Copying {} to {}", asset.src.display(), dst_path.display()); - let src_path = Path::new(&asset.src); - log::info!("Opening {} from archive.", src_path.display()); - let mut src = entries - .next() - .await - .ok_or(Error::CorruptedPkgFile("missing asset")) - .no_code()??; - crate::ensure_code!( - src.path()? == src_path, - crate::error::GENERAL_ERROR, - "Package File Invalid or Corrupted" - ); - let dst_path_file = dst_path.join(src_path); - if dst_path_file.exists() && !asset.overwrite { - log::info!("{} already exists, skipping.", dst_path_file.display()); - } else { - if dst_path_file.exists() { - if dst_path_file.is_dir() { - tokio::fs::remove_dir_all(&dst_path_file) - .await - .with_context(|e| format!("{}: {}", dst_path_file.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - } else { - tokio::fs::remove_file(&dst_path_file) - .await - .with_context(|e| format!("{}: {}", dst_path_file.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - } - } - src.unpack_in(&dst_path).await?; - if src.header().entry_type().is_dir() { - loop { - let mut file = entries - .next() - .await - .ok_or(Error::CorruptedPkgFile("missing asset")) - .no_code()??; - if file - .path()? - .starts_with(format!("APPMGR_DIR_END:{}", asset.src.display())) - { - break; - } else { - file.unpack_in(&dst_path).await?; - } - } - } - } - } - - let tag = match &manifest.image { - ImageConfig::Tar => { - let image_name = format!("start9/{}", manifest.id); - let tag = format!("{}:latest", image_name); - if tokio::process::Command::new("docker") - .arg("images") - .arg("-q") - .arg(&image_name) - .output() - .await? - .stdout - .len() - > 0 - { - tokio::process::Command::new("docker") - .arg("stop") - .arg(&manifest.id) - .spawn()? - .wait() - .await?; - tokio::process::Command::new("docker") - .arg("rm") - .arg(&manifest.id) - .spawn()? - .wait() - .await?; - crate::ensure_code!( - tokio::process::Command::new("docker") - .arg("rmi") - .arg(&image_name) - .output() - .await? - .status - .success(), - crate::error::DOCKER_ERROR, - "Failed to Remove Existing Image" - ) - } - log::info!("Opening image.tar from archive."); - let mut image = entries - .next() - .await - .ok_or(Error::CorruptedPkgFile("missing image.tar")) - .no_code()??; - let image_path = image.path()?; - if image_path != Path::new("image.tar") { - return Err(crate::Error::from(format_err!( - "Package File Invalid or Corrupted: expected image.tar, got {}", - image_path.display() - ))); - } - log::info!( - "Loading docker image start9/{} from image.tar.", - manifest.id - ); - let mut child = tokio::process::Command::new("docker") - .arg("load") - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::inherit()) - .stderr(match log::max_level() { - log::LevelFilter::Error => std::process::Stdio::null(), - _ => std::process::Stdio::inherit(), - }) - .spawn()?; - let mut child_in = child.stdin.take().unwrap(); - tokio::io::copy(&mut image, &mut child_in).await?; - child_in.flush().await?; - child_in.shutdown().await?; - drop(child_in); - crate::ensure_code!( - child.wait().await?.success(), - crate::error::DOCKER_ERROR, - "Failed to Load Docker Image From Tar" - ); - tag - } - }; - log::info!("Creating docker container: {} from {}.", manifest.id, tag); - let volume_arg = format!( - "type=bind,src={}/{},dst={}", - crate::VOLUMES, - manifest.id, - manifest.mount.display() - ); - let mut args = vec![ - Cow::Borrowed(OsStr::new("create")), - Cow::Borrowed(OsStr::new("--restart")), - Cow::Borrowed(OsStr::new("no")), - Cow::Borrowed(OsStr::new("--name")), - Cow::Borrowed(OsStr::new(&manifest.id)), - Cow::Borrowed(OsStr::new("--mount")), - Cow::Borrowed(OsStr::new(&volume_arg)), - Cow::Borrowed(OsStr::new("--net")), - Cow::Borrowed(OsStr::new("start9")), - Cow::Borrowed(OsStr::new("--ip")), - Cow::Owned(OsString::from(format!("{}", ip))), - ]; - if let (Some(ref tor_addr), Some(ref tor_key)) = (&tor_addr, &tor_key) { - args.extend( - std::iter::empty() - .chain(std::iter::once(Cow::Borrowed(OsStr::new("--env")))) - .chain(std::iter::once(Cow::Owned(OsString::from(format!( - "TOR_ADDRESS={}", - tor_addr - ))))) - .chain(std::iter::once(Cow::Borrowed(OsStr::new("--env")))) - .chain(std::iter::once(Cow::Owned(OsString::from(format!( - "TOR_KEY={}", - tor_key - ))))), - ); - } - if let Some(shm_size_mb) = manifest.shm_size_mb { - args.push(Cow::Borrowed(OsStr::new("--shm-size"))); - args.push(Cow::Owned(OsString::from(format!("{}m", shm_size_mb)))); - } - args.push(Cow::Borrowed(OsStr::new(&tag))); - crate::ensure_code!( - std::process::Command::new("docker") - .args(&args) - .stdout(std::process::Stdio::null()) - .stderr(match log::max_level() { - log::LevelFilter::Error => std::process::Stdio::null(), - _ => std::process::Stdio::inherit(), - }) - .status()? - .success(), - crate::error::DOCKER_ERROR, - "Failed to Create Docker Container" - ); - tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id).join("start9")).await?; - if let Some(public) = manifest.public { - tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id).join(public)) - .await?; - } - if let Some(shared) = manifest.shared { - tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id).join(shared)) - .await?; - } - log::info!("Updating app list."); - crate::apps::add( - &manifest.id, - crate::apps::AppInfo { - title: manifest.title.clone(), - version: manifest.version.clone(), - tor_address: tor_addr.clone(), - configured: false, - recoverable, - needs_restart: false, - }, - ) - .await?; - let config = crate::apps::config(&manifest.id).await?; - if let Some(cfg) = config.config { - if config.spec.matches(&cfg).is_ok() { - crate::apps::set_configured(&manifest.id, true).await?; - } - } else { - let empty_config = crate::config::Config::default(); - if config.spec.matches(&empty_config).is_ok() { - crate::config::configure(&manifest.id, Some(empty_config), None, false).await?; - } - } - crate::dependencies::update_binds(&manifest.id).await?; - for (dep_id, dep_info) in manifest.dependencies.0 { - if dep_info.mount_shared - && crate::apps::list_info().await?.get(&dep_id).is_some() - && crate::apps::manifest(&dep_id).await?.shared.is_some() - { - match crate::apps::status(&dep_id, false).await?.status { - crate::apps::DockerStatus::Stopped => (), - crate::apps::DockerStatus::Running => crate::control::restart_app(&dep_id).await?, - _ => crate::apps::set_needs_restart(&dep_id, true).await?, - } - } - } - - Ok(()) -} diff --git a/appmgr/src/install/mod.rs b/appmgr/src/install/mod.rs new file mode 100644 index 000000000..c8bcffd33 --- /dev/null +++ b/appmgr/src/install/mod.rs @@ -0,0 +1,465 @@ +use std::fmt::Display; +use std::io::SeekFrom; +use std::path::Path; +use std::pin::Pin; +use std::process::Stdio; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::Duration; + +use anyhow::anyhow; +use futures::TryStreamExt; +use http::HeaderMap; +use indexmap::{IndexMap, IndexSet}; +use patch_db::json_ptr::JsonPointer; +use patch_db::{ + DbHandle, HasModel, MapModel, Model, ModelData, OptionModel, PatchDbHandle, Revision, +}; +use reqwest::Response; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use tokio::fs::{File, OpenOptions}; +use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt}; + +use self::progress::{InstallProgress, InstallProgressTracker}; +use crate::context::RpcContext; +use crate::db::model::{ + CurrentDependencyInfo, InstalledPackageDataEntry, PackageDataEntry, StaticFiles, +}; +use crate::s9pk::manifest::{Manifest, PackageId}; +use crate::s9pk::reader::S9pkReader; +use crate::status::{DependencyErrors, MainStatus, Status}; +use crate::util::{AsyncFileExt, Version}; +use crate::Error; + +pub mod progress; + +pub const PKG_CACHE: &'static str = "/mnt/embassy-os/cache/packages"; +pub const PKG_PUBLIC_DIR: &'static str = "/mnt/embassy-os/public/package-data"; + +pub async fn download_install_s9pk( + ctx: RpcContext, + temp_manifest: &Manifest, + s9pk: Response, +) -> Result<(), Error> { + let pkg_id = &temp_manifest.id; + let version = &temp_manifest.version; + + let mut db = ctx.db.handle(); + + let pkg_cache_dir = Path::new(PKG_CACHE).join(pkg_id).join(version.as_str()); + tokio::fs::create_dir_all(&pkg_cache_dir).await?; + let pkg_cache = AsRef::::as_ref(pkg_id).with_extension("s9pk"); + + let pkg_data_entry = crate::db::DatabaseModel::new() + .package_data() + .idx_model(pkg_id); + + let res = (|| async { + let progress = InstallProgress::new(s9pk.content_length()); + let static_files = StaticFiles::remote(pkg_id, version, temp_manifest.assets.icon_type())?; + let mut pde = pkg_data_entry.get_mut(&mut db).await?; + match pde.take() { + Some(PackageDataEntry::Installed { installed, .. }) => { + *pde = Some(PackageDataEntry::Updating { + install_progress: progress.clone(), + static_files, + installed, + temp_manifest: temp_manifest.clone(), + }) + } + None => { + *pde = Some(PackageDataEntry::Installing { + install_progress: progress.clone(), + static_files, + temp_manifest: temp_manifest.clone(), + }) + } + _ => { + return Err(Error::new( + anyhow!("Cannot install over an app in a transient state"), + crate::ErrorKind::InvalidRequest, + )) + } + } + pde.save(&mut db).await?; + let progress_model = pkg_data_entry.and_then(|pde| pde.install_progress()); + + async fn check_cache( + pkg_id: &PackageId, + version: &Version, + pkg_cache: &Path, + headers: &HeaderMap, + progress: &Arc, + model: OptionModel, + ctx: &RpcContext, + db: &mut PatchDbHandle, + ) -> Option>> { + fn warn_ok( + pkg_id: &PackageId, + version: &Version, + res: Result, + ) -> Option { + match res { + Ok(a) => Some(a), + Err(e) => { + log::warn!( + "Install {}@{}: Could not open cache: {}", + pkg_id, + version, + e + ); + None + } + } + } + let hash = headers.get("x-s9pk-hash")?; + let file = warn_ok(pkg_id, version, File::maybe_open(&pkg_cache).await)??; + let progress_reader = InstallProgressTracker::new(file, progress.clone()); + let rdr = warn_ok( + pkg_id, + version, + progress + .track_read_during(model, &ctx.db, db, || { + S9pkReader::from_reader(progress_reader) + }) + .await, + )?; + if hash.as_bytes() == rdr.hash_str().as_bytes() { + Some(rdr) + } else { + None + } + } + let cached = check_cache( + pkg_id, + version, + &pkg_cache, + s9pk.headers(), + &progress, + progress_model.clone(), + &ctx, + &mut db, + ) + .await; + + let mut s9pk_reader = if let Some(cached) = cached { + cached + } else { + File::delete(&pkg_cache).await?; + let mut dst = OpenOptions::new() + .create(true) + .write(true) + .read(true) + .open(&pkg_cache) + .await?; + + progress + .track_download_during(progress_model.clone(), &ctx.db, &mut db, || async { + let mut progress_writer = + InstallProgressTracker::new(&mut dst, progress.clone()); + tokio::io::copy( + &mut tokio_util::io::StreamReader::new(s9pk.bytes_stream().map_err(|e| { + std::io::Error::new( + if e.is_connect() { + std::io::ErrorKind::ConnectionRefused + } else if e.is_timeout() { + std::io::ErrorKind::TimedOut + } else { + std::io::ErrorKind::Other + }, + e, + ) + })), + &mut progress_writer, + ) + .await?; + progress.download_complete(); + Ok(()) + }) + .await?; + + dst.seek(SeekFrom::Start(0)).await?; + + let progress_reader = InstallProgressTracker::new(dst, progress.clone()); + let rdr = progress + .track_read_during(progress_model.clone(), &ctx.db, &mut db, || { + S9pkReader::from_reader(progress_reader) + }) + .await?; + rdr + }; + install_s9pk(&ctx, &mut db, pkg_id, version, &mut s9pk_reader, progress).await?; + + Ok(()) + })() + .await; + + if let Err(e) = res { + let mut broken = crate::db::DatabaseModel::new() + .broken_packages() + .get_mut(&mut db) + .await?; + broken.push(pkg_id.clone()); + broken.save(&mut db).await?; + Err(e) + } else { + Ok(()) + } +} + +pub async fn install_s9pk( + ctx: &RpcContext, + mut db: &mut PatchDbHandle, + pkg_id: &PackageId, + version: &Version, + rdr: &mut S9pkReader>, + progress: Arc, +) -> Result<(), Error> { + rdr.validate().await?; + rdr.validated(); + let model = crate::db::DatabaseModel::new() + .package_data() + .idx_model(pkg_id) + .check(db) + .await? + .ok_or_else(|| { + Error::new( + anyhow!("PackageDataEntry does not exist"), + crate::ErrorKind::Database, + ) + })?; + let progress_model = model.clone().install_progress(); + + log::info!("Install {}@{}: Unpacking Manifest", pkg_id, version); + let manifest = progress + .track_read_during(progress_model.clone(), &ctx.db, db, || rdr.manifest()) + .await?; + log::info!("Install {}@{}: Unpacked Manifest", pkg_id, version); + + let public_dir_path = Path::new(PKG_PUBLIC_DIR) + .join(pkg_id) + .join(version.as_str()); + tokio::fs::create_dir_all(&public_dir_path).await?; + + log::info!("Install {}@{}: Unpacking LICENSE.md", pkg_id, version); + progress + .track_read_during(progress_model.clone(), &ctx.db, db, || async { + let license_path = public_dir_path.join("LICENSE.md"); + let mut dst = File::create(&license_path).await?; + tokio::io::copy(&mut rdr.license().await?, &mut dst).await?; + dst.sync_all().await?; + Ok(()) + }) + .await?; + log::info!("Install {}@{}: Unpacked LICENSE.md", pkg_id, version); + + log::info!("Install {}@{}: Unpacking INSTRUCTIONS.md", pkg_id, version); + progress + .track_read_during(progress_model.clone(), &ctx.db, db, || async { + let instructions_path = public_dir_path.join("INSTRUCTIONS.md"); + let mut dst = File::create(&instructions_path).await?; + tokio::io::copy(&mut rdr.instructions().await?, &mut dst).await?; + dst.sync_all().await?; + Ok(()) + }) + .await?; + log::info!("Install {}@{}: Unpacked INSTRUCTIONS.md", pkg_id, version); + + let icon_path = Path::new("icon").with_extension(&manifest.assets.icon_type()); + log::info!( + "Install {}@{}: Unpacking {}", + pkg_id, + version, + icon_path.display() + ); + progress + .track_read_during(progress_model.clone(), &ctx.db, db, || async { + let icon_path = public_dir_path.join(&icon_path); + let mut dst = File::create(&icon_path).await?; + tokio::io::copy(&mut rdr.icon().await?, &mut dst).await?; + dst.sync_all().await?; + Ok(()) + }) + .await?; + log::info!( + "Install {}@{}: Unpacked {}", + pkg_id, + version, + icon_path.display() + ); + + log::info!("Install {}@{}: Unpacking Docker Images", pkg_id, version); + progress + .track_read_during(progress_model.clone(), &ctx.db, db, || async { + let mut load = tokio::process::Command::new("docker") + .arg("load") + .stdin(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + let mut dst = load.stdin.take().ok_or_else(|| { + Error::new( + anyhow!("Could not write to stdin of docker load"), + crate::ErrorKind::Docker, + ) + })?; + tokio::io::copy(&mut rdr.docker_images().await?, &mut dst).await?; + dst.flush().await?; + dst.shutdown().await?; + drop(dst); + let res = load.wait_with_output().await?; + if !res.status.success() { + Err(Error::new( + anyhow!( + "{}", + String::from_utf8(res.stderr) + .unwrap_or_else(|e| format!("Could not parse stderr: {}", e)) + ), + crate::ErrorKind::Docker, + )) + } else { + Ok(()) + } + }) + .await?; + log::info!("Install {}@{}: Unpacked Docker Images", pkg_id, version,); + + progress.read_complete.store(true, Ordering::SeqCst); + + progress_model.put(&mut db, &progress).await?; + + let mut tx = db.begin().await?; + + let mut network = crate::db::DatabaseModel::new() + .network() + .get_mut(&mut tx) + .await?; + + log::info!("Install {}@{}: Installing main", pkg_id, version); + let ip = network.register_host(&manifest.id)?; + manifest + .main + .install(pkg_id, version, &manifest.volumes, ip) + .await?; + let hosts = network.hosts.clone(); + network.save(&mut tx).await?; + log::info!("Install {}@{}: Installed main", pkg_id, version); + + log::info!("Install {}@{}: Installing interfaces", pkg_id, version); + let interface_info = manifest.interfaces.install(ip).await?; + log::info!("Install {}@{}: Installed interfaces", pkg_id, version); + + log::info!("Install {}@{}: Complete", pkg_id, version); + + let static_files = StaticFiles::local(pkg_id, version, manifest.assets.icon_type())?; + let installed = InstalledPackageDataEntry { + manifest: manifest.clone(), + status: Status { + configured: manifest.config.is_none(), + main: MainStatus::Stopped, + dependency_errors: todo!(), + }, + system_pointers: Vec::new(), + current_dependents: { + // search required dependencies + let mut deps = IndexMap::new(); + for package in crate::db::DatabaseModel::new() + .package_data() + .keys(&mut tx) + .await? + { + if let Some(dep) = crate::db::DatabaseModel::new() + .package_data() + .idx_model(&package) + .expect(&mut tx) + .await? + .installed() + .and_then(|i| i.current_dependencies().idx_model(pkg_id)) + .get(&mut tx) + .await? + .to_owned() + { + deps.insert(package, dep); + } + } + deps + }, + current_dependencies: manifest + .dependencies + .0 + .iter() + .filter_map(|(id, info)| { + if info.optional.is_none() { + Some((id.clone(), CurrentDependencyInfo::default())) + } else { + None + } + }) + .collect(), + interface_info, + }; + let mut pde = model.get_mut(&mut tx).await?; + let prev = std::mem::replace( + &mut *pde, + PackageDataEntry::Installed { + installed, + static_files, + }, + ); + pde.save(&mut tx).await?; + if let PackageDataEntry::Updating { + installed: prev, .. + } = prev + { + let mut configured = prev.status.configured; + if let Some(res) = prev + .manifest + .migrations + .to( + version, + pkg_id, + &prev.manifest.version, + &prev.manifest.volumes, + &hosts, + ) + .await? + { + configured &= res.configured; + } + // cleanup(pkg_id, Some(prev)).await?; + if let Some(res) = manifest + .migrations + .from( + &prev.manifest.version, + pkg_id, + version, + &manifest.volumes, + &hosts, + ) + .await? + { + configured &= res.configured; + } + if configured { + crate::config::configure( + &mut tx, + &ctx.docker, + &hosts, + pkg_id, + None, + &None, + false, + &mut IndexMap::new(), + &mut IndexMap::new(), + ) + .await?; + todo!("set as running if viable"); + } + } + + tx.commit(None).await?; + + Ok(()) +} diff --git a/appmgr/src/install/progress.rs b/appmgr/src/install/progress.rs new file mode 100644 index 000000000..d0b5b6bd2 --- /dev/null +++ b/appmgr/src/install/progress.rs @@ -0,0 +1,227 @@ +use std::future::Future; +use std::io::SeekFrom; +use std::pin::Pin; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::Duration; + +use patch_db::{DbHandle, HasModel, OptionModel, PatchDb, PatchDbHandle}; +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; + +use crate::db::model::PackageDataEntry; +use crate::Error; + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +pub struct InstallProgress { + pub size: Option, + pub downloaded: AtomicU64, + pub download_complete: AtomicBool, + pub validated: AtomicU64, + pub validation_complete: AtomicBool, + pub read: AtomicU64, + pub read_complete: AtomicBool, +} +impl InstallProgress { + pub fn new(size: Option) -> Arc { + Arc::new(InstallProgress { + size, + downloaded: AtomicU64::new(0), + download_complete: AtomicBool::new(false), + validated: AtomicU64::new(0), + validation_complete: AtomicBool::new(false), + read: AtomicU64::new(0), + read_complete: AtomicBool::new(false), + }) + } + pub fn download_complete(&self) { + self.download_complete.store(true, Ordering::SeqCst) + } + pub async fn track_download( + self: Arc, + model: OptionModel, + mut db: Db, + ) -> (Db, Result<(), Error>) { + while !self.download_complete.load(Ordering::SeqCst) { + if let Err(e) = model.put(&mut db, &self).await { + return (db, Err(e.into())); + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + if let Err(e) = model.put(&mut db, &self).await { + (db, Err(e.into())) + } else { + (db, Ok(())) + } + } + pub async fn track_download_during< + F: FnOnce() -> Fut, + Fut: Future>, + T, + >( + self: &Arc, + model: OptionModel, + db: &PatchDb, + handle: &mut PatchDbHandle, + f: F, + ) -> Result { + let local_db = std::mem::replace(handle, db.handle()); + let tracker = tokio::spawn(self.clone().track_download(model.clone(), local_db)); + let res = f().await; + self.download_complete.store(true, Ordering::SeqCst); + let (local_db, tracker_res) = tracker.await.unwrap(); + let _ = std::mem::replace(handle, local_db); + tracker_res?; + res + } + pub async fn track_read( + self: Arc, + model: OptionModel, + mut db: Db, + complete: Arc, + ) -> (Db, Result<(), Error>) { + while !complete.load(Ordering::SeqCst) { + if let Err(e) = model.put(&mut db, &self).await { + return (db, Err(e.into())); + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + (db, Ok(())) + } + pub async fn track_read_during< + F: FnOnce() -> Fut, + Fut: Future>, + T, + >( + self: &Arc, + model: OptionModel, + db: &PatchDb, + handle: &mut PatchDbHandle, + f: F, + ) -> Result { + let local_db = std::mem::replace(handle, db.handle()); + let complete = Arc::new(AtomicBool::new(false)); + let tracker = tokio::spawn(self.clone().track_read( + model.clone(), + local_db, + complete.clone(), + )); + let res = f().await; + complete.store(true, Ordering::SeqCst); + let (local_db, tracker_res) = tracker.await.unwrap(); + let _ = std::mem::replace(handle, local_db); + tracker_res?; + res + } +} + +#[pin_project::pin_project] +pub struct InstallProgressTracker { + #[pin] + inner: RW, + validating: bool, + progress: Arc, +} +impl InstallProgressTracker { + pub fn new(inner: RW, progress: Arc) -> Self { + InstallProgressTracker { + inner, + validating: true, + progress, + } + } + pub fn validated(&mut self) { + self.validating = false; + } +} +impl AsyncWrite for InstallProgressTracker { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.project(); + match this.inner.poll_write(cx, buf) { + Poll::Ready(Ok(n)) => { + this.progress + .downloaded + .fetch_add(n as u64, Ordering::SeqCst); + Poll::Ready(Ok(n)) + } + a => a, + } + } + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + this.inner.poll_flush(cx) + } + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + let this = self.project(); + this.inner.poll_shutdown(cx) + } + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + let this = self.project(); + match this.inner.poll_write_vectored(cx, bufs) { + Poll::Ready(Ok(n)) => { + this.progress + .downloaded + .fetch_add(n as u64, Ordering::SeqCst); + Poll::Ready(Ok(n)) + } + a => a, + } + } +} +impl AsyncRead for InstallProgressTracker { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + let this = self.project(); + let prev = buf.filled().len() as u64; + match this.inner.poll_read(cx, buf) { + Poll::Ready(Ok(())) => { + if *this.validating { + &this.progress.validated + } else { + &this.progress.read + } + .fetch_add(buf.filled().len() as u64 - prev, Ordering::SeqCst); + + Poll::Ready(Ok(())) + } + a => a, + } + } +} +impl AsyncSeek for InstallProgressTracker { + fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> std::io::Result<()> { + let this = self.project(); + this.inner.start_seek(position) + } + fn poll_complete(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + match this.inner.poll_complete(cx) { + Poll::Ready(Ok(n)) => { + if *this.validating { + &this.progress.validated + } else { + &this.progress.read + } + .store(n, Ordering::SeqCst); + Poll::Ready(Ok(n)) + } + a => a, + } + } +} diff --git a/appmgr/src/lan.rs b/appmgr/src/lan.rs deleted file mode 100644 index 439027081..000000000 --- a/appmgr/src/lan.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::Error; -use avahi_sys; -use futures::future::pending; - -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct AppId { - pub un_app_id: String, -} - -pub async fn enable_lan() -> Result<(), Error> { - unsafe { - let app_list = crate::apps::list_info().await?; - - let simple_poll = avahi_sys::avahi_simple_poll_new(); - let poll = avahi_sys::avahi_simple_poll_get(simple_poll); - let mut stack_err = 0; - let err_c: *mut i32 = &mut stack_err; - let avahi_client = avahi_sys::avahi_client_new( - poll, - avahi_sys::AvahiClientFlags::AVAHI_CLIENT_NO_FAIL, - None, - std::ptr::null_mut(), - err_c, - ); - let group = - avahi_sys::avahi_entry_group_new(avahi_client, Some(noop), std::ptr::null_mut()); - let hostname_raw = avahi_sys::avahi_client_get_host_name_fqdn(avahi_client); - let hostname_bytes = std::ffi::CStr::from_ptr(hostname_raw).to_bytes_with_nul(); - const HOSTNAME_LEN: usize = 1 + 15 + 1 + 5; // leading byte, main address, dot, "local" - debug_assert_eq!(hostname_bytes.len(), HOSTNAME_LEN); - let mut hostname_buf = [0; HOSTNAME_LEN + 1]; - hostname_buf[1..].copy_from_slice(hostname_bytes); - // assume fixed length prefix on hostname due to local address - hostname_buf[0] = 15; // set the prefix length to 15 for the main address - hostname_buf[16] = 5; // set the prefix length to 5 for "local" - - for (app_id, app_info) in app_list { - let man = crate::apps::manifest(&app_id).await?; - if man - .ports - .iter() - .filter(|p| p.lan.is_some()) - .next() - .is_none() - { - continue; - } - let tor_address = if let Some(addr) = app_info.tor_address { - addr - } else { - continue; - }; - let lan_address = tor_address - .strip_suffix(".onion") - .ok_or_else(|| failure::format_err!("Invalid Tor Address: {:?}", tor_address))? - .to_owned() - + ".local"; - let lan_address_ptr = std::ffi::CString::new(lan_address) - .expect("Could not cast lan address to c string"); - let _ = avahi_sys::avahi_entry_group_add_record( - group, - avahi_sys::AVAHI_IF_UNSPEC, - avahi_sys::AVAHI_PROTO_UNSPEC, - avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_USE_MULTICAST - | avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_ALLOW_MULTIPLE, - lan_address_ptr.as_ptr(), - avahi_sys::AVAHI_DNS_CLASS_IN as u16, - avahi_sys::AVAHI_DNS_TYPE_CNAME as u16, - avahi_sys::AVAHI_DEFAULT_TTL, - hostname_buf.as_ptr().cast(), - hostname_buf.len(), - ); - log::info!("Published {:?}", lan_address_ptr); - } - avahi_sys::avahi_entry_group_commit(group); - ctrlc::set_handler(move || { - // please the borrow checker with the below semantics - // avahi_sys::avahi_entry_group_free(group); - // avahi_sys::avahi_client_free(avahi_client); - // drop(Box::from_raw(err_c)); - std::process::exit(0); - }) - .expect("Error setting signal handler"); - } - pending().await -} - -unsafe extern "C" fn noop( - _group: *mut avahi_sys::AvahiEntryGroup, - _state: avahi_sys::AvahiEntryGroupState, - _userdata: *mut core::ffi::c_void, -) { -} diff --git a/appmgr/src/lib.rs b/appmgr/src/lib.rs index aa0400dfe..62d264189 100644 --- a/appmgr/src/lib.rs +++ b/appmgr/src/lib.rs @@ -1,8 +1,4 @@ -#[macro_use] -extern crate failure; -#[macro_use] -extern crate pest_derive; - +pub const CONFIG_PATH: &'static str = "/etc/embassy/config.toml"; pub const TOR_RC: &'static str = "/root/appmgr/tor/torrc"; pub const SERVICES_YAML: &'static str = "tor/services.yaml"; pub const VOLUMES: &'static str = "/root/volumes"; @@ -20,35 +16,37 @@ lazy_static::lazy_static! { pub static ref QUIET: tokio::sync::RwLock = tokio::sync::RwLock::new(!std::env::var("APPMGR_QUIET").map(|a| a == "0").unwrap_or(true)); } -pub mod actions; -pub mod apps; +pub mod action; pub mod backup; pub mod config; -pub mod control; +pub mod context; +pub mod db; pub mod dependencies; -pub mod disks; pub mod error; -pub mod index; -pub mod inspect; +pub mod id; pub mod install; -#[cfg(feature = "avahi")] -pub mod lan; -pub mod logs; -pub mod manifest; -pub mod pack; +pub mod migration; +pub mod net; pub mod registry; -pub mod remove; -pub mod tor; -pub mod update; +pub mod s9pk; +pub mod status; pub mod util; pub mod version; +pub mod volume; -pub use config::{configure, Config}; -pub use control::{restart_app, start_app, stop_app, stop_dependents}; -pub use error::{Error, ResultExt}; -pub use install::{install_name, install_path, install_url}; -pub use logs::{logs, notifications, stats, LogOptions}; -pub use pack::{pack, verify}; -pub use remove::remove; -pub use update::update; +pub use config::Config; +use context::{CliContext, EitherContext}; +pub use error::{Error, ErrorKind, ResultExt}; +use rpc_toolkit::command; +use rpc_toolkit::yajrc::RpcError; pub use version::{init, self_update}; + +#[command(subcommands(config::config, version::git_info))] +pub fn main_api(#[context] ctx: EitherContext) -> Result { + Ok(ctx) +} + +#[command(subcommands(version::git_info))] +pub fn portable_api(#[context] ctx: EitherContext) -> Result { + Ok(ctx) +} diff --git a/appmgr/src/logs.rs b/appmgr/src/logs.rs deleted file mode 100644 index da6cf5b63..000000000 --- a/appmgr/src/logs.rs +++ /dev/null @@ -1,199 +0,0 @@ -use std::borrow::Cow; -use std::ffi::{OsStr, OsString}; -use std::path::Path; - -use failure::ResultExt as _; -use futures::stream::StreamExt; -use futures::stream::TryStreamExt; -use itertools::Itertools; - -use crate::util::PersistencePath; -use crate::Error; -use crate::ResultExt as _; - -#[derive(Clone, Copy, Debug, serde::Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum Level { - Error, - Warn, - Success, - Info, -} -impl std::fmt::Display for Level { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Level::Error => write!(f, "ERROR"), - Level::Warn => write!(f, "WARN"), - Level::Success => write!(f, "SUCCESS"), - Level::Info => write!(f, "INFO"), - } - } -} -impl std::str::FromStr for Level { - type Err = Error; - fn from_str(s: &str) -> Result { - match s { - "ERROR" => Ok(Level::Error), - "WARN" => Ok(Level::Warn), - "SUCCESS" => Ok(Level::Success), - "INFO" => Ok(Level::Info), - _ => Err(Error::from(format_err!("Unknown Notification Level"))), - } - } -} - -#[derive(Clone, Debug, serde::Serialize)] -pub struct Notification { - pub time: i64, - pub level: Level, - pub code: usize, - pub title: String, - pub message: String, -} -impl std::fmt::Display for Notification { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}:{}:{}:{}", - self.level, - self.code, - self.title.replace(":", "\u{A789}"), - self.message.replace("\n", "\u{2026}") - ) - } -} -impl std::str::FromStr for Notification { - type Err = Error; - fn from_str(s: &str) -> Result { - let mut split = s.split(":"); - Ok(Notification { - time: split - .next() - .ok_or_else(|| format_err!("missing time"))? - .parse::() - .map(|a| a as i64) - .no_code()?, - level: split - .next() - .ok_or_else(|| format_err!("missing level"))? - .parse()?, - code: split - .next() - .ok_or_else(|| format_err!("missing code"))? - .parse() - .no_code()?, - title: split - .next() - .ok_or_else(|| format_err!("missing title"))? - .replace("\u{A789}", ":"), - message: split - .intersperse(":") - .collect::() - .replace("\u{2026}", "\n"), - }) - } -} - -pub struct LogOptions, B: AsRef> { - pub details: bool, - pub follow: bool, - pub since: Option, - pub until: Option, - pub tail: Option, - pub timestamps: bool, -} - -pub async fn logs, B: AsRef>( - name: &str, - options: LogOptions, -) -> Result<(), Error> { - let mut args = vec![Cow::Borrowed(OsStr::new("logs"))]; - if options.details { - args.push(Cow::Borrowed(OsStr::new("--details"))); - } - if options.follow { - args.push(Cow::Borrowed(OsStr::new("-f"))); - } - if let Some(since) = options.since.as_ref() { - args.push(Cow::Borrowed(OsStr::new("--since"))); - args.push(Cow::Borrowed(OsStr::new(since.as_ref()))); - } - if let Some(until) = options.until.as_ref() { - args.push(Cow::Borrowed(OsStr::new("--until"))); - args.push(Cow::Borrowed(OsStr::new(until.as_ref()))); - } - if let Some(tail) = options.tail { - args.push(Cow::Borrowed(OsStr::new("--tail"))); - args.push(Cow::Owned(OsString::from(format!("{}", tail)))); - } - if options.timestamps { - args.push(Cow::Borrowed(OsStr::new("-t"))); - } - args.push(Cow::Borrowed(OsStr::new(name))); - crate::ensure_code!( - std::process::Command::new("docker") - .args(args.into_iter()) - .status()? - .success(), - crate::error::DOCKER_ERROR, - "Failed to Collect Logs from Docker" - ); - Ok(()) -} - -pub async fn notifications(id: &str) -> Result, Error> { - let p = PersistencePath::from_ref("notifications").join(id).tmp(); - if let Some(parent) = p.parent() { - if !parent.exists() { - tokio::fs::create_dir_all(parent).await?; - } - } - match tokio::fs::rename( - Path::new(crate::VOLUMES) - .join(id) - .join("start9") - .join("notifications.log"), - &p, - ) - .await - { - Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), - a => a, - }?; - let f = tokio::fs::File::open(&p) - .await - .with_context(|e| format!("{}: {}", p.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - tokio::io::AsyncBufReadExt::lines(tokio::io::BufReader::new(f)) - .map(|a| a.map_err(From::from).and_then(|a| a.parse())) - .try_collect() - .await -} - -pub async fn stats(id: &str) -> Result { - let p = PersistencePath::from_ref("stats").join(id).tmp(); - if let Some(parent) = p.parent() { - if !parent.exists() { - tokio::fs::create_dir_all(parent).await?; - } - } - match tokio::fs::copy( - Path::new(crate::VOLUMES) - .join(id) - .join("start9") - .join("stats.yaml"), - &p, - ) - .await - { - Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => { - return Ok(serde_yaml::Value::Null) - } - a => a, - }?; - let f = tokio::fs::File::open(&p) - .await - .with_context(|e| format!("{}: {}", p.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - crate::util::from_yaml_async_reader(f).await.no_code() -} diff --git a/appmgr/src/main.rs b/appmgr/src/main.rs deleted file mode 100644 index 964244114..000000000 --- a/appmgr/src/main.rs +++ /dev/null @@ -1,1711 +0,0 @@ -#![type_length_limit = "10000000"] - -use std::borrow::Cow; -use std::path::Path; - -use appmgrlib::version::VersionT; -use appmgrlib::*; - -use clap::{App, Arg, SubCommand}; - -#[tokio::main] -async fn main() { - match inner_main().await { - Ok(()) => (), - Err(e) => { - eprintln!("{}", e.failure); - log::warn!("{:?}", e.failure); - std::process::exit(e.code.unwrap_or(1)); - } - } -} - -async fn inner_main() -> Result<(), Error> { - simple_logging::log_to_stderr(log::LevelFilter::Info); - #[cfg(not(feature = "portable"))] - { - if !Path::new(crate::PERSISTENCE_DIR).join(".lock").exists() { - tokio::fs::create_dir_all(crate::PERSISTENCE_DIR).await?; - tokio::fs::File::create(Path::new(crate::PERSISTENCE_DIR).join(".lock")).await?; - } - } - let q = *QUIET.read().await; - *QUIET.write().await = true; - #[cfg(not(feature = "portable"))] - init().await?; - *QUIET.write().await = q; - let version = format!("{}", crate::version::Current::new().semver()); - let git_version = - git_version::git_version!(args = ["--always", "--abbrev=40", "--dirty=-modified"]); - #[cfg(not(feature = "production"))] - let git_version = format!("{}-dev", git_version); - #[allow(unused_mut)] - let mut app = App::new("Start9 Application Manager") - .version(version.as_str()) - .author("Dr. BoneZ ") - .about("Manage applications installed on the Start9 Embassy") - .arg( - Arg::with_name("verbosity") - .short("v") - .help("Sets verbosity level") - .multiple(true), - ) - .subcommand(SubCommand::with_name("semver").about("Prints semantic version and exits")) - .subcommand(SubCommand::with_name("git-info").about("Prints git version info and exits")) - .subcommand( - SubCommand::with_name("pack") - .about("Creates a new application package") - .arg( - Arg::with_name("output") - .short("o") - .long("output") - .takes_value(true) - .default_value("app.s9pk"), - ) - .arg( - Arg::with_name("PATH") - .help("Path to the folder containing the application data") - .required(true), - ), - ) - .subcommand( - SubCommand::with_name("verify") - .about("Verifies an application package") - .arg( - Arg::with_name("PATH") - .help("Path to the s9pk file to verify") - .required(true), - ), - ) - .subcommand( - SubCommand::with_name("inspect") - .about("Inspects an application package") - .subcommand( - SubCommand::with_name("info") - .about("Prints information about an app") - .arg( - Arg::with_name("PATH") - .help("Path to the s9pk file to inspect") - .required(true), - ) - .arg( - Arg::with_name("json") - .conflicts_with("yaml") - .required_unless("yaml") - .long("json") - .short("j") - .help("Output as json"), - ) - .arg( - Arg::with_name("pretty") - .requires("json") - .long("pretty") - .short("p") - .help("Pretty print output"), - ) - .arg( - Arg::with_name("yaml") - .conflicts_with("json") - .required_unless("json") - .long("yaml") - .short("y") - .help("Output as yaml"), - ) - .arg( - Arg::with_name("include-manifest") - .long("include-manifest") - .short("m"), - ) - .arg( - Arg::with_name("include-config") - .long("include-config") - .short("c"), - ) - .arg( - Arg::with_name("only-manifest") - .long("only-manifest") - .short("M") - .conflicts_with_all(&[ - "include-manifest", - "include-config", - "only-config", - ]), - ) - .arg( - Arg::with_name("only-config") - .long("only-config") - .short("C") - .conflicts_with_all(&[ - "include-manifest", - "include-config", - "only-manifest", - ]), - ), - ) - .subcommand( - SubCommand::with_name("instructions") - .about("Prints instructions for an app") - .arg( - Arg::with_name("PATH") - .help("Path to the s9pk file to inspect") - .required(true), - ), - ), - ) - .subcommand( - SubCommand::with_name("index") - .about("Indexes all s9pk files in a directory") - .arg( - Arg::with_name("DIR") - .help("Path to the directory to index") - .required(true), - ), - ); - - #[cfg(feature = "avahi")] - #[allow(unused_mut)] - let mut app = app.subcommand( - SubCommand::with_name("lan") - .about("Configures LAN services") - .subcommand( - SubCommand::with_name("enable") - .about("Publishes the LAN addresses for all services"), - ), - ); - - #[cfg(not(feature = "portable"))] - let mut app = app - .subcommand( - SubCommand::with_name("install") - .about("Installs a new app") - .arg( - Arg::with_name("no-cache") - .long("no-cache") - .help("Replace cached download of application"), - ) - .arg( - Arg::with_name("ID|PATH|URL") - .help("The app to install") - .long_help(concat!( - "The app to install\n", - "ID: The id of the app in the Start9 registry\n", - "PATH: The path to the s9pk file on your local file system\n", - "URL: The url of the s9pk file" - )) - .required(true), - ), - ) - .subcommand( - SubCommand::with_name("update") - .about("Updates an app") - .arg( - Arg::with_name("ID") - .help("The id of the app in the Start9 registry") - .required(true), - ) - .arg( - Arg::with_name("dry-run") - .long("dry-run") - .help("Do not commit result"), - ) - .arg( - Arg::with_name("json") - .conflicts_with("yaml") - .long("json") - .short("j") - .help("Output as json"), - ) - .arg( - Arg::with_name("pretty") - .requires("json") - .long("pretty") - .short("p") - .help("Pretty print output"), - ) - .arg( - Arg::with_name("yaml") - .conflicts_with("json") - .long("yaml") - .short("y") - .help("Output as yaml"), - ), - ) - .subcommand( - SubCommand::with_name("start") - .about("Starts an app") - .arg(Arg::with_name("ID").help("The app to start").required(true)), - ) - .subcommand( - SubCommand::with_name("stop") - .about("Stops an app") - .arg(Arg::with_name("ID").help("The app to stop").required(true)) - .arg( - Arg::with_name("dry-run") - .long("dry-run") - .help("Do not commit result"), - ) - .arg( - Arg::with_name("json") - .conflicts_with("yaml") - .long("json") - .short("j") - .help("Output as json"), - ) - .arg( - Arg::with_name("pretty") - .requires("json") - .long("pretty") - .short("p") - .help("Pretty print output"), - ) - .arg( - Arg::with_name("yaml") - .conflicts_with("json") - .long("yaml") - .short("y") - .help("Output as yaml"), - ), - ) - .subcommand( - SubCommand::with_name("restart") - .about("Restarts an app") - .arg( - Arg::with_name("ID") - .help("The app to restart") - .required(true), - ), - ) - .subcommand( - SubCommand::with_name("configure") - .about("Configures an app") - .arg( - Arg::with_name("ID") - .help("The app to configure") - .required(true), - ) - .arg(Arg::with_name("FILE").help("The configuration file to use")) - .arg( - Arg::with_name("stdin") - .long("stdin") - .help("Use stdin for the config file") - .conflicts_with("FILE"), - ) - .arg( - Arg::with_name("timeout") - .short("t") - .long("timeout") - .help("Max seconds to attempt generating entropy per field") - .default_value("3") - .conflicts_with("no-timeout"), - ) - .arg( - Arg::with_name("no-timeout") - .long("no-timeout") - .help("Disable timeout on entropy generation") - .conflicts_with("timeout"), - ) - .arg( - Arg::with_name("dry-run") - .long("dry-run") - .help("Do not commit result"), - ) - .arg( - Arg::with_name("json") - .conflicts_with("yaml") - .long("json") - .short("j") - .help("Output as json"), - ) - .arg( - Arg::with_name("pretty") - .requires("json") - .long("pretty") - .short("p") - .help("Pretty print output"), - ) - .arg( - Arg::with_name("yaml") - .conflicts_with("json") - .long("yaml") - .short("y") - .help("Output as yaml"), - ), - ) - .subcommand( - SubCommand::with_name("check-dependencies") - .about("Check dependencies for an app") - .arg( - Arg::with_name("ID") - .help("The app to check dependencies for.") - .required(true), - ) - .arg(Arg::with_name("local-only").long("local-only").help( - "Disable reaching out to the Start9 registry if the app isn't installed.", - )) - .arg( - Arg::with_name("json") - .conflicts_with("yaml") - .long("json") - .short("j") - .help("Output as json"), - ) - .arg( - Arg::with_name("pretty") - .requires("json") - .long("pretty") - .short("p") - .help("Pretty print output"), - ) - .arg( - Arg::with_name("yaml") - .conflicts_with("json") - .long("yaml") - .short("y") - .help("Output as yaml"), - ), - ) - .subcommand( - SubCommand::with_name("autoconfigure-dependency") - .about("Automatically configure a dependency") - .arg( - Arg::with_name("ID") - .help("The app to autoconfigure a dependency for.") - .required(true), - ) - .arg( - Arg::with_name("DEPENDENCY") - .help("The dependency to autoconfigure.") - .required(true), - ) - .arg( - Arg::with_name("dry-run") - .long("dry-run") - .help("Do not commit result"), - ) - .arg( - Arg::with_name("json") - .conflicts_with("yaml") - .long("json") - .short("j") - .help("Output as json"), - ) - .arg( - Arg::with_name("pretty") - .requires("json") - .long("pretty") - .short("p") - .help("Pretty print output"), - ) - .arg( - Arg::with_name("yaml") - .conflicts_with("json") - .long("yaml") - .short("y") - .help("Output as yaml"), - ), - ) - .subcommand( - SubCommand::with_name("remove") - .alias("rm") - .about("Removes an installed app") - .arg( - Arg::with_name("purge") - .long("purge") - .help("Deletes all application data"), - ) - .arg( - Arg::with_name("ID") - .help("ID of the application to be removed") - .required(true), - ) - .arg( - Arg::with_name("dry-run") - .long("dry-run") - .help("Do not commit result"), - ) - .arg( - Arg::with_name("json") - .conflicts_with("yaml") - .long("json") - .short("j") - .help("Output as json"), - ) - .arg( - Arg::with_name("pretty") - .requires("json") - .long("pretty") - .short("p") - .help("Pretty print output"), - ) - .arg( - Arg::with_name("yaml") - .conflicts_with("json") - .long("yaml") - .short("y") - .help("Output as yaml"), - ), - ) - .subcommand( - SubCommand::with_name("tor") - .about("Configures tor hidden services") - .subcommand( - SubCommand::with_name("show") - .about("Shows the onion address for the hidden service") - .arg( - Arg::with_name("ID") - .help("ID of the application to get the onion address for") - .required(true), - ), - ) - .subcommand(SubCommand::with_name("reload").about("Reloads the tor configuration")), - ) - .subcommand( - SubCommand::with_name("info") - .about("Prints information about an installed app") - .arg( - Arg::with_name("ID") - .help("ID of the application to print information about") - .required(true), - ) - .arg( - Arg::with_name("json") - .conflicts_with("yaml") - .required_unless("yaml") - .long("json") - .short("j") - .help("Output as json"), - ) - .arg( - Arg::with_name("pretty") - .requires("json") - .long("pretty") - .short("p") - .help("Pretty print output"), - ) - .arg( - Arg::with_name("yaml") - .conflicts_with("json") - .required_unless("json") - .long("yaml") - .short("y") - .help("Output as yaml"), - ) - .arg( - Arg::with_name("include-status") - .long("include-status") - .short("s"), - ) - .arg( - Arg::with_name("include-manifest") - .long("include-manifest") - .short("m"), - ) - .arg( - Arg::with_name("include-config") - .long("include-config") - .short("c"), - ) - .arg( - Arg::with_name("include-dependencies") - .long("include-dependencies") - .short("d"), - ) - .arg( - Arg::with_name("only-status") - .long("only-status") - .short("S") - .conflicts_with_all(&[ - "include-status", - "include-manifest", - "include-config", - "include-dependencies", - "only-manifest", - "only-config", - "only-dependencies", - ]), - ) - .arg( - Arg::with_name("only-manifest") - .long("only-manifest") - .short("M") - .conflicts_with_all(&[ - "include-status", - "include-manifest", - "include-config", - "include-dependencies", - "only-status", - "only-config", - "only-dependencies", - ]), - ) - .arg( - Arg::with_name("only-config") - .long("only-config") - .short("C") - .conflicts_with_all(&[ - "include-status", - "include-manifest", - "include-config", - "include-dependencies", - "only-status", - "only-manifest", - "only-dependencies", - ]), - ) - .arg( - Arg::with_name("only-dependencies") - .long("only-dependencies") - .short("D") - .conflicts_with_all(&[ - "include-status", - "include-manifest", - "include-config", - "include-dependencies", - "only-status", - "only-manifest", - "only-config", - ]), - ), - ) - .subcommand( - SubCommand::with_name("instructions") - .about("Prints instructions for an installed app") - .arg( - Arg::with_name("ID") - .help("ID of the application to print instructions for") - .required(true), - ), - ) - .subcommand( - SubCommand::with_name("list") - .alias("ls") - .about("Lists apps successfully installed on the system") - .arg( - Arg::with_name("json") - .conflicts_with("yaml") - .long("json") - .short("j") - .help("Output as json"), - ) - .arg( - Arg::with_name("pretty") - .requires("json") - .long("pretty") - .short("p") - .help("Pretty print output"), - ) - .arg( - Arg::with_name("yaml") - .conflicts_with("json") - .long("yaml") - .short("y") - .help("Output as yaml"), - ) - .arg( - Arg::with_name("include-status") - .long("include-status") - .short("s"), - ) - .arg( - Arg::with_name("include-manifest") - .long("include-manifest") - .short("m"), - ) - .arg( - Arg::with_name("include-config") - .long("include-config") - .short("c"), - ) - .arg( - Arg::with_name("include-dependencies") - .long("include-dependencies") - .short("d"), - ), - ) - .subcommand( - SubCommand::with_name("self-update") - .about("Updates appmgr") - .arg( - Arg::with_name("VERSION_REQUIREMENT") - .help("Version requirement to update to (i.e. ^0.1.0)"), - ), - ) - .subcommand( - SubCommand::with_name("logs") - .about("Fetch the logs of an app") - .arg( - Arg::with_name("ID") - .help("ID of the application to fetch logs for") - .required(true), - ) - .arg( - Arg::with_name("details") - .help("Show extra details provided to logs") - .long("details"), - ) - .arg( - Arg::with_name("follow") - .help("Follow log output") - .long("follow") - .short("f"), - ) - .arg( - Arg::with_name("since") - .help(concat!( - "Show logs since timestamp (e.g. 2013-01-02T13:23:37)", - " or relative (e.g. 42m for 42 minutes)" - )) - .long("since") - .takes_value(true), - ) - .arg( - Arg::with_name("tail") - .help("Number of lines to show from the end of the logs") - .long("tail") - .takes_value(true) - .default_value("all"), - ) - .arg( - Arg::with_name("timestamps") - .help("Show timestamps") - .short("t") - .long("timestamps"), - ) - .arg( - Arg::with_name("until") - .help(concat!( - "Show logs before a timestamp (e.g. 2013-01-02T13:23:37)", - " or relative (e.g. 42m for 42 minutes)" - )) - .long("until") - .takes_value(true), - ), - ) - .subcommand( - SubCommand::with_name("notifications") - .about("Get notifications broadcast by an app") - .arg( - Arg::with_name("ID") - .help("ID of the application to get notifications for") - .required(true), - ) - .arg( - Arg::with_name("json") - .conflicts_with("yaml") - .long("json") - .short("j") - .help("Output as json"), - ) - .arg( - Arg::with_name("pretty") - .requires("json") - .long("pretty") - .short("p") - .help("Pretty print output"), - ) - .arg( - Arg::with_name("yaml") - .conflicts_with("json") - .long("yaml") - .short("y") - .help("Output as yaml"), - ), - ) - .subcommand( - SubCommand::with_name("stats") - .about("Get stats broadcast by an app") - .arg( - Arg::with_name("ID") - .help("ID of the application to get stats for") - .required(true), - ) - .arg( - Arg::with_name("json") - .conflicts_with("yaml") - .required_unless("yaml") - .long("json") - .short("j") - .help("Output as json"), - ) - .arg( - Arg::with_name("pretty") - .requires("json") - .long("pretty") - .short("p") - .help("Pretty print output"), - ) - .arg( - Arg::with_name("yaml") - .conflicts_with("json") - .required_unless("json") - .long("yaml") - .short("y") - .help("Output as yaml"), - ), - ) - .subcommand( - SubCommand::with_name("disks") - .about("Manage external disks") - .subcommand( - SubCommand::with_name("show") - .alias("list") - .alias("ls") - .about("List external drive information") - .arg( - Arg::with_name("json") - .conflicts_with("yaml") - .long("json") - .short("j") - .help("Output as json"), - ) - .arg( - Arg::with_name("pretty") - .requires("json") - .long("pretty") - .short("p") - .help("Pretty print output"), - ) - .arg( - Arg::with_name("yaml") - .conflicts_with("json") - .long("yaml") - .short("y") - .help("Output as yaml"), - ), - ) - .subcommand(SubCommand::with_name("use")), - ) - .subcommand( - SubCommand::with_name("backup") - .about("Manage app data backups") - .subcommand( - SubCommand::with_name("create") - .about("Backup current app state") - .arg( - Arg::with_name("ID") - .help("ID of the application to backup data for") - .required(true), - ) - .arg( - Arg::with_name("PARTITION") - .help("Logical name of the partition you would like to backup to") - .required(true), - ) - .arg( - Arg::with_name("password") - .long("password") - .short("p") - .takes_value(true) - .help("Password to use for encryption of backup file"), - ), - ) - .subcommand( - SubCommand::with_name("restore") - .about("Restore app state from backup") - .arg( - Arg::with_name("ID") - .help("ID of the application to restore data for") - .required(true), - ) - .arg( - Arg::with_name("PARTITION") - .help("Logical name of the partition you would like to backup to") - .required(true), - ) - .arg( - Arg::with_name("timestamp") - .long("timestamp") - .short("t") - .takes_value(true) - .help("Timestamp of the backup to restore"), - ) - .arg( - Arg::with_name("password") - .long("password") - .short("p") - .takes_value(true) - .help("Password to use for encryption of backup file"), - ), - ), - ) - .subcommand( - SubCommand::with_name("repair-app-status").about("Restarts crashed apps"), // TODO: remove - ) - .subcommand( - SubCommand::with_name("actions") - .about("Perform an action for a service") - .arg( - Arg::with_name("SERVICE") - .help("ID of the service to perform an action on") - .required(true), - ) - .arg(Arg::with_name("ACTION").help("ID of the action to perform")), - ); - - let matches = app.clone().get_matches(); - - log::set_max_level(match matches.occurrences_of("verbosity") { - 0 => log::LevelFilter::Error, - 1 => log::LevelFilter::Warn, - 2 => log::LevelFilter::Info, - 3 => log::LevelFilter::Debug, - _ => log::LevelFilter::Trace, - }); - - match matches.subcommand() { - ("semver", _) => { - println!("{}", version); - } - ("git-info", _) => { - println!("{}", git_version); - } - #[cfg(not(feature = "portable"))] - ("install", Some(sub_m)) => { - let target = sub_m.value_of("ID|PATH|URL").unwrap(); - if target.starts_with("https://") || target.starts_with("http://") { - install_url(target, None).await?; - } else if target.ends_with(".s9pk") { - install_path(target, None).await?; - } else { - install_name(target, !sub_m.is_present("no-cache")).await?; - } - } - #[cfg(not(feature = "portable"))] - ("update", Some(sub_m)) => { - let res = update(sub_m.value_of("ID").unwrap(), sub_m.is_present("dry-run")).await?; - if sub_m.is_present("json") { - if sub_m.is_present("pretty") { - println!( - "{}", - serde_json::to_string_pretty(&res).with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string(&res).with_code(crate::error::SERDE_ERROR)? - ); - } - } else if sub_m.is_present("yaml") { - println!( - "{}", - serde_yaml::to_string(&res).with_code(crate::error::SERDE_ERROR)? - ); - } else if !res.is_empty() { - use prettytable::{Cell, Row, Table}; - let mut table = Table::new(); - let heading = vec![ - Cell::new("APPLICATION ID"), - Cell::new("STATUS"), - Cell::new("REASON"), - ]; - table.add_row(Row::new(heading)); - for (name, reason) in res { - table.add_row(Row::new(vec![ - Cell::new(&name), - Cell::new("Stopped"), - Cell::new(&format!("{}", reason)), - ])); - } - table.print(&mut std::io::stdout())?; - } - } - #[cfg(not(feature = "portable"))] - ("start", Some(sub_m)) => { - start_app(sub_m.value_of("ID").unwrap(), true).await?; - } - #[cfg(not(feature = "portable"))] - ("stop", Some(sub_m)) => { - let res = stop_app( - sub_m.value_of("ID").unwrap(), - true, - sub_m.is_present("dry-run"), - ) - .await?; - if sub_m.is_present("json") { - if sub_m.is_present("pretty") { - println!( - "{}", - serde_json::to_string_pretty(&res).with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string(&res).with_code(crate::error::SERDE_ERROR)? - ); - } - } else if sub_m.is_present("yaml") { - println!( - "{}", - serde_yaml::to_string(&res).with_code(crate::error::SERDE_ERROR)? - ); - } else if !res.is_empty() { - use prettytable::{Cell, Row, Table}; - let mut table = Table::new(); - let heading = vec![ - Cell::new("APPLICATION ID"), - Cell::new("STATUS"), - Cell::new("REASON"), - ]; - table.add_row(Row::new(heading)); - for (name, reason) in res { - table.add_row(Row::new(vec![ - Cell::new(&name), - Cell::new("Stopped"), - Cell::new(&format!("{}", reason)), - ])); - } - table.print(&mut std::io::stdout())?; - } - } - #[cfg(not(feature = "portable"))] - ("restart", Some(sub_m)) => { - restart_app(sub_m.value_of("ID").unwrap()).await?; - } - #[cfg(not(feature = "portable"))] - ("configure", Some(sub_m)) => { - let config: Option = if let Some(path) = sub_m.value_of("FILE") { - let p = Path::new(path); - if p.extension() == Some(std::ffi::OsStr::new("json")) - || (sub_m.is_present("json") - && p.extension() != Some(std::ffi::OsStr::new("yaml"))) - { - Some(util::from_json_async_reader(tokio::fs::File::open(p).await?).await?) - } else { - Some(util::from_yaml_async_reader(tokio::fs::File::open(p).await?).await?) - } - } else if sub_m.is_present("stdin") { - if sub_m.is_present("json") { - Some(util::from_yaml_async_reader(tokio::io::stdin()).await?) - } else { - Some(util::from_yaml_async_reader(tokio::io::stdin()).await?) - } - } else { - None - }; - let timeout = if sub_m.is_present("no-timeout") { - None - } else if let Some(t) = sub_m.value_of("timeout") { - Some(std::time::Duration::from_secs(t.parse().no_code()?)) - } else { - Some(std::time::Duration::from_secs(3)) - }; - let res = configure( - sub_m.value_of("ID").unwrap(), - config, - timeout, - sub_m.is_present("dry-run"), - ) - .await?; - if sub_m.is_present("json") { - if sub_m.is_present("pretty") { - println!( - "{}", - serde_json::to_string_pretty(&res).with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string(&res).with_code(crate::error::SERDE_ERROR)? - ); - } - } else if sub_m.is_present("yaml") { - println!( - "{}", - serde_yaml::to_string(&res).with_code(crate::error::SERDE_ERROR)? - ); - } else if !res.needs_restart.is_empty() || !res.stopped.is_empty() { - use prettytable::{Cell, Row, Table}; - let mut table = Table::new(); - let heading = vec![ - Cell::new("APPLICATION ID"), - Cell::new("STATUS"), - Cell::new("REASON"), - ]; - table.add_row(Row::new(heading)); - for name in res.needs_restart { - table.add_row(Row::new(vec![ - Cell::new(&name), - Cell::new("Needs Restart"), - Cell::new("Configuration Changed"), - ])); - } - for (name, reason) in res.stopped { - table.add_row(Row::new(vec![ - Cell::new(&name), - Cell::new("Stopped"), - Cell::new(&format!("{}", reason)), - ])); - } - table.print(&mut std::io::stdout())?; - } - } - #[cfg(not(feature = "portable"))] - ("check-dependencies", Some(sub_m)) => { - let res = apps::dependencies( - sub_m.value_of("ID").unwrap(), - sub_m.is_present("local-only"), - ) - .await?; - if sub_m.is_present("json") { - if sub_m.is_present("pretty") { - println!( - "{}", - serde_json::to_string_pretty(&res).with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string(&res).with_code(crate::error::SERDE_ERROR)? - ); - } - } else if sub_m.is_present("yaml") { - println!( - "{}", - serde_yaml::to_string(&res).with_code(crate::error::SERDE_ERROR)? - ); - } else if !res.0.is_empty() { - use prettytable::{Cell, Row, Table}; - let mut table = Table::new(); - let heading = vec![ - Cell::new("APPLICATION ID"), - Cell::new("REQUIRED"), - Cell::new("VIOLATION"), - ]; - table.add_row(Row::new(heading)); - for (name, info) in res.0 { - table.add_row(Row::new(vec![ - Cell::new(&name), - Cell::new(&format!("{}", info.required)), - Cell::new(&if let Some(error) = info.error { - format!("{}", error) - } else { - "N/A".to_owned() - }), - ])); - } - table.print(&mut std::io::stdout())?; - } else { - println!("No dependencies for {}", sub_m.value_of("ID").unwrap()); - } - } - ("autoconfigure-dependency", Some(sub_m)) => { - let res = dependencies::auto_configure( - sub_m.value_of("ID").unwrap(), - sub_m.value_of("DEPENDENCY").unwrap(), - sub_m.is_present("dry-run"), - ) - .await?; - if sub_m.is_present("json") { - if sub_m.is_present("pretty") { - println!( - "{}", - serde_json::to_string_pretty(&res).with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string(&res).with_code(crate::error::SERDE_ERROR)? - ); - } - } else if sub_m.is_present("yaml") { - println!( - "{}", - serde_yaml::to_string(&res).with_code(crate::error::SERDE_ERROR)? - ); - } else if !res.needs_restart.is_empty() || !res.stopped.is_empty() { - use prettytable::{Cell, Row, Table}; - let mut table = Table::new(); - let heading = vec![ - Cell::new("APPLICATION ID"), - Cell::new("STATUS"), - Cell::new("REASON"), - ]; - table.add_row(Row::new(heading)); - for name in res.needs_restart { - table.add_row(Row::new(vec![ - Cell::new(&name), - Cell::new("Needs Restart"), - Cell::new("Configuration Changed"), - ])); - } - for (name, reason) in res.stopped { - table.add_row(Row::new(vec![ - Cell::new(&name), - Cell::new("Stopped"), - Cell::new(&format!("{}", reason)), - ])); - } - table.print(&mut std::io::stdout())?; - } - } - #[cfg(not(feature = "portable"))] - ("remove", Some(sub_m)) | ("rm", Some(sub_m)) => { - let res = remove( - sub_m.value_of("ID").unwrap(), - sub_m.is_present("purge"), - sub_m.is_present("dry-run"), - ) - .await?; - if sub_m.is_present("json") { - if sub_m.is_present("pretty") { - println!( - "{}", - serde_json::to_string_pretty(&res).with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string(&res).with_code(crate::error::SERDE_ERROR)? - ); - } - } else if sub_m.is_present("yaml") { - println!( - "{}", - serde_yaml::to_string(&res).with_code(crate::error::SERDE_ERROR)? - ); - } else if !res.is_empty() { - use prettytable::{Cell, Row, Table}; - let mut table = Table::new(); - let heading = vec![ - Cell::new("APPLICATION ID"), - Cell::new("STATUS"), - Cell::new("REASON"), - ]; - table.add_row(Row::new(heading)); - for (name, reason) in res { - table.add_row(Row::new(vec![ - Cell::new(&name), - Cell::new("Stopped"), - Cell::new(&format!("{}", reason)), - ])); - } - table.print(&mut std::io::stdout())?; - } - } - #[cfg(not(feature = "portable"))] - ("tor", Some(sub_m)) => match sub_m.subcommand() { - ("show", Some(sub_sub_m)) => { - println!( - "{}", - crate::tor::read_tor_address(sub_sub_m.value_of("ID").unwrap(), None).await? - ); - } - ("reload", Some(_)) => { - crate::tor::reload().await?; - } - _ => { - println!("{}", sub_m.usage()); - std::process::exit(1); - } - }, - #[cfg(feature = "avahi")] - #[cfg(not(feature = "portable"))] - ("lan", Some(sub_m)) => match sub_m.subcommand() { - ("enable", _) => crate::lan::enable_lan().await?, - _ => { - println!("{}", sub_m.usage()); - std::process::exit(1); - } - }, - #[cfg(not(feature = "portable"))] - ("info", Some(sub_m)) => { - let name = sub_m.value_of("ID").unwrap(); - let info = crate::apps::info_full( - &name, - sub_m.is_present("include-status") || sub_m.is_present("only-status"), - sub_m.is_present("include-manifest") || sub_m.is_present("only-manifest"), - sub_m.is_present("include-config") || sub_m.is_present("only-config"), - sub_m.is_present("include-dependencies") || sub_m.is_present("only-dependencies"), - ) - .await?; - if sub_m.is_present("json") { - if sub_m.is_present("pretty") { - if sub_m.is_present("only-status") { - println!( - "{}", - serde_json::to_string_pretty(&info.status) - .with_code(crate::error::SERDE_ERROR)? - ); - } else if sub_m.is_present("only-manifest") { - println!( - "{}", - serde_json::to_string_pretty(&info.manifest) - .with_code(crate::error::SERDE_ERROR)? - ); - } else if sub_m.is_present("only-config") { - println!( - "{}", - serde_json::to_string_pretty(&info.config) - .with_code(crate::error::SERDE_ERROR)? - ); - } else if sub_m.is_present("only-dependencies") { - println!( - "{}", - serde_json::to_string_pretty(&info.dependencies) - .with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string_pretty(&info) - .with_code(crate::error::SERDE_ERROR)? - ); - } - } else { - if sub_m.is_present("only-status") { - println!( - "{}", - serde_json::to_string(&info.status) - .with_code(crate::error::SERDE_ERROR)? - ); - } else if sub_m.is_present("only-manifest") { - println!( - "{}", - serde_json::to_string(&info.manifest) - .with_code(crate::error::SERDE_ERROR)? - ); - } else if sub_m.is_present("only-config") { - println!( - "{}", - serde_json::to_string(&info.config) - .with_code(crate::error::SERDE_ERROR)? - ); - } else if sub_m.is_present("only-dependencies") { - println!( - "{}", - serde_json::to_string(&info.dependencies) - .with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string(&info).with_code(crate::error::SERDE_ERROR)? - ); - } - } - } else if sub_m.is_present("yaml") { - if sub_m.is_present("only-status") { - println!( - "{}", - serde_yaml::to_string(&info.status).with_code(crate::error::SERDE_ERROR)? - ); - } else if sub_m.is_present("only-manifest") { - println!( - "{}", - serde_yaml::to_string(&info.manifest) - .with_code(crate::error::SERDE_ERROR)? - ); - } else if sub_m.is_present("only-config") { - println!( - "{}", - serde_yaml::to_string(&info.config).with_code(crate::error::SERDE_ERROR)? - ); - } else if sub_m.is_present("only-dependencies") { - println!( - "{}", - serde_yaml::to_string(&info.dependencies) - .with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_yaml::to_string(&info).with_code(crate::error::SERDE_ERROR)? - ); - } - } - } - #[cfg(not(feature = "portable"))] - ("instructions", Some(sub_m)) => { - crate::apps::print_instructions(sub_m.value_of("ID").unwrap()).await?; - } - #[cfg(not(feature = "portable"))] - ("list", Some(sub_m)) | ("ls", Some(sub_m)) => { - let info = crate::apps::list( - sub_m.is_present("include-status"), - sub_m.is_present("include-manifest"), - sub_m.is_present("include-config"), - sub_m.is_present("include-dependencies"), - ) - .await?; - if sub_m.is_present("json") { - if sub_m.is_present("pretty") { - println!( - "{}", - serde_json::to_string_pretty(&info).with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string(&info).with_code(crate::error::SERDE_ERROR)? - ); - } - } else if sub_m.is_present("yaml") { - println!( - "{}", - serde_yaml::to_string(&info).with_code(crate::error::SERDE_ERROR)? - ); - } else if !info.is_empty() { - use prettytable::{Cell, Row, Table}; - let mut table = Table::new(); - let mut heading = vec![ - Cell::new("APPLICATION ID"), - Cell::new("TITLE"), - Cell::new("VERSION"), - Cell::new("TOR ADDRESS"), - Cell::new("CONFIGURED"), - ]; - if sub_m.is_present("include-status") { - heading.push(Cell::new("STATUS")); - } - if sub_m.is_present("include-dependencies") { - heading.push(Cell::new("DEPENDENCIES MET")) - } - table.add_row(Row::new(heading)); - for (name, info) in info { - table.add_row(Row::new( - vec![ - Cell::new(&name), - Cell::new(&format!("{}", info.info.title)), - Cell::new(&format!("{}", info.info.version)), - Cell::new(&format!( - "{}", - info.info.tor_address.unwrap_or_else(|| "N/A".to_owned()) - )), - Cell::new(&format!("{}", info.info.configured)), - ] - .into_iter() - .chain( - info.status - .into_iter() - .map(|s| Cell::new(&format!("{:?}", s.status))), - ) - .chain(info.dependencies.into_iter().map(|s| { - Cell::new(&format!( - "{}", - s.0.into_iter() - .all(|(_, dep)| dep.error.is_none() || !dep.required) - )) - })) - .collect(), - )); - } - table.print(&mut std::io::stdout())?; - } else { - println!("No apps installed"); - } - } - #[cfg(not(feature = "portable"))] - ("self-update", Some(sub_m)) => { - self_update( - sub_m - .value_of("VERSION_REQUIREMENT") - .map(|a| a.parse()) - .transpose() - .no_code()? - .unwrap_or_else(|| emver::VersionRange::any()), - ) - .await?; - } - #[cfg(not(feature = "portable"))] - ("logs", Some(sub_m)) => { - logs( - sub_m.value_of("ID").unwrap(), - LogOptions { - details: sub_m.is_present("details"), - follow: sub_m.is_present("follow"), - since: sub_m.value_of("since"), - until: sub_m.value_of("until"), - tail: sub_m - .value_of("tail") - .filter(|t| t != &"all") - .map(|a| a.parse()) - .transpose() - .no_code()?, - timestamps: sub_m.is_present("timestamps"), - }, - ) - .await?; - } - #[cfg(not(feature = "portable"))] - ("notifications", Some(sub_m)) => { - let info = notifications(sub_m.value_of("ID").unwrap()).await?; - if sub_m.is_present("json") { - if sub_m.is_present("pretty") { - println!( - "{}", - serde_json::to_string_pretty(&info).with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string(&info).with_code(crate::error::SERDE_ERROR)? - ); - } - } else if sub_m.is_present("yaml") { - println!( - "{}", - serde_yaml::to_string(&info).with_code(crate::error::SERDE_ERROR)? - ); - } else if !info.is_empty() { - use prettytable::{Cell, Row, Table}; - let mut table = Table::new(); - let heading = vec![ - Cell::new("LEVEL"), - Cell::new("CODE"), - Cell::new("TITLE"), - Cell::new("MESSAGE"), - ]; - table.add_row(Row::new(heading)); - for note in info { - table.add_row(Row::new(vec![ - Cell::new(&format!("{}", note.level)), - Cell::new(&format!("{}", note.code)), - Cell::new(&format!("{}", note.title)), - Cell::new(&format!("{}", note.message)), - ])); - } - table.print(&mut std::io::stdout())?; - } else { - println!("No notifications for {}", sub_m.value_of("ID").unwrap()); - } - } - #[cfg(not(feature = "portable"))] - ("stats", Some(sub_m)) => { - let info = stats(sub_m.value_of("ID").unwrap()).await?; - if sub_m.is_present("json") { - if sub_m.is_present("pretty") { - println!( - "{}", - serde_json::to_string_pretty(&info).with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string(&info).with_code(crate::error::SERDE_ERROR)? - ); - } - } else if sub_m.is_present("yaml") { - println!( - "{}", - serde_yaml::to_string(&info).with_code(crate::error::SERDE_ERROR)? - ); - } else if let serde_yaml::Value::Mapping(map) = info { - use prettytable::{Cell, Row, Table}; - let mut table = Table::new(); - for (k, v) in map { - let ks = match k { - serde_yaml::Value::Bool(k) => format!("{}", k), - serde_yaml::Value::Null => "null".to_owned(), - serde_yaml::Value::Number(k) => format!("{}", k), - serde_yaml::Value::String(k) => k, - k => serde_yaml::to_string(&k).with_code(crate::error::SERDE_ERROR)?, - }; - let vs = match v { - serde_yaml::Value::Bool(v) => format!("{}", v), - serde_yaml::Value::Null => "null".to_owned(), - serde_yaml::Value::Number(v) => format!("{}", v), - serde_yaml::Value::String(v) => v, - v => serde_yaml::to_string(&v).with_code(crate::error::SERDE_ERROR)?, - }; - table.add_row(Row::new(vec![Cell::new(&ks), Cell::new(&vs)])); - } - table.print(&mut std::io::stdout())?; - } - } - #[cfg(not(feature = "portable"))] - ("disks", Some(sub_m)) => match sub_m.subcommand() { - ("show", Some(sub_sub_m)) | ("list", Some(sub_sub_m)) | ("ls", Some(sub_sub_m)) => { - let info = disks::list().await?; - if sub_sub_m.is_present("json") { - if sub_sub_m.is_present("pretty") { - println!( - "{}", - serde_json::to_string_pretty(&info) - .with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string(&info).with_code(crate::error::SERDE_ERROR)? - ); - } - } else if sub_sub_m.is_present("yaml") { - println!( - "{}", - serde_yaml::to_string(&info).with_code(crate::error::SERDE_ERROR)? - ); - } else { - todo!() - } - } - _ => { - println!("{}", sub_m.usage()); - std::process::exit(1); - } - }, - #[cfg(not(feature = "portable"))] - ("backup", Some(sub_m)) => match sub_m.subcommand() { - ("create", Some(sub_sub_m)) => { - crate::backup::backup_to_partition( - sub_sub_m.value_of("PARTITION").unwrap(), - sub_sub_m.value_of("ID").unwrap(), - &match sub_sub_m.value_of("password") { - Some(a) => Cow::Borrowed(a), - None => Cow::Owned(rpassword::read_password_from_tty(Some("Password: "))?), - }, - ) - .await? - } - ("restore", Some(sub_sub_m)) => { - crate::backup::restore_from_partition( - sub_sub_m.value_of("PARTITION").unwrap(), - sub_sub_m.value_of("ID").unwrap(), - &match sub_sub_m.value_of("password") { - Some(a) => Cow::Borrowed(a), - None => Cow::Owned(rpassword::read_password_from_tty(Some("Password: "))?), - }, - ) - .await? - } - _ => { - println!("{}", sub_m.usage()); - std::process::exit(1); - } - }, - #[cfg(not(feature = "portable"))] - ("repair-app-status", _) => { - control::repair_app_status().await?; - } - #[cfg(not(feature = "portable"))] - ("actions", Some(sub_m)) => { - use yajrc::{GenericRpcMethod, RpcResponse}; - - let man = apps::manifest(sub_m.value_of("SERVICE").unwrap()).await?; - let action_id = sub_m.value_of("ACTION").unwrap(); - println!( - "{}", - serde_json::to_string(&RpcResponse::::from_result( - man.actions - .iter() - .filter(|a| &a.id == &action_id) - .next() - .ok_or_else(|| { - failure::format_err!( - "action {} does not exist for {}", - action_id, - man.id - ) - }) - .with_code(error::NOT_FOUND)? - .perform(&man.id) - .await - .map(serde_json::Value::String) - )) - .with_code(error::SERDE_ERROR)? - ) - } - ("pack", Some(sub_m)) => { - pack( - sub_m.value_of("PATH").unwrap(), - sub_m.value_of("output").unwrap(), - ) - .await? - } - ("verify", Some(sub_m)) => verify(sub_m.value_of("PATH").unwrap()).await?, - ("inspect", Some(sub_m)) => match sub_m.subcommand() { - ("info", Some(sub_sub_m)) => { - let path = sub_sub_m.value_of("PATH").unwrap(); - let info = crate::inspect::info_full( - path, - sub_sub_m.is_present("include-manifest") - || sub_sub_m.is_present("only-manifest"), - sub_sub_m.is_present("include-config") || sub_sub_m.is_present("only-config"), - ) - .await?; - if sub_sub_m.is_present("json") { - if sub_sub_m.is_present("pretty") { - if sub_sub_m.is_present("only-manifest") { - println!( - "{}", - serde_json::to_string_pretty(&info.manifest) - .with_code(crate::error::SERDE_ERROR)? - ); - } else if sub_sub_m.is_present("only-config") { - println!( - "{}", - serde_json::to_string_pretty(&info.config) - .with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string_pretty(&info) - .with_code(crate::error::SERDE_ERROR)? - ); - } - } else { - if sub_sub_m.is_present("only-manifest") { - println!( - "{}", - serde_json::to_string(&info.manifest) - .with_code(crate::error::SERDE_ERROR)? - ); - } else if sub_sub_m.is_present("only-config") { - println!( - "{}", - serde_json::to_string(&info.config) - .with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_json::to_string(&info) - .with_code(crate::error::SERDE_ERROR)? - ); - } - } - } else if sub_sub_m.is_present("yaml") { - if sub_sub_m.is_present("only-manifest") { - println!( - "{}", - serde_yaml::to_string(&info.manifest) - .with_code(crate::error::SERDE_ERROR)? - ); - } else if sub_sub_m.is_present("only-config") { - println!( - "{}", - serde_yaml::to_string(&info.config) - .with_code(crate::error::SERDE_ERROR)? - ); - } else { - println!( - "{}", - serde_yaml::to_string(&info).with_code(crate::error::SERDE_ERROR)? - ); - } - } - } - ("instructions", Some(sub_sub_m)) => { - crate::inspect::print_instructions(Path::new(sub_sub_m.value_of("PATH").unwrap())) - .await?; - } - _ => { - println!("{}", sub_m.usage()); - std::process::exit(1); - } - }, - ("index", Some(sub_m)) => { - let idx = crate::index::index(Path::new(sub_m.value_of("DIR").unwrap())).await?; - println!( - "{}", - serde_yaml::to_string(&idx).with_code(crate::error::SERDE_ERROR)? - ); - } - _ => { - app.print_long_help().unwrap(); - std::process::exit(1); - } - } - - Ok(()) -} diff --git a/appmgr/src/manifest.rs b/appmgr/src/manifest.rs deleted file mode 100644 index 8bd107c3a..000000000 --- a/appmgr/src/manifest.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::path::PathBuf; - -use linear_map::LinearMap; - -use crate::actions::Action; -use crate::dependencies::Dependencies; -use crate::tor::HiddenServiceVersion; -use crate::tor::PortMapping; - -pub type ManifestLatest = ManifestV0; - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct Description { - pub short: String, - pub long: String, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -#[serde(tag = "type")] -#[serde(rename_all = "snake_case")] -pub enum ImageConfig { - Tar, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct Asset { - pub src: PathBuf, - pub dst: PathBuf, - pub overwrite: bool, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ManifestV0 { - pub id: String, - pub version: emver::Version, - pub title: String, - pub description: Description, - pub release_notes: String, - #[serde(default)] - pub install_alert: Option, - #[serde(default)] - pub uninstall_alert: Option, - #[serde(default)] - pub restore_alert: Option, - #[serde(default)] - pub start_alert: Option, - #[serde(default)] - pub has_instructions: bool, - #[serde(default = "emver::VersionRange::any")] - pub os_version_required: emver::VersionRange, - #[serde(default = "emver::VersionRange::any")] - pub os_version_recommended: emver::VersionRange, - pub ports: Vec, - pub image: ImageConfig, - #[serde(default)] - pub shm_size_mb: Option, - pub mount: PathBuf, - #[serde(default)] - pub public: Option, - #[serde(default)] - pub shared: Option, - #[serde(default)] - pub assets: Vec, - #[serde(default)] - pub hidden_service_version: HiddenServiceVersion, - #[serde(default)] - pub dependencies: Dependencies, - #[serde(default)] - pub actions: Vec, - #[serde(flatten)] - pub extra: LinearMap, -} - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -#[serde(tag = "compat")] -#[serde(rename_all = "lowercase")] -pub enum Manifest { - V0(ManifestV0), -} -impl Manifest { - pub fn into_latest(self) -> ManifestLatest { - match self { - Manifest::V0(m) => m, - } - } -} diff --git a/appmgr/src/migration.rs b/appmgr/src/migration.rs new file mode 100644 index 000000000..7ead9c702 --- /dev/null +++ b/appmgr/src/migration.rs @@ -0,0 +1,80 @@ +use anyhow::anyhow; +use emver::VersionRange; +use indexmap::{IndexMap, IndexSet}; +use patch_db::HasModel; +use serde::{Deserialize, Serialize}; + +use crate::action::ActionImplementation; +use crate::net::host::Hosts; +use crate::s9pk::manifest::PackageId; +use crate::status::health_check::HealthCheckId; +use crate::util::Version; +use crate::volume::Volumes; +use crate::Error; + +#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +pub struct Migrations { + pub from: IndexMap, + pub to: IndexMap, +} +impl Migrations { + pub async fn from( + &self, + version: &Version, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + hosts: &Hosts, + ) -> Result, Error> { + Ok( + if let Some((_, migration)) = self + .from + .iter() + .find(|(range, _)| version.satisfies(*range)) + { + Some( + migration + .execute(pkg_id, pkg_version, volumes, hosts, Some(version), false) + .await? + .map_err(|e| { + Error::new(anyhow!("{}", e.1), crate::ErrorKind::MigrationFailed) + })?, + ) + } else { + None + }, + ) + } + pub async fn to( + &self, + version: &Version, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + hosts: &Hosts, + ) -> Result, Error> { + Ok( + if let Some((_, migration)) = + self.to.iter().find(|(range, _)| version.satisfies(*range)) + { + Some( + migration + .execute(pkg_id, pkg_version, volumes, hosts, Some(version), false) + .await? + .map_err(|e| { + Error::new(anyhow!("{}", e.1), crate::ErrorKind::MigrationFailed) + })?, + ) + } else { + None + }, + ) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +pub struct MigrationRes { + pub configured: bool, +} diff --git a/appmgr/src/cert-local.csr.conf.template b/appmgr/src/net/cert-local.csr.conf.template similarity index 100% rename from appmgr/src/cert-local.csr.conf.template rename to appmgr/src/net/cert-local.csr.conf.template diff --git a/appmgr/src/net/host.rs b/appmgr/src/net/host.rs new file mode 100644 index 000000000..03431214a --- /dev/null +++ b/appmgr/src/net/host.rs @@ -0,0 +1,24 @@ +use std::ffi::{OsStr, OsString}; +use std::net::Ipv4Addr; + +use indexmap::IndexMap; +use patch_db::DbHandle; +use serde::{Deserialize, Serialize}; + +use crate::s9pk::manifest::PackageId; +use crate::{Error, HOST_IP}; + +pub const TLD: &'static str = "embassy"; + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Hosts(pub IndexMap); +impl Hosts { + pub fn docker_args(&self) -> Vec { + let mut res = Vec::with_capacity(self.0.len() + 1); + res.push(format!("--add-host={}:{}", TLD, Ipv4Addr::from(HOST_IP)).into()); + for (id, ip) in &self.0 { + res.push(format!("--add-host={}.{}:{}", id, TLD, ip).into()); + } + res + } +} diff --git a/appmgr/src/net/mdns.rs b/appmgr/src/net/mdns.rs new file mode 100644 index 000000000..fe233639a --- /dev/null +++ b/appmgr/src/net/mdns.rs @@ -0,0 +1,109 @@ +use avahi_sys; +use futures::future::pending; +use patch_db::{DbHandle, OptionModel}; + +use crate::db::model::{InterfaceAddressesModel, InterfaceInfoModel}; +use crate::util::Apply; +use crate::Error; + +pub async fn enable_lan(mut db: Db) -> Result { + unsafe { + // let app_list = crate::apps::list_info().await?; + + let simple_poll = avahi_sys::avahi_simple_poll_new(); + let poll = avahi_sys::avahi_simple_poll_get(simple_poll); + let mut stack_err = 0; + let err_c: *mut i32 = &mut stack_err; + let avahi_client = avahi_sys::avahi_client_new( + poll, + avahi_sys::AvahiClientFlags::AVAHI_CLIENT_NO_FAIL, + None, + std::ptr::null_mut(), + err_c, + ); + let group = + avahi_sys::avahi_entry_group_new(avahi_client, Some(noop), std::ptr::null_mut()); + let hostname_raw = avahi_sys::avahi_client_get_host_name_fqdn(avahi_client); + let hostname_bytes = std::ffi::CStr::from_ptr(hostname_raw).to_bytes_with_nul(); + const HOSTNAME_LEN: usize = 1 + 15 + 1 + 5; // leading byte, main address, dot, "local" + debug_assert_eq!(hostname_bytes.len(), HOSTNAME_LEN); + let mut hostname_buf = [0; HOSTNAME_LEN + 1]; + hostname_buf[1..].copy_from_slice(hostname_bytes); + // assume fixed length prefix on hostname due to local address + hostname_buf[0] = 15; // set the prefix length to 15 for the main address + hostname_buf[16] = 5; // set the prefix length to 5 for "local" + + for app_id in crate::db::DatabaseModel::new() + .package_data() + .keys(&mut db) + .await? + { + let iface_model = if let Some(model) = crate::db::DatabaseModel::new() + .package_data() + .idx_model(&app_id) + .expect(&mut db) + .await? + .installed() + .map(|i| i.interface_info().addresses()) + .apply(OptionModel::from) + .check(&mut db) + .await? + { + model + } else { + continue; + }; + for iface in iface_model.keys(&mut db).await? { + let lan_address = if let Some(addr) = iface_model + .clone() + .idx_model(&iface) + .expect(&mut db) + .await? + .lan_address() + .get(&mut db) + .await? + .to_owned() + { + addr + } else { + continue; + }; + let lan_address_ptr = std::ffi::CString::new(lan_address) + .expect("Could not cast lan address to c string"); + let _ = avahi_sys::avahi_entry_group_add_record( + group, + avahi_sys::AVAHI_IF_UNSPEC, + avahi_sys::AVAHI_PROTO_UNSPEC, + avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_USE_MULTICAST + | avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_ALLOW_MULTIPLE, + lan_address_ptr.as_ptr(), + avahi_sys::AVAHI_DNS_CLASS_IN as u16, + avahi_sys::AVAHI_DNS_TYPE_CNAME as u16, + avahi_sys::AVAHI_DEFAULT_TTL, + hostname_buf.as_ptr().cast(), + hostname_buf.len(), + ); + log::info!("Published {:?}", lan_address_ptr); + } + } + avahi_sys::avahi_entry_group_commit(group); + Ok(LanHandle(group)) + } +} + +unsafe extern "C" fn noop( + _group: *mut avahi_sys::AvahiEntryGroup, + _state: avahi_sys::AvahiEntryGroupState, + _userdata: *mut core::ffi::c_void, +) { +} + +pub struct LanHandle(*mut avahi_sys::AvahiEntryGroup); +impl Drop for LanHandle { + fn drop(&mut self) { + unsafe { + avahi_sys::avahi_entry_group_reset(self.0); + avahi_sys::avahi_entry_group_free(self.0); + } + } +} diff --git a/appmgr/src/net/mod.rs b/appmgr/src/net/mod.rs new file mode 100644 index 000000000..c9f69524c --- /dev/null +++ b/appmgr/src/net/mod.rs @@ -0,0 +1,62 @@ +use std::net::Ipv4Addr; + +use anyhow::anyhow; +use id_pool::IdPool; +use patch_db::HasModel; +use serde::{Deserialize, Serialize}; + +use self::host::Hosts; +use crate::s9pk::manifest::PackageId; +use crate::{Error, ResultExt}; + +pub mod host; +#[cfg(feature = "avahi")] +pub mod mdns; +pub mod tor; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct IpPool(IdPool); +impl IpPool { + pub fn new() -> Self { + let pool = IdPool::new(); + IpPool(pool) + } + + pub fn get(&mut self) -> Option { + let id = self.0.request_id()?; + let ip = u32::from_be_bytes(crate::HOST_IP) + id as u32; + Some(ip.into()) + } + + pub fn put(&mut self, ip: Ipv4Addr) { + let ip = u32::from_be_bytes(ip.octets()); + let id = ip - u32::from_be_bytes(crate::HOST_IP); + let _ = self.0.return_id(id as u16); + } +} +impl Default for IpPool { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] +pub struct Network { + pub ip_pool: IpPool, + pub hosts: Hosts, +} +impl Network { + pub fn register_host(&mut self, id: &PackageId) -> Result { + if let Some(exists) = self.hosts.0.get(id) { + Ok(*exists) + } else { + let ip = self + .ip_pool + .get() + .ok_or_else(|| anyhow!("No available IP addresses")) + .with_kind(crate::ErrorKind::Network)?; + self.hosts.0.insert(id.clone(), ip); + Ok(ip) + } + } +} diff --git a/appmgr/src/nginx-standard.conf.template b/appmgr/src/net/nginx-standard.conf.template similarity index 100% rename from appmgr/src/nginx-standard.conf.template rename to appmgr/src/net/nginx-standard.conf.template diff --git a/appmgr/src/nginx.conf.template b/appmgr/src/net/nginx.conf.template similarity index 100% rename from appmgr/src/nginx.conf.template rename to appmgr/src/net/nginx.conf.template diff --git a/appmgr/src/tor.rs b/appmgr/src/net/tor.rs similarity index 78% rename from appmgr/src/tor.rs rename to appmgr/src/net/tor.rs index 72cd96987..bed57ea4d 100644 --- a/appmgr/src/tor.rs +++ b/appmgr/src/net/tor.rs @@ -1,14 +1,13 @@ use std::collections::{BTreeSet, HashMap}; use std::net::Ipv4Addr; use std::os::unix::process::ExitStatusExt; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; -use failure::ResultExt as _; -use tokio::io::AsyncReadExt; -use tokio::io::AsyncWriteExt; +use anyhow::anyhow; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use crate::util::{Invoke, PersistencePath, YamlUpdateHandle}; +use crate::util::Invoke; use crate::{Error, ResultExt as _}; #[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] @@ -81,17 +80,17 @@ impl From for usize { } } } -impl std::convert::TryFrom for HiddenServiceVersion { - type Error = failure::Error; - fn try_from(v: usize) -> Result { - Ok(match v { - 1 => HiddenServiceVersion::V1, - 2 => HiddenServiceVersion::V2, - 3 => HiddenServiceVersion::V3, - n => bail!("Invalid HiddenServiceVersion {}", n), - }) - } -} +// impl std::convert::TryFrom for HiddenServiceVersion { +// type Error = anyhow::Error; +// fn try_from(v: usize) -> Result { +// Ok(match v { +// 1 => HiddenServiceVersion::V1, +// 2 => HiddenServiceVersion::V2, +// 3 => HiddenServiceVersion::V3, +// n => bail!("Invalid HiddenServiceVersion {}", n), +// }) +// } +// } impl Default for HiddenServiceVersion { fn default() -> Self { HiddenServiceVersion::V3 @@ -167,26 +166,15 @@ impl ServicesMap { } } -pub async fn services_map(path: &PersistencePath) -> Result { - let f = path.maybe_read(false).await.transpose()?; - if let Some(mut f) = f { - crate::util::from_yaml_async_reader(&mut *f).await - } else { - Ok(Default::default()) - } -} - -pub async fn services_map_mut( - path: PersistencePath, -) -> Result, Error> { - YamlUpdateHandle::new_or_default(path).await -} - pub async fn write_services(hidden_services: &ServicesMap) -> Result<(), Error> { tokio::fs::copy(crate::TOR_RC, ETC_TOR_RC) .await - .with_context(|e| format!("{} -> {}: {}", crate::TOR_RC, ETC_TOR_RC, e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + format!("{} -> {}", crate::TOR_RC, ETC_TOR_RC), + ) + })?; let mut f = tokio::fs::OpenOptions::new() .append(true) .open(ETC_TOR_RC) @@ -233,27 +221,29 @@ pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Err .join("hostname"), ) .await - .with_context(|e| format!("{}/app-{}/hostname: {}", HIDDEN_SERVICE_DIR_ROOT, app_id, e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + format!("{}/app-{}/hostname", HIDDEN_SERVICE_DIR_ROOT, app_id), + ) + })?; let hostname_str = hostname .trim() .strip_suffix(".onion") - .ok_or_else(|| failure::format_err!("invalid tor hostname")) - .no_code()?; + .ok_or_else(|| anyhow!("{}", hostname)) + .with_kind(crate::ErrorKind::InvalidOnionAddress)?; for mapping in &service.ports { match &mapping.lan { Some(LanOptions::Standard) => { log::info!("Writing LAN certificates for {}", app_id); - let base_path = PersistencePath::from_ref("apps").join(&app_id); - let key_path = base_path.join("cert-local.key.pem").path(); - let conf_path = base_path.join("cert-local.csr.conf").path(); - let req_path = base_path.join("cert-local.csr").path(); - let cert_path = base_path.join("cert-local.crt.pem").path(); + let base_path: PathBuf = todo!(); //PersistencePath::from_ref("apps").join(&app_id); + let key_path = base_path.join("cert-local.key.pem"); + let conf_path = base_path.join("cert-local.csr.conf"); + let req_path = base_path.join("cert-local.csr"); + let cert_path = base_path.join("cert-local.crt.pem"); let fullchain_path = base_path.join("cert-local.fullchain.crt.pem"); - if !fullchain_path.exists().await - || tokio::fs::metadata(&key_path).await.is_err() - { - let mut fullchain_file = fullchain_path.write(None).await?; + if !fullchain_path.exists() || tokio::fs::metadata(&key_path).await.is_err() { + let mut fullchain_file = tokio::fs::File::create(&fullchain_path).await?; tokio::process::Command::new("openssl") .arg("ecparam") .arg("-genkey") @@ -262,8 +252,12 @@ pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Err .arg("-noout") .arg("-out") .arg(&key_path) - .invoke("OpenSSL GenKey") - .await?; + .invoke(crate::ErrorKind::OpenSSL) + .await + .map_err(|e| { + let ctx = format!("GenKey: {}", e); + crate::Error::new(e.source.context(ctx), e.kind) + })?; tokio::fs::write( &conf_path, format!( @@ -286,8 +280,12 @@ pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Err )) .arg("-out") .arg(&req_path) - .invoke("OpenSSL Req") - .await?; + .invoke(crate::ErrorKind::OpenSSL) + .await + .map_err(|e| { + let ctx = format!("Req: {}", e); + crate::Error::new(e.source.context(ctx), e.kind) + })?; tokio::process::Command::new("openssl") .arg("ca") .arg("-batch") @@ -307,12 +305,16 @@ pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Err .arg(&req_path) .arg("-out") .arg(&cert_path) - .invoke("OpenSSL CA") - .await?; - log::info!("Writing fullchain to: {}", fullchain_path.path().display()); + .invoke(crate::ErrorKind::OpenSSL) + .await + .map_err(|e| { + let ctx = format!("CA: {}", e); + crate::Error::new(e.source.context(ctx), e.kind) + })?; + log::info!("Writing fullchain to: {}", fullchain_path.display()); tokio::io::copy( &mut tokio::fs::File::open(&cert_path).await?, - &mut *fullchain_file, + &mut fullchain_file, ) .await?; tokio::io::copy( @@ -320,14 +322,13 @@ pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Err "/root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem", ) .await - .with_context(|e| { - format!( - "{}: /root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem", - e + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + "/root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem", ) - }) - .with_code(crate::error::FILESYSTEM_ERROR)?, - &mut *fullchain_file, + })?, + &mut fullchain_file, ) .await?; tokio::io::copy( @@ -335,15 +336,16 @@ pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Err "/root/agent/ca/certs/embassy-root-ca.cert.pem", ) .await - .with_context(|e| { - format!("{}: /root/agent/ca/certs/embassy-root-ca.cert.pem", e) - }) - .with_code(crate::error::FILESYSTEM_ERROR)?, - &mut *fullchain_file, + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + "/root/agent/ca/certs/embassy-root-ca.cert.pem", + ) + })?, + &mut fullchain_file, ) .await?; - fullchain_file.commit().await?; - log::info!("{} written successfully", fullchain_path.path().display()); + log::info!("{} written successfully", fullchain_path.display()); } f.write_all( format!( @@ -402,12 +404,15 @@ pub async fn read_tor_address(name: &str, timeout: Option) -> Result Err(e) - .with_context(|e| format!("{}: {}", addr_path.display(), e)) - .with_code(crate::error::NOT_FOUND), - a => a - .with_context(|e| format!("{}: {}", addr_path.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + Err(e).with_ctx(|_| (crate::ErrorKind::NotFound, addr_path.display().to_string())) + } + a => a.with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + addr_path.display().to_string(), + ) + }), }?; Ok(tor_addr.trim().to_owned()) } @@ -443,18 +448,24 @@ pub async fn read_tor_key( } let tor_key = match version { HiddenServiceVersion::V3 => { - let mut f = tokio::fs::File::open(&addr_path) - .await - .with_context(|e| format!("{}: {}", e, addr_path.display())) - .with_code(crate::error::FILESYSTEM_ERROR)?; + let mut f = tokio::fs::File::open(&addr_path).await.with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + addr_path.display().to_string(), + ) + })?; let mut buf = [0; 96]; f.read_exact(&mut buf).await?; base32::encode(base32::Alphabet::RFC4648 { padding: false }, &buf[32..]).to_lowercase() } _ => tokio::fs::read_to_string(&addr_path) .await - .with_context(|e| format!("{}: {}", e, addr_path.display())) - .with_code(crate::error::FILESYSTEM_ERROR)? + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + addr_path.display().to_string(), + ) + })? .trim_end_matches("\u{0}") .to_string(), }; @@ -471,8 +482,8 @@ pub async fn set_svc( crate::SERVICES_YAML ); let is_listening = !service.ports.is_empty(); - let path = PersistencePath::from_ref(crate::SERVICES_YAML); - let mut hidden_services = services_map_mut(path).await?; + // let path = PersistencePath::from_ref(crate::SERVICES_YAML); + let mut hidden_services: ServicesMap = todo!(); //services_map_mut(path).await?; let ver = service.hidden_service_version; let ip = hidden_services.add(name.to_owned(), service); log::info!("Adding Tor hidden service {} to {}.", name, ETC_TOR_RC); @@ -495,7 +506,7 @@ pub async fn set_svc( .status()?; crate::ensure_code!( svc_exit.success(), - crate::error::GENERAL_ERROR, + crate::ErrorKind::Tor, "Failed to Reload Tor: {}", svc_exit .code() @@ -519,14 +530,13 @@ pub async fn set_svc( .status()?; crate::ensure_code!( svc_exit.success(), - crate::error::GENERAL_ERROR, + crate::ErrorKind::Nginx, "Failed to Reload Nginx: {}", svc_exit .code() .or_else(|| { svc_exit.signal().map(|a| 128 + a) }) .unwrap_or(0) ); - hidden_services.commit().await?; Ok((ip, addr, key)) } @@ -536,8 +546,8 @@ pub async fn rm_svc(name: &str) -> Result<(), Error> { name, crate::SERVICES_YAML ); - let path = PersistencePath::from_ref(crate::SERVICES_YAML); - let mut hidden_services = services_map_mut(path).await?; + // let path = PersistencePath::from_ref(crate::SERVICES_YAML); + let mut hidden_services: ServicesMap = todo!(); //services_map_mut(path).await?; hidden_services.remove(name); let hidden_service_path = Path::new(HIDDEN_SERVICE_DIR_ROOT).join(format!("app-{}", name)); log::info!("Removing {}", hidden_service_path.display()); @@ -552,7 +562,7 @@ pub async fn rm_svc(name: &str) -> Result<(), Error> { .status()?; crate::ensure_code!( svc_exit.success(), - crate::error::GENERAL_ERROR, + crate::ErrorKind::Tor, "Failed to Reload Tor: {}", svc_exit.code().unwrap_or(0) ); @@ -563,14 +573,13 @@ pub async fn rm_svc(name: &str) -> Result<(), Error> { .status()?; crate::ensure_code!( svc_exit.success(), - crate::error::GENERAL_ERROR, + crate::ErrorKind::Nginx, "Failed to Reload Nginx: {}", svc_exit .code() .or_else(|| { svc_exit.signal().map(|a| 128 + a) }) .unwrap_or(0) ); - hidden_services.commit().await?; Ok(()) } @@ -583,8 +592,12 @@ pub async fn change_key( if hidden_service_path.exists() { tokio::fs::remove_dir_all(&hidden_service_path) .await - .with_context(|e| format!("{}: {}", hidden_service_path.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + hidden_service_path.display().to_string(), + ) + })?; } if let Some(key) = key { tokio::fs::create_dir_all(&hidden_service_path).await?; @@ -593,8 +606,7 @@ pub async fn change_key( key_data.extend_from_slice(&key.to_bytes()); tokio::fs::write(&key_path, key_data) .await - .with_context(|e| format!("{}: {}", key_path.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; + .with_ctx(|_| (crate::ErrorKind::Filesystem, key_path.display().to_string()))?; } log::info!("Reloading Tor."); let svc_exit = std::process::Command::new("service") @@ -602,22 +614,22 @@ pub async fn change_key( .status()?; crate::ensure_code!( svc_exit.success(), - crate::error::GENERAL_ERROR, + crate::ErrorKind::Tor, "Failed to Reload Tor: {}", svc_exit.code().unwrap_or(0) ); - let mut info = crate::apps::list_info_mut().await?; - if let Some(mut i) = info.get_mut(name) { - if i.tor_address.is_some() { - i.tor_address = Some(read_tor_address(name, Some(Duration::from_secs(30))).await?); - } - } + // let mut info = crate::apps::list_info_mut().await?; + // if let Some(mut i) = info.get_mut(name) { + // if i.tor_address.is_some() { + // i.tor_address = Some(read_tor_address(name, Some(Duration::from_secs(30))).await?); + // } + // } Ok(()) } pub async fn reload() -> Result<(), Error> { - let path = PersistencePath::from_ref(crate::SERVICES_YAML); - let hidden_services = services_map(&path).await?; + // let path = PersistencePath::from_ref(crate::SERVICES_YAML); + let hidden_services = todo!(); //services_map(&path).await?; log::info!("Syncing Tor hidden services to {}.", ETC_TOR_RC); write_services(&hidden_services).await?; log::info!("Reloading Tor."); @@ -626,7 +638,7 @@ pub async fn reload() -> Result<(), Error> { .status()?; crate::ensure_code!( svc_exit.success(), - crate::error::GENERAL_ERROR, + crate::ErrorKind::Tor, "Failed to Reload Tor: {}", svc_exit.code().unwrap_or(0) ); @@ -634,8 +646,8 @@ pub async fn reload() -> Result<(), Error> { } pub async fn restart() -> Result<(), Error> { - let path = PersistencePath::from_ref(crate::SERVICES_YAML); - let hidden_services = services_map(&path).await?; + // let path = PersistencePath::from_ref(crate::SERVICES_YAML); + let hidden_services = todo!(); //services_map(&path).await?; log::info!("Syncing Tor hidden services to {}.", ETC_TOR_RC); write_services(&hidden_services).await?; log::info!("Restarting Tor."); @@ -644,7 +656,7 @@ pub async fn restart() -> Result<(), Error> { .status()?; crate::ensure_code!( svc_exit.success(), - crate::error::GENERAL_ERROR, + crate::ErrorKind::Tor, "Failed to Restart Tor: {}", svc_exit.code().unwrap_or(0) ); diff --git a/appmgr/src/pack.rs b/appmgr/src/pack.rs deleted file mode 100644 index 6c347e5cd..000000000 --- a/appmgr/src/pack.rs +++ /dev/null @@ -1,388 +0,0 @@ -use std::borrow::Cow; -use std::path::{Path, PathBuf}; - -use failure::ResultExt; -use futures::stream::StreamExt; -use linear_map::LinearMap; -use rand::SeedableRng; -use tokio_tar as tar; - -use crate::config::{ConfigRuleEntry, ConfigSpec}; -use crate::manifest::{ImageConfig, Manifest}; -use crate::util::{from_cbor_async_reader, from_json_async_reader, from_yaml_async_reader}; -use crate::version::VersionT; - -#[derive(Clone, Debug, Fail)] -pub enum Error { - #[fail(display = "Invalid Directory Name: {}", _0)] - InvalidDirectoryName(String), - #[fail(display = "Invalid File Name: {}", _0)] - InvalidFileName(String), - #[fail(display = "Invalid Output Path: {}", _0)] - InvalidOutputPath(String), -} - -pub async fn pack(path: &str, output: &str) -> Result<(), failure::Error> { - let path = Path::new(path.trim_end_matches("/")); - let output = Path::new(output); - log::info!( - "Starting pack of {} to {}.", - path.file_name() - .and_then(|a| a.to_str()) - .ok_or_else(|| Error::InvalidDirectoryName(format!("{}", path.display())))?, - output.display(), - ); - let out_file = tokio::fs::File::create(output).await?; - let mut out = tar::Builder::new(out_file); - log::info!("Reading {}/manifest.yaml.", path.display()); - let manifest: Manifest = crate::util::from_yaml_async_reader( - tokio::fs::File::open(path.join("manifest.yaml")) - .await - .with_context(|e| format!("{}: manifest.yaml", e))?, - ) - .await?; - log::info!("Writing manifest to archive."); - let bin_manifest = serde_cbor::to_vec(&manifest)?; - let mut manifest_header = tar::Header::new_gnu(); - manifest_header.set_size(bin_manifest.len() as u64); - out.append_data( - &mut manifest_header, - "manifest.cbor", - std::io::Cursor::new(bin_manifest), - ) - .await?; - let manifest = manifest.into_latest(); - ensure!( - crate::version::Current::new() - .semver() - .satisfies(&manifest.os_version_required), - "Unsupported AppMgr version: expected {}", - manifest.os_version_required - ); - log::info!("Reading {}/config_spec.yaml.", path.display()); - let config_spec: ConfigSpec = from_yaml_async_reader( - tokio::fs::File::open(path.join("config_spec.yaml")) - .await - .with_context(|e| format!("{}: config_spec.yaml", e))?, - ) - .await?; - log::info!("Writing config spec to archive."); - let bin_config_spec = serde_cbor::to_vec(&config_spec)?; - let mut config_spec_header = tar::Header::new_gnu(); - config_spec_header.set_size(bin_config_spec.len() as u64); - out.append_data( - &mut config_spec_header, - "config_spec.cbor", - std::io::Cursor::new(bin_config_spec), - ) - .await?; - log::info!("Reading {}/config_rules.yaml.", path.display()); - let config_rules: Vec = from_yaml_async_reader( - tokio::fs::File::open(path.join("config_rules.yaml")) - .await - .with_context(|e| format!("{}: config_rules.yaml", e))?, - ) - .await?; - log::info!("Writing config rules to archive."); - let bin_config_rules = serde_cbor::to_vec(&config_rules)?; - let mut config_rules_header = tar::Header::new_gnu(); - config_rules_header.set_size(bin_config_rules.len() as u64); - out.append_data( - &mut config_rules_header, - "config_rules.cbor", - std::io::Cursor::new(bin_config_rules), - ) - .await?; - if manifest.has_instructions { - log::info!("Packing instructions.md"); - out.append_path_with_name(path.join("instructions.md"), "instructions.md") - .await?; - } - log::info!("Copying over assets."); - for asset in &manifest.assets { - let src_path = Path::new("assets").join(&asset.src); - log::info!("Reading {}/{}.", path.display(), src_path.display()); - let file_path = path.join(&src_path); - let src = tokio::fs::File::open(&file_path) - .await - .with_context(|e| format!("{}: {}", e, src_path.display()))?; - log::info!("Writing {} to archive.", src_path.display()); - if src.metadata().await?.is_dir() { - out.append_dir_all(&asset.src, &file_path).await?; - let mut h = tar::Header::new_gnu(); - h.set_size(0); - h.set_path(format!("APPMGR_DIR_END:{}", asset.src.display()))?; - h.set_cksum(); - out.append(&h, tokio::io::empty()).await?; - } else { - out.append_path_with_name(&file_path, &asset.src).await?; - } - } - match manifest.image { - ImageConfig::Tar => { - log::info!("Reading {}/image.tar.", path.display()); - let image = tokio::fs::File::open(path.join("image.tar")) - .await - .with_context(|e| format!("{}: image.tar", e))?; - log::info!("Writing image.tar to archive."); - let mut header = tar::Header::new_gnu(); - header.set_size(image.metadata().await?.len()); - out.append_data(&mut header, "image.tar", image).await?; - } - } - out.into_inner().await?; - - Ok(()) -} - -pub fn validate_path>(p: P) -> Result<(), Error> { - let path = p.as_ref(); - if path.is_absolute() { - return Err(Error::InvalidFileName(format!("{}", path.display()))); - } - for seg in path { - if seg == ".." { - return Err(Error::InvalidFileName(format!("{}", path.display()))); - } - } - Ok(()) -} - -pub async fn verify(path: &str) -> Result<(), failure::Error> { - let path = Path::new(path.trim_end_matches("/")); - ensure!( - path.extension() - .and_then(|a| a.to_str()) - .ok_or_else(|| Error::InvalidFileName(format!("{}", path.display())))? - == "s9pk", - "Extension Must Be '.s9pk'" - ); - let name = path - .file_stem() - .and_then(|a| a.to_str()) - .ok_or_else(|| Error::InvalidFileName(format!("{}", path.display())))?; - ensure!( - !name.starts_with("start9") - && name - .chars() - .filter(|c| !c.is_alphanumeric() && c != &'-') - .next() - .is_none(), - "Invalid Application ID" - ); - log::info!( - "Starting verification of {}.", - path.file_name() - .and_then(|a| a.to_str()) - .ok_or_else(|| Error::InvalidFileName(format!("{}", path.display())))?, - ); - {} - log::info!("Opening file."); - let r = tokio::fs::File::open(&path) - .await - .with_context(|e| format!("{}: {}", path.display(), e))?; - log::info!("Extracting archive."); - let mut pkg = tar::Archive::new(r); - let mut entries = pkg.entries()?; - log::info!("Opening manifest from archive."); - let manifest = entries - .next() - .await - .ok_or_else(|| format_err!("missing manifest"))??; - ensure!( - manifest.path()?.to_str() == Some("manifest.cbor"), - "Package File Invalid or Corrupted: expected manifest.cbor, got {}", - manifest.path()?.display() - ); - log::trace!("Deserializing manifest."); - let manifest: Manifest = from_cbor_async_reader(manifest).await?; - let manifest = manifest.into_latest(); - ensure!( - crate::version::Current::new() - .semver() - .satisfies(&manifest.os_version_required), - "Unsupported AppMgr Version: expected {}", - manifest.os_version_required - ); - ensure!(manifest.id == name, "Package Name Does Not Match Expected",); - if let (Some(public), Some(shared)) = (&manifest.public, &manifest.shared) { - ensure!( - !public.starts_with(shared) && !shared.starts_with(public), - "Public Directory Conflicts With Shared Directory" - ) - } - if let Some(public) = &manifest.public { - validate_path(public)?; - } - if let Some(shared) = &manifest.shared { - validate_path(shared)?; - } - for action in &manifest.actions { - ensure!( - !action.command.is_empty(), - "Command Cannot Be Empty: {}", - action.id - ); - } - log::info!("Opening config spec from archive."); - let config_spec = entries - .next() - .await - .ok_or_else(|| format_err!("missing config spec"))??; - ensure!( - config_spec.path()?.to_str() == Some("config_spec.cbor"), - "Package File Invalid or Corrupted: expected config_rules.cbor, got {}", - config_spec.path()?.display() - ); - log::trace!("Deserializing config spec."); - let config_spec: ConfigSpec = from_cbor_async_reader(config_spec).await?; - log::trace!("Validating config spec."); - config_spec.validate(&manifest)?; - let config = config_spec.gen(&mut rand::rngs::StdRng::from_entropy(), &None)?; - config_spec.matches(&config)?; - log::info!("Opening config rules from archive."); - let config_rules = entries - .next() - .await - .ok_or_else(|| format_err!("missing config rules"))??; - ensure!( - config_rules.path()?.to_str() == Some("config_rules.cbor"), - "Package File Invalid or Corrupted: expected config_rules.cbor, got {}", - config_rules.path()?.display() - ); - log::trace!("Deserializing config rules."); - let config_rules: Vec = from_cbor_async_reader(config_rules).await?; - log::trace!("Validating config rules against config spec."); - let mut cfgs = LinearMap::new(); - cfgs.insert(name, Cow::Borrowed(&config)); - for rule in &config_rules { - rule.check(&config, &cfgs) - .with_context(|e| format!("Default Config does not satisfy: {}", e))?; - } - if manifest.has_instructions { - let instructions = entries - .next() - .await - .ok_or_else(|| format_err!("missing instructions"))??; - ensure!( - instructions.path()?.to_str() == Some("instructions.md"), - "Package File Invalid or Corrupted: expected instructions.md, got {}", - instructions.path()?.display() - ); - } - for asset_info in manifest.assets { - validate_path(&asset_info.src)?; - validate_path(&asset_info.dst)?; - let asset = entries - .next() - .await - .ok_or_else(|| format_err!("missing asset: {}", asset_info.src.display()))??; - if asset.header().entry_type().is_file() { - ensure!( - asset.path()?.to_str() == Some(&format!("{}", asset_info.src.display())), - "Package File Invalid or Corrupted: expected {}, got {}", - asset_info.src.display(), - asset.path()?.display() - ); - } else if asset.header().entry_type().is_dir() { - ensure!( - asset.path()?.to_str() == Some(&format!("{}/", asset_info.src.display())), - "Package File Invalid or Corrupted: expected {}, got {}", - asset_info.src.display(), - asset.path()?.display() - ); - loop { - let file = entries.next().await.ok_or_else(|| { - format_err!( - "missing directory end marker: APPMGR_DIR_END:{}", - asset_info.src.display() - ) - })??; - if file - .path()? - .starts_with(format!("APPMGR_DIR_END:{}", asset_info.src.display())) - { - break; - } else { - ensure!( - file.path()? - .to_str() - .map(|p| p.starts_with(&format!("{}/", asset_info.src.display()))) - .unwrap_or(false), - "Package File Invalid or Corrupted: expected {}, got {}", - asset_info.src.display(), - asset.path()?.display() - ); - } - } - } else { - bail!("Asset Not Regular File: {}", asset_info.src.display()); - } - } - match &manifest.image { - ImageConfig::Tar => { - #[derive(Clone, Debug, serde::Deserialize)] - #[serde(rename_all = "PascalCase")] - struct DockerManifest { - config: PathBuf, - repo_tags: Vec, - layers: Vec, - } - let image_name = format!("start9/{}", manifest.id); - log::debug!("Opening image.tar from archive."); - let image = entries - .next() - .await - .ok_or_else(|| format_err!("missing image.tar"))??; - let image_path = image.path()?; - if image_path != Path::new("image.tar") { - return Err(format_err!( - "Package File Invalid or Corrupted: expected image.tar, got {}", - image_path.display() - )); - } - log::info!("Verifying image.tar."); - let mut image_tar = tar::Archive::new(image); - let image_manifest = image_tar - .entries()? - .map(|e| { - let e = e?; - Ok((e.path()?.to_path_buf(), e)) - }) - .filter_map(|res: Result<(PathBuf, tar::Entry<_>), std::io::Error>| { - futures::future::ready(match res { - Ok((path, e)) => { - if path == Path::new("manifest.json") { - Some(Ok(e)) - } else { - None - } - } - Err(e) => Some(Err(e)), - }) - }) - .next() - .await - .ok_or_else(|| format_err!("image.tar is missing manifest.json"))??; - let image_manifest: Vec = - from_json_async_reader(image_manifest).await?; - image_manifest - .into_iter() - .flat_map(|a| a.repo_tags) - .map(|t| { - if t.starts_with("start9/") { - if t.split(":").next().unwrap() != image_name { - Err(format_err!("Contains prohibited image tag: {}", t)) - } else { - Ok(()) - } - } else { - Ok(()) - } - }) - .collect::>()?; - } - }; - - Ok(()) -} diff --git a/appmgr/src/registry.rs b/appmgr/src/registry.rs index e7fda973f..03261c0fe 100644 --- a/appmgr/src/registry.rs +++ b/appmgr/src/registry.rs @@ -1,13 +1,11 @@ use emver::VersionRange; use tokio_compat_02::FutureExt; -use crate::apps::AppConfig; -use crate::manifest::ManifestLatest; -use crate::Error; -use crate::ResultExt as _; +use crate::s9pk::manifest::Manifest; +use crate::{Error, ResultExt as _}; -pub async fn manifest(id: &str, version: &VersionRange) -> Result { - let manifest: ManifestLatest = reqwest::get(&format!( +pub async fn manifest(id: &str, version: &VersionRange) -> Result { + let manifest: Manifest = reqwest::get(&format!( "{}/manifest/{}?spec={}", &*crate::APP_REGISTRY_URL, id, @@ -15,12 +13,12 @@ pub async fn manifest(id: &str, version: &VersionRange) -> Result Result Result { - let config: crate::inspect::AppConfig = reqwest::get(&format!( - "{}/config/{}?spec={}", - &*crate::APP_REGISTRY_URL, - id, - version - )) - .compat() - .await - .with_code(crate::error::NETWORK_ERROR)? - .error_for_status() - .with_code(crate::error::REGISTRY_ERROR)? - .json() - .await - .with_code(crate::error::SERDE_ERROR)?; - Ok(AppConfig { - config: None, - spec: config.spec, - rules: config.rules, - }) -} diff --git a/appmgr/src/remove.rs b/appmgr/src/remove.rs deleted file mode 100644 index 416ce9f66..000000000 --- a/appmgr/src/remove.rs +++ /dev/null @@ -1,150 +0,0 @@ -use crate::failure::ResultExt; -use std::path::Path; - -use linear_map::LinearMap; - -use crate::dependencies::{DependencyError, TaggedDependencyError}; -use crate::Error; -use crate::ResultExt as _; - -pub async fn remove( - name: &str, - purge: bool, - dry_run: bool, -) -> Result, Error> { - let manifest = crate::apps::manifest(name).await?; - let mut res = LinearMap::new(); - crate::stop_dependents(name, dry_run, DependencyError::NotInstalled, &mut res).await?; - if dry_run { - return Ok(res); - } - let image_name = format!("start9/{}", name); - log::info!("Removing app from manifest."); - crate::apps::remove(name).await?; - log::info!("Stopping docker container."); - let res = crate::control::stop_app(name, false, false) - .await - .unwrap_or_else(|e| { - log::error!("Error stopping app: {}", e); - LinearMap::new() - }); - log::info!("Removing docker container."); - if !std::process::Command::new("docker") - .args(&["rm", name]) - .stdout(std::process::Stdio::null()) - .stderr(match log::max_level() { - log::LevelFilter::Error => std::process::Stdio::null(), - _ => std::process::Stdio::inherit(), - }) - .status()? - .success() - { - log::error!("Failed to Remove Docker Container"); - }; - if !std::process::Command::new("docker") - .args(&["rmi", &image_name]) - .stdout(std::process::Stdio::null()) - .stderr(match log::max_level() { - log::LevelFilter::Error => std::process::Stdio::null(), - _ => std::process::Stdio::inherit(), - }) - .status()? - .success() - { - log::error!("Failed to Remove Docker Image"); - }; - if purge { - log::info!("Removing tor hidden service."); - crate::tor::rm_svc(name).await?; - log::info!("Removing app metadata."); - let metadata_path = Path::new(crate::PERSISTENCE_DIR).join("apps").join(name); - tokio::fs::remove_dir_all(&metadata_path) - .await - .with_context(|e| format!("rm {}: {}", metadata_path.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - log::info!("Unbinding shared filesystem."); - let installed_apps = crate::apps::list_info().await?; - for (dep, _) in manifest.dependencies.0.iter() { - let path = Path::new(crate::VOLUMES) - .join(name) - .join("start9") - .join("public") - .join(&dep); - if path.exists() { - crate::disks::unmount(&path).await?; - } else { - log::warn!("{} does not exist, skipping...", path.display()); - } - let path = Path::new(crate::VOLUMES) - .join(name) - .join("start9") - .join("shared") - .join(&dep); - if path.exists() { - crate::disks::unmount(&path).await?; - } else { - log::warn!("{} does not exist, skipping...", path.display()); - } - if installed_apps.contains_key(dep) { - let dep_man = crate::apps::manifest(dep).await?; - if let Some(shared) = dep_man.shared { - let path = Path::new(crate::VOLUMES).join(dep).join(&shared).join(name); - if path.exists() { - tokio::fs::remove_dir_all(&path) - .await - .with_context(|e| format!("rm {}: {}", path.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - } - } - } else { - log::warn!("{} is not installed, skipping...", dep); - } - } - if manifest.public.is_some() || manifest.shared.is_some() { - for dependent in crate::apps::dependents(name, false).await? { - let path = Path::new(crate::VOLUMES) - .join(&dependent) - .join("start9") - .join("public") - .join(name); - if path.exists() { - crate::disks::unmount(&path).await?; - } else { - log::warn!("{} does not exist, skipping...", path.display()); - } - let path = Path::new(crate::VOLUMES) - .join(dependent) - .join("start9") - .join("shared") - .join(name); - if path.exists() { - crate::disks::unmount(&path).await?; - } else { - log::warn!("{} does not exist, skipping...", path.display()); - } - } - } - log::info!("Destroying mounted volume."); - let volume_path = Path::new(crate::VOLUMES).join(name); - tokio::fs::remove_dir_all(&volume_path) - .await - .with_context(|e| format!("rm {}: {}", volume_path.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - log::info!("Pruning unused docker images."); - crate::ensure_code!( - std::process::Command::new("docker") - .args(&["image", "prune", "-a", "-f"]) - .stdout(std::process::Stdio::null()) - .stderr(match log::max_level() { - log::LevelFilter::Error => std::process::Stdio::null(), - _ => std::process::Stdio::inherit(), - }) - .status()? - .success(), - crate::error::DOCKER_ERROR, - "Failed to Prune Docker Images" - ); - }; - - Ok(res) -} diff --git a/appmgr/src/s9pk/builder.rs b/appmgr/src/s9pk/builder.rs new file mode 100644 index 000000000..f8a3ce1f1 --- /dev/null +++ b/appmgr/src/s9pk/builder.rs @@ -0,0 +1,106 @@ +use std::io::{Read, Seek, SeekFrom, Write}; + +use typed_builder::TypedBuilder; + +use super::header::{FileSection, Header}; +use super::manifest::Manifest; +use crate::{Error, ResultExt}; + +#[derive(TypedBuilder)] +pub struct S9pkPacker< + 'a, + W: Write + Seek, + RLicense: Read, + RInstructions: Read, + RIcon: Read, + RDockerImages: Read, +> { + writer: W, + manifest: &'a Manifest, + license: RLicense, + instructions: RInstructions, + icon: RIcon, + docker_images: RDockerImages, +} +impl< + 'a, + W: Write + Seek, + RLicense: Read, + RInstructions: Read, + RIcon: Read, + RDockerImages: Read, + > S9pkPacker<'a, W, RLicense, RInstructions, RIcon, RDockerImages> +{ + /// BLOCKING + pub fn pack(mut self) -> Result<(), Error> { + let header_pos = self.writer.stream_position()?; + if header_pos != 0 { + log::warn!("Appending to non-empty file."); + } + let mut header = Header::placeholder(); + header.serialize(&mut self.writer).with_ctx(|_| { + ( + crate::ErrorKind::Serialization, + "Writing Placeholder Header", + ) + })?; + let mut position = self.writer.stream_position()?; + // manifest + serde_cbor::to_writer(&mut self.writer, self.manifest).with_ctx(|_| { + ( + crate::ErrorKind::Serialization, + "Serializing Manifest (CBOR)", + ) + })?; + let new_pos = self.writer.stream_position()?; + header.table_of_contents.manifest = FileSection { + position, + length: new_pos - position, + }; + position = new_pos; + // license + std::io::copy(&mut self.license, &mut self.writer) + .with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying License"))?; + let new_pos = self.writer.stream_position()?; + header.table_of_contents.license = FileSection { + position, + length: new_pos - position, + }; + position = new_pos; + // instructions + std::io::copy(&mut self.instructions, &mut self.writer) + .with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying Instructions"))?; + let new_pos = self.writer.stream_position()?; + header.table_of_contents.instructions = FileSection { + position, + length: new_pos - position, + }; + position = new_pos; + // icon + std::io::copy(&mut self.icon, &mut self.writer) + .with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying Icon"))?; + let new_pos = self.writer.stream_position()?; + header.table_of_contents.icon = FileSection { + position, + length: new_pos - position, + }; + position = new_pos; + // docker_images + std::io::copy(&mut self.docker_images, &mut self.writer) + .with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying App Image"))?; + let new_pos = self.writer.stream_position()?; + header.table_of_contents.docker_images = FileSection { + position, + length: new_pos - position, + }; + position = new_pos; + // header + self.writer.seek(SeekFrom::Start(header_pos))?; + header + .serialize(&mut self.writer) + .with_ctx(|_| (crate::ErrorKind::Serialization, "Writing Header"))?; + self.writer.seek(SeekFrom::Start(position))?; + + Ok(()) + } +} diff --git a/appmgr/src/s9pk/header.rs b/appmgr/src/s9pk/header.rs new file mode 100644 index 000000000..49376174e --- /dev/null +++ b/appmgr/src/s9pk/header.rs @@ -0,0 +1,169 @@ +use std::collections::HashMap; +use std::io::Write; + +use anyhow::anyhow; +use ed25519_dalek::{PublicKey, Signature}; +use tokio::io::{AsyncRead, AsyncReadExt}; + +use crate::Error; + +pub const MAGIC: [u8; 2] = [59, 59]; +pub const VERSION: u8 = 1; + +pub struct Header { + pub pubkey: PublicKey, + pub signature: Signature, + pub table_of_contents: TableOfContents, +} +impl Header { + pub fn placeholder() -> Self { + Header { + pubkey: PublicKey::default(), + signature: Signature::new([0; 64]), + table_of_contents: Default::default(), + } + } + // MUST BE SAME SIZE REGARDLESS OF DATA + pub fn serialize(&self, mut writer: W) -> std::io::Result<()> { + writer.write_all(&MAGIC)?; + writer.write_all(&[VERSION])?; + writer.write_all(self.pubkey.as_bytes())?; + writer.write_all(self.signature.as_ref())?; + self.table_of_contents.serialize(writer)?; + Ok(()) + } + pub async fn deserialize(mut reader: R) -> Result { + let mut magic = [0; 2]; + reader.read_exact(&mut magic).await?; + if magic != MAGIC { + return Err(Error::new( + anyhow!("Incorrect Magic"), + crate::ErrorKind::ParseS9pk, + )); + } + let mut version = [0]; + reader.read_exact(&mut version).await?; + if version[0] != VERSION { + return Err(Error::new( + anyhow!("Unknown Version"), + crate::ErrorKind::ParseS9pk, + )); + } + let mut pubkey_bytes = [0; 32]; + reader.read_exact(&mut pubkey_bytes).await?; + let pubkey = PublicKey::from_bytes(&pubkey_bytes) + .map_err(|e| Error::new(e, crate::ErrorKind::ParseS9pk))?; + let mut sig_bytes = [0; 64]; + reader.read_exact(&mut sig_bytes).await?; + let signature = Signature::new(sig_bytes); + let table_of_contents = TableOfContents::deserialize(reader).await?; + + Ok(Header { + pubkey, + signature, + table_of_contents, + }) + } +} + +#[derive(Debug, Default)] +pub struct TableOfContents { + pub manifest: FileSection, + pub license: FileSection, + pub instructions: FileSection, + pub icon: FileSection, + pub docker_images: FileSection, +} +impl TableOfContents { + pub fn serialize(&self, mut writer: W) -> std::io::Result<()> { + let len: u32 = 16 // size of FileSection + * ( + 1 + // manifest + 1 + // license + 1 + // instructions + 1 + // icon + 1 // docker_images + ); + writer.write_all(&u32::to_be_bytes(len))?; + self.manifest.serialize_entry("manifest", &mut writer)?; + self.license.serialize_entry("license", &mut writer)?; + self.instructions + .serialize_entry("instructions", &mut writer)?; + self.icon.serialize_entry("icon", &mut writer)?; + self.docker_images + .serialize_entry("docker_images", &mut writer)?; + Ok(()) + } + pub async fn deserialize(mut reader: R) -> std::io::Result { + let mut toc_len = [0; 4]; + reader.read_exact(&mut toc_len).await?; + let toc_len = u32::from_be_bytes(toc_len); + let mut reader = reader.take(toc_len as u64); + let mut table = HashMap::new(); + while let Some((label, section)) = FileSection::deserialize_entry(&mut reader).await? { + table.insert(label, section); + } + fn from_table( + table: &HashMap, FileSection>, + label: &str, + ) -> std::io::Result { + table.get(label.as_bytes()).copied().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + format!("Missing Required Label: {}", label), + ) + }) + } + fn as_opt(fs: FileSection) -> Option { + if fs.position | fs.length == 0 { + // 0/0 is not a valid file section + None + } else { + Some(fs) + } + } + Ok(TableOfContents { + manifest: from_table(&table, "manifest")?, + license: from_table(&table, "license")?, + instructions: from_table(&table, "instructions")?, + icon: from_table(&table, "icon")?, + docker_images: from_table(&table, "docker_images")?, + }) + } +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct FileSection { + pub position: u64, + pub length: u64, +} +impl FileSection { + pub fn serialize_entry(self, label: &str, mut writer: W) -> std::io::Result<()> { + writer.write_all(&[label.len() as u8])?; + writer.write_all(label.as_bytes())?; + writer.write_all(&u64::to_be_bytes(self.position))?; + writer.write_all(&u64::to_be_bytes(self.length))?; + Ok(()) + } + pub async fn deserialize_entry( + mut reader: R, + ) -> std::io::Result, Self)>> { + let mut label_len = [0]; + let read = reader.read(&mut label_len).await?; + if read == 0 { + return Ok(None); + } + let label = vec![0; label_len[0] as usize]; + let mut pos = [0; 8]; + reader.read_exact(&mut pos).await?; + let mut len = [0; 8]; + reader.read_exact(&mut len).await?; + Ok(Some(( + label, + FileSection { + position: u64::from_be_bytes(pos), + length: u64::from_be_bytes(len), + }, + ))) + } +} diff --git a/appmgr/src/s9pk/manifest.rs b/appmgr/src/s9pk/manifest.rs new file mode 100644 index 000000000..028f345a2 --- /dev/null +++ b/appmgr/src/s9pk/manifest.rs @@ -0,0 +1,222 @@ +use std::borrow::Borrow; +use std::net::Ipv4Addr; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use chrono::{DateTime, Utc}; +use indexmap::IndexMap; +use patch_db::HasModel; +use serde::{Deserialize, Serialize, Serializer}; +use url::Url; + +use crate::action::{ActionImplementation, Actions}; +use crate::backup::BackupActions; +use crate::config::action::ConfigActions; +use crate::db::model::InterfaceInfo; +use crate::dependencies::Dependencies; +use crate::id::{Id, InterfaceId, InvalidId, SYSTEM_ID}; +use crate::migration::Migrations; +use crate::net::host::Hosts; +use crate::net::tor::HiddenServiceVersion; +use crate::status::health_check::{HealthCheckResult, HealthChecks}; +use crate::util::Version; +use crate::volume::Volumes; +use crate::Error; + +pub const SYSTEM_PACKAGE_ID: PackageId<&'static str> = PackageId(SYSTEM_ID); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PackageId = String>(Id); +impl FromStr for PackageId { + type Err = InvalidId; + fn from_str(s: &str) -> Result { + Ok(PackageId(Id::try_from(s.to_owned())?)) + } +} +impl> AsRef> for PackageId { + fn as_ref(&self) -> &PackageId { + self + } +} +impl> std::fmt::Display for PackageId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } +} +impl> AsRef for PackageId { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} +impl> Borrow for PackageId { + fn borrow(&self) -> &str { + self.0.as_ref() + } +} +impl> AsRef for PackageId { + fn as_ref(&self) -> &Path { + self.0.as_ref().as_ref() + } +} +impl<'de, S> Deserialize<'de> for PackageId +where + S: AsRef, + Id: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + Ok(PackageId(Deserialize::deserialize(deserializer)?)) + } +} +impl Serialize for PackageId +where + S: AsRef, +{ + fn serialize(&self, serializer: Ser) -> Result + where + Ser: Serializer, + { + Serialize::serialize(&self.0, serializer) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +pub struct Manifest { + pub id: PackageId, + pub title: String, + pub version: Version, + pub description: Description, + #[serde(default)] + pub assets: Assets, + #[serde(default)] + pub build: Option>, + pub release_notes: String, + pub license: String, // type of license + pub wrapper_repo: Url, + pub upstream_repo: Url, + pub support_site: Option, + pub marketing_site: Option, + #[serde(default)] + pub alerts: Alerts, + #[model] + pub main: ActionImplementation, + pub health_checks: HealthChecks, + #[model] + pub config: Option, + #[model] + pub volumes: Volumes, + // #[serde(default = "current_version")] + pub min_os_version: Version, + // #[serde(default)] + pub interfaces: Interfaces, + // #[serde(default)] + #[model] + pub backup: BackupActions, + #[serde(default)] + #[model] + pub migrations: Migrations, + #[serde(default)] + pub actions: Actions, + // #[serde(default)] + // pub permissions: Permissions, + #[serde(default)] + #[model] + pub dependencies: Dependencies, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Interfaces(IndexMap); // TODO +impl Interfaces { + pub async fn install(&self, ip: Ipv4Addr) -> Result { + todo!() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Interface { + tor_config: Option, + lan_config: Option>, + ui: bool, + protocols: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct TorConfig { + #[serde(default)] + hidden_service_version: HiddenServiceVersion, + port_mapping: IndexMap, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct LanPortConfig { + ssl: bool, + mapping: u16, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Assets { + #[serde(default)] + license: Option, + #[serde(default)] + icon: Option, + #[serde(default)] + docker_images: Option, + #[serde(default)] + instructions: Option, +} +impl Assets { + pub fn license_path(&self) -> &Path { + self.license + .as_ref() + .map(|a| a.as_path()) + .unwrap_or(Path::new("LICENSE.md")) + } + pub fn icon_path(&self) -> &Path { + self.icon + .as_ref() + .map(|a| a.as_path()) + .unwrap_or(Path::new("icon.png")) + } + pub fn icon_type(&self) -> &str { + self.icon + .as_ref() + .and_then(|icon| icon.extension()) + .and_then(|ext| ext.to_str()) + .unwrap_or("png") + } + pub fn docker_images_path(&self) -> &Path { + self.docker_images + .as_ref() + .map(|a| a.as_path()) + .unwrap_or(Path::new("images.tar")) + } + pub fn instructions_path(&self) -> &Path { + self.instructions + .as_ref() + .map(|a| a.as_path()) + .unwrap_or(Path::new("INSTRUCTIONS.md")) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Description { + pub short: String, + pub long: String, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Alerts { + pub install: Option, + pub uninstall: Option, + pub restore: Option, + pub start: Option, + pub stop: Option, +} diff --git a/appmgr/src/s9pk/mod.rs b/appmgr/src/s9pk/mod.rs new file mode 100644 index 000000000..33bb052f4 --- /dev/null +++ b/appmgr/src/s9pk/mod.rs @@ -0,0 +1,6 @@ +pub mod builder; +pub mod header; +pub mod manifest; +pub mod reader; + +pub const SIG_CONTEXT: &'static [u8] = b"s9pk"; diff --git a/appmgr/src/s9pk/reader.rs b/appmgr/src/s9pk/reader.rs new file mode 100644 index 000000000..791bf2799 --- /dev/null +++ b/appmgr/src/s9pk/reader.rs @@ -0,0 +1,144 @@ +use std::io::SeekFrom; +use std::path::Path; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use digest::Output; +use sha2::{Digest, Sha512}; +use tokio::fs::File; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, ReadBuf, Take}; + +use super::header::{FileSection, Header, TableOfContents}; +use super::manifest::Manifest; +use super::SIG_CONTEXT; +use crate::install::progress::InstallProgressTracker; +use crate::{Error, ResultExt}; + +#[pin_project::pin_project] +pub struct ReadHandle<'a, R: AsyncRead + AsyncSeek + Unpin = File> { + pos: &'a mut u64, + #[pin] + rdr: Take<&'a mut R>, +} +impl<'a, R: AsyncRead + AsyncSeek + Unpin> ReadHandle<'a, R> { + pub async fn to_vec(mut self) -> std::io::Result> { + let mut buf = vec![0; self.rdr.limit() as usize]; + self.rdr.read_exact(&mut buf).await?; + Ok(buf) + } +} +impl<'a, R: AsyncRead + AsyncSeek + Unpin> AsyncRead for ReadHandle<'a, R> { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let start = buf.filled().len(); + let this = self.project(); + let pos = this.pos; + AsyncRead::poll_read(this.rdr, cx, buf).map(|res| { + **pos += (buf.filled().len() - start) as u64; + res + }) + } +} + +pub struct S9pkReader { + hash: Output, + hash_string: String, + toc: TableOfContents, + pos: u64, + rdr: R, +} +impl S9pkReader { + pub async fn open>(path: P) -> Result { + let p = path.as_ref(); + let rdr = File::open(p) + .await + .with_ctx(|_| (crate::error::ErrorKind::Filesystem, p.display().to_string()))?; + + Self::from_reader(rdr).await + } +} +impl S9pkReader> { + pub fn validated(&mut self) { + self.rdr.validated() + } +} +impl S9pkReader { + pub async fn validate(&mut self) -> Result<(), Error> { + todo!() + } + pub async fn from_reader(mut rdr: R) -> Result { + let header = Header::deserialize(&mut rdr).await?; + let pos = rdr.stream_position().await?; + + let mut hasher = Sha512::new(); + let mut buf = [0; 1024]; + let mut read; + while { + read = rdr.read(&mut buf).await?; + read != 0 + } { + hasher.update(&buf[0..read]); + } + let hash = hasher.clone().finalize(); + header + .pubkey + .verify_prehashed(hasher, Some(SIG_CONTEXT), &header.signature)?; + + Ok(S9pkReader { + hash_string: base32::encode( + base32::Alphabet::RFC4648 { padding: false }, + hash.as_slice(), + ), + hash, + toc: header.table_of_contents, + pos, + rdr, + }) + } + + pub fn hash(&self) -> &Output { + &self.hash + } + + pub fn hash_str(&self) -> &str { + self.hash_string.as_str() + } + + async fn read_handle<'a>( + &'a mut self, + section: FileSection, + ) -> Result, Error> { + if self.pos != section.position { + self.rdr.seek(SeekFrom::Start(section.position)).await?; + self.pos = section.position; + } + Ok(ReadHandle { + pos: &mut self.pos, + rdr: (&mut self.rdr).take(section.length), + }) + } + + pub async fn manifest(&mut self) -> Result { + serde_cbor::from_slice(&self.read_handle(self.toc.manifest).await?.to_vec().await?) + .with_ctx(|_| (crate::ErrorKind::ParseS9pk, "Deserializing Manifest (CBOR)")) + } + + pub async fn license<'a>(&'a mut self) -> Result, Error> { + Ok(self.read_handle(self.toc.license).await?) + } + + pub async fn instructions<'a>(&'a mut self) -> Result, Error> { + Ok(self.read_handle(self.toc.instructions).await?) + } + + pub async fn icon<'a>(&'a mut self) -> Result, Error> { + Ok(self.read_handle(self.toc.icon).await?) + } + + pub async fn docker_images<'a>(&'a mut self) -> Result, Error> { + Ok(self.read_handle(self.toc.docker_images).await?) + } +} diff --git a/appmgr/src/status/health_check.rs b/appmgr/src/status/health_check.rs new file mode 100644 index 000000000..5a059d5b9 --- /dev/null +++ b/appmgr/src/status/health_check.rs @@ -0,0 +1,135 @@ +use std::path::Path; + +use chrono::{DateTime, Utc}; +use indexmap::IndexMap; +use serde::{Deserialize, Deserializer, Serialize}; + +use crate::action::ActionImplementation; +use crate::id::Id; +use crate::net::host::Hosts; +use crate::s9pk::manifest::PackageId; +use crate::util::Version; +use crate::volume::Volumes; +use crate::Error; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] +pub struct HealthCheckId = String>(Id); +impl> std::fmt::Display for HealthCheckId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } +} +impl> AsRef for HealthCheckId { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} +impl<'de, S> Deserialize<'de> for HealthCheckId +where + S: AsRef, + Id: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(HealthCheckId(Deserialize::deserialize(deserializer)?)) + } +} +impl> AsRef for HealthCheckId { + fn as_ref(&self) -> &Path { + self.0.as_ref().as_ref() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HealthChecks(pub IndexMap); +impl HealthChecks { + pub async fn check_all( + &self, + started: &DateTime, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + hosts: &Hosts, + ) -> Result, Error> { + let res = futures::future::try_join_all(self.0.iter().map(|(id, check)| async move { + Ok::<_, Error>(( + id.clone(), + check + .check(started, pkg_id, pkg_version, volumes, hosts) + .await?, + )) + })) + .await?; + Ok(res.into_iter().collect()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HealthCheck { + #[serde(flatten)] + implementation: ActionImplementation, + pub critical: bool, +} +impl HealthCheck { + pub async fn check( + &self, + started: &DateTime, + pkg_id: &PackageId, + pkg_version: &Version, + volumes: &Volumes, + hosts: &Hosts, + ) -> Result { + let res = self + .implementation + .execute(pkg_id, pkg_version, volumes, hosts, Some(started), true) + .await?; + Ok(HealthCheckResult { + time: Utc::now(), + result: match res { + Ok(()) => HealthCheckResultVariant::Success, + Err((59, _)) => HealthCheckResultVariant::Disabled, + Err((60, _)) => HealthCheckResultVariant::WarmingUp, + Err((_, error)) => HealthCheckResultVariant::Failure { error }, + }, + }) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HealthCheckResult { + pub time: DateTime, + #[serde(flatten)] + pub result: HealthCheckResultVariant, +} +impl HealthCheckResult { + pub fn not_available() -> Self { + HealthCheckResult { + time: Utc::now(), + result: HealthCheckResultVariant::Failure { + error: "Health Check Status Not Available".to_owned(), + }, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "result")] +pub enum HealthCheckResultVariant { + WarmingUp, + Disabled, + Success, + Failure { error: String }, +} +impl std::fmt::Display for HealthCheckResultVariant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HealthCheckResultVariant::WarmingUp => write!(f, "Warming Up"), + HealthCheckResultVariant::Disabled => write!(f, "Disabled"), + HealthCheckResultVariant::Success => write!(f, "Succeeded"), + HealthCheckResultVariant::Failure { error } => write!(f, "Failed ({})", error), + } + } +} diff --git a/appmgr/src/status/mod.rs b/appmgr/src/status/mod.rs new file mode 100644 index 000000000..1a5d0ea35 --- /dev/null +++ b/appmgr/src/status/mod.rs @@ -0,0 +1,464 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use anyhow::anyhow; +use bollard::container::{ListContainersOptions, StartContainerOptions, StopContainerOptions}; +use bollard::models::{ContainerStateStatusEnum, ContainerSummaryInner}; +use bollard::Docker; +use chrono::{DateTime, Utc}; +use futures::{StreamExt, TryFutureExt}; +use indexmap::IndexMap; +use patch_db::{DbHandle, HasModel, Map, MapModel, Model, ModelData, ModelDataMut}; +use serde::{Deserialize, Serialize}; + +use self::health_check::{HealthCheckId, HealthCheckResult}; +use crate::action::docker::DockerAction; +use crate::context::RpcContext; +use crate::db::model::{ + CurrentDependencyInfo, InstalledPackageDataEntryModel, PackageDataEntryModel, +}; +use crate::dependencies::{Dependencies, DependencyError}; +use crate::id::InterfaceId; +use crate::net::host::Hosts; +use crate::s9pk::manifest::{Manifest, PackageId}; +use crate::status::health_check::HealthCheckResultVariant; +use crate::util::Invoke; +use crate::Error; + +pub mod health_check; + +// Assume docker for now +pub async fn synchronize_all(ctx: &RpcContext) -> Result<(), Error> { + let mut db = ctx.db.handle(); + let mut pkg_ids = crate::db::DatabaseModel::new() + .package_data() + .keys(&mut db) + .await?; + let mut container_names = Vec::with_capacity(pkg_ids.len()); + for id in pkg_ids.clone().into_iter() { + if let Some(version) = &*crate::db::DatabaseModel::new() + .package_data() + .idx_model(&id) + .expect(&mut db) + .await? + .installed() + .map(|i| i.manifest().version()) + .get(&mut db) + .await? + { + container_names.push(DockerAction::container_name(id.as_ref(), version)); + } else { + pkg_ids.remove(&id); + } + } + let mut filters = HashMap::new(); + filters.insert("name".to_owned(), container_names); + let info = ctx + .docker + .list_containers(Some(ListContainersOptions { + all: true, + size: false, + limit: None, + filters, + })) + .await?; + let mut fuckening = false; + for summary in info { + let id = if let Some(id) = summary.names.iter().flatten().find_map(|s| { + DockerAction::uncontainer_name(s.as_str()).and_then(|id| pkg_ids.take(id)) + }) { + id + } else { + continue; + }; + async fn status( + docker: &Docker, + id: &PackageId, + db: &mut Db, + summary: &ContainerSummaryInner, + ) -> Result { + let pkg_data = crate::db::DatabaseModel::new() + .package_data() + .idx_model(id) + .check(db) + .await? + .ok_or_else(|| { + Error::new( + anyhow!("VersionedPackageData does not exist"), + crate::ErrorKind::Database, + ) + })?; + let (mut status, manifest) = + if let Some(installed) = pkg_data.installed().check(db).await? { + ( + installed.clone().status().get_mut(db).await?, + installed.manifest().get(db).await?, + ) + } else { + return Ok(false); + }; + + let res = status.main.synchronize(docker, &*manifest, summary).await?; + + status.save(db).await?; + + Ok(res) + } + match status(&ctx.docker, &id, &mut db, &summary).await { + Ok(a) => fuckening |= a, + Err(e) => log::error!("Error syncronizing status of {}: {}", id, e), + } + } + + if fuckening { + tokio::process::Command::new("service") + .arg("docker") + .arg("restart") + .invoke(crate::ErrorKind::Docker) + .await?; + } + + for id in pkg_ids { + log::warn!("No container for {}", id); + } + + Ok(()) +} + +pub async fn check_all(ctx: &RpcContext) -> Result<(), Error> { + let mut db = ctx.db.handle(); + let hosts = Arc::new( + crate::db::DatabaseModel::new() + .network() + .hosts() + .get(&mut db) + .await? + .to_owned(), + ); + let pkg_ids = crate::db::DatabaseModel::new() + .package_data() + .keys(&mut db) + .await?; + let mut status_manifest = Vec::with_capacity(pkg_ids.len()); + let mut status_deps = Vec::with_capacity(pkg_ids.len()); + for id in &pkg_ids { + let model = crate::db::DatabaseModel::new() + .package_data() + .idx_model(id) + .check(&mut db) + .await? + .ok_or_else(|| { + Error::new( + anyhow!("PackageDataEntry does not exist"), + crate::ErrorKind::Database, + ) + })?; + if let Some(installed) = model.installed().check(&mut db).await? { + status_manifest.push(( + installed.clone().status(), + Arc::new(installed.clone().manifest().get(&mut db).await?), + )); + status_deps.push(( + installed.clone().status(), + Arc::new(installed.current_dependencies().get(&mut db).await?), + )); + } + } + drop(db); + async fn main_status( + status_model: StatusModel, + manifest: Arc>, + hosts: Arc, + mut db: Db, + ) -> Result { + let mut status = status_model.get_mut(&mut db).await?; + + status.main.check(&*manifest, &*hosts).await?; + + let res = status.main.clone(); + + status.save(&mut db).await?; + + Ok(res) + } + let (status_sender, mut statuses_recv) = tokio::sync::mpsc::channel(status_manifest.len() + 1); + let mut statuses = HashMap::with_capacity(status_manifest.len()); + futures::stream::iter( + status_manifest + .into_iter() + .zip(pkg_ids.clone()) + .zip(std::iter::repeat(hosts)), + ) + .for_each_concurrent(None, move |(((status, manifest), id), hosts)| { + let status_sender = status_sender.clone(); + async move { + match tokio::spawn(main_status(status, manifest, hosts, ctx.db.handle())) + .await + .unwrap() + { + Err(e) => { + log::error!("Error running main health check for {}: {}", id, e); + log::debug!("{:?}", e); + } + Ok(status) => { + status_sender.send((id, status)).await.expect("unreachable"); + } + } + } + }) + .await; + while let Some((id, status)) = statuses_recv.recv().await { + statuses.insert(id, status); + } + let statuses = Arc::new(statuses); + async fn dependency_status( + statuses: Arc>, + status_model: StatusModel, + current_deps: Arc>>, + mut db: Db, + ) -> Result<(), Error> { + let mut status = status_model.get_mut(&mut db).await?; + + status + .dependency_errors + .update_health_based(¤t_deps, &*statuses) + .await?; + + status.save(&mut db).await?; + + Ok(()) + } + futures::stream::iter(status_deps.into_iter().zip(pkg_ids.clone())) + .for_each_concurrent(None, |((status, deps), id)| { + let statuses = statuses.clone(); + async move { + if let Err(e) = + tokio::spawn(dependency_status(statuses, status, deps, ctx.db.handle())) + .await + .unwrap() + { + log::error!("Error running dependency health check for {}: {}", id, e); + log::debug!("{:?}", e); + } + } + }) + .await; + + Ok(()) +} + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] +pub struct Status { + pub configured: bool, + pub main: MainStatus, + pub dependency_errors: DependencyErrors, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "status")] +#[serde(rename_all = "kebab-case")] +pub enum MainStatus { + Stopped, + Stopping, + Running { + started: DateTime, + health: IndexMap, + }, + BackingUp { + started: Option>, + health: IndexMap, + }, + Restoring { + running: bool, + }, +} +impl MainStatus { + pub async fn synchronize( + &mut self, + docker: &Docker, + manifest: &Manifest, + summary: &ContainerSummaryInner, + ) -> Result { + // true if Docker Fuckening + async fn check_fuckening(docker: &Docker, manifest: &Manifest) -> Result { + Ok(docker + .inspect_container( + &DockerAction::container_name(&manifest.id, &manifest.version), + None, + ) + .await? + .state + .as_ref() + .and_then(|s| s.status) + == Some(ContainerStateStatusEnum::RUNNING)) + } + let name = DockerAction::container_name(&manifest.id, &manifest.version); + let state = summary.state.as_ref().map(|s| s.as_str()); + match state { + Some("created") | Some("exited") => match self { + MainStatus::Stopped => (), + MainStatus::Stopping => { + *self = MainStatus::Stopped; + } + MainStatus::Running { started, .. } => { + *started = Utc::now(); + docker + .start_container(&name, None::>) + .await?; + } + MainStatus::BackingUp { .. } => (), + MainStatus::Restoring { .. } => (), + }, + Some("running") | Some("restarting") => match self { + MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restoring { .. } => { + docker + .stop_container(&name, Some(StopContainerOptions { t: 30 })) + .await?; + return check_fuckening(docker, manifest).await; + } + MainStatus::Running { .. } => (), + MainStatus::BackingUp { .. } => { + docker.pause_container(&name).await?; + } + }, + Some("paused") => match self { + MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restoring { .. } => { + docker.unpause_container(&name).await?; + docker + .stop_container(&name, Some(StopContainerOptions { t: 30 })) + .await?; + return check_fuckening(docker, manifest).await; + } + MainStatus::Running { .. } => { + docker.unpause_container(&name).await?; + } + MainStatus::BackingUp { .. } => (), + }, + unknown => { + return Err(Error::new( + anyhow!("Unexpected Docker Status: {:?}", unknown), + crate::ErrorKind::Docker, + )); + } + } + Ok(false) + } + pub async fn check(&mut self, manifest: &Manifest, hosts: &Hosts) -> Result<(), Error> { + match self { + MainStatus::Running { started, health } => { + *health = manifest + .health_checks + .check_all( + started, + &manifest.id, + &manifest.version, + &manifest.volumes, + hosts, + ) + .await?; + for (check, res) in health { + if matches!( + res.result, + health_check::HealthCheckResultVariant::Failure { .. } + ) && manifest + .health_checks + .0 + .get(check) + .map(|hc| hc.critical) + .unwrap_or_default() + { + todo!("emit notification"); + *self = MainStatus::Stopping; + } + } + } + _ => (), + } + Ok(()) + } + pub fn running(&self) -> bool { + match self { + MainStatus::Running { .. } + | MainStatus::BackingUp { + started: Some(_), .. + } + | MainStatus::Restoring { running: true } => true, + _ => false, + } + } + pub fn stop(&mut self) { + match self { + MainStatus::Running { .. } => { + *self = MainStatus::Stopping; + } + MainStatus::BackingUp { started, .. } => { + *started = None; + } + MainStatus::Restoring { running } => { + *running = false; + } + _ => (), + } + } +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct DependencyErrors(pub IndexMap); +impl Map for DependencyErrors { + type Key = PackageId; + type Value = DependencyError; + fn get(&self, key: &Self::Key) -> Option<&Self::Value> { + self.0.get(key) + } +} +impl HasModel for DependencyErrors { + type Model = MapModel; +} +impl DependencyErrors { + async fn update_health_based( + &mut self, + dependencies: &IndexMap, + statuses: &HashMap, + ) -> Result<(), Error> { + for (dep_id, dep_info) in dependencies { + if matches!( + self.get(&dep_id), + Some(&DependencyError::NotRunning) + | Some(&DependencyError::HealthChecksFailed { .. }) + | None + ) { + match statuses.get(dep_id) { + Some(MainStatus::Running { ref health, .. }) + | Some(MainStatus::BackingUp { + started: Some(_), + ref health, + }) => { + let mut failures = IndexMap::new(); + for check in &dep_info.health_checks { + let res = + health + .get(check) + .cloned() + .unwrap_or_else(|| HealthCheckResult { + result: HealthCheckResultVariant::Disabled, + time: Utc::now(), + }); + if !matches!(res.result, HealthCheckResultVariant::Success) { + failures.insert(check.clone(), res); + } + } + self.0.insert( + dep_id.clone(), + DependencyError::HealthChecksFailed { failures }, + ); + } + _ => { + self.0.insert(dep_id.clone(), DependencyError::NotRunning); + } + } + } + } + + Ok(()) + } +} diff --git a/appmgr/src/update.rs b/appmgr/src/update.rs deleted file mode 100644 index 9b60111d3..000000000 --- a/appmgr/src/update.rs +++ /dev/null @@ -1,84 +0,0 @@ -use linear_map::LinearMap; - -use crate::dependencies::{DependencyError, TaggedDependencyError}; -use crate::Error; -use crate::ResultExt as _; - -pub async fn update( - name_version: &str, - dry_run: bool, -) -> Result, Error> { - let mut name_version_iter = name_version.split("@"); - let name = name_version_iter.next().unwrap(); - let version_req = name_version_iter - .next() - .map(|v| v.parse()) - .transpose() - .no_code()? - .unwrap_or_else(emver::VersionRange::any); - let version = crate::registry::version(name, &version_req).await?; - let mut res = LinearMap::new(); - for dependent in crate::apps::dependents(name, false).await? { - if crate::apps::status(&dependent, false).await?.status - != crate::apps::DockerStatus::Stopped - { - let manifest = crate::apps::manifest(&dependent).await?; - match manifest.dependencies.0.get(name) { - Some(dep) if !version.satisfies(&dep.version) => { - crate::control::stop_dependents( - &dependent, - dry_run, - DependencyError::NotRunning, - &mut res, - ) - .await?; - if crate::apps::status(name, false).await?.status - != crate::apps::DockerStatus::Stopped - { - crate::control::stop_app(&dependent, false, dry_run).await?; - res.insert( - dependent, - TaggedDependencyError { - dependency: name.to_owned(), - error: DependencyError::IncorrectVersion { - expected: version_req.clone(), - received: version.clone(), - }, - }, - ); - } - } - _ => { - crate::control::stop_dependents( - &dependent, - dry_run, - DependencyError::NotRunning, - &mut res, - ) - .await?; - if crate::apps::status(name, false).await?.status - != crate::apps::DockerStatus::Stopped - { - crate::control::stop_app(&dependent, false, dry_run).await?; - res.insert( - dependent, - TaggedDependencyError { - dependency: name.to_owned(), - error: DependencyError::NotRunning, - }, - ); - } - } - } - } - } - if dry_run { - return Ok(res); - } - let download_path = crate::install::download_name(name_version).await?; - crate::remove::remove(name, false, false).await?; - crate::install::install_path(download_path, Some(name)).await?; - crate::apps::set_recoverable(name, false).await?; - - Ok(res) -} diff --git a/appmgr/src/util.rs b/appmgr/src/util.rs index 0390797be..a26594a72 100644 --- a/appmgr/src/util.rs +++ b/appmgr/src/util.rs @@ -1,369 +1,36 @@ -use std::fmt; +use std::future::Future; +use std::hash::{Hash, Hasher}; use std::marker::PhantomData; -use std::path::{Path, PathBuf}; +use std::ops::Deref; +use std::path::Path; +use std::process::{exit, Stdio}; +use std::str::FromStr; +use std::time::Duration; -use failure::ResultExt as _; -use file_lock::FileLock; +use anyhow::anyhow; +use async_trait::async_trait; +use clap::ArgMatches; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; use tokio::fs::File; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}; -use crate::Error; -use crate::ResultExt as _; +use crate::{Error, ResultExt as _}; -#[derive(Debug, Clone)] -pub struct PersistencePath(PathBuf); -impl PersistencePath { - pub fn from_ref>(p: P) -> Self { - let path = p.as_ref(); - PersistencePath(if path.has_root() { - path.strip_prefix("/").unwrap().to_owned() - } else { - path.to_owned() - }) - } - - pub fn new(path: PathBuf) -> Self { - PersistencePath(if path.has_root() { - path.strip_prefix("/").unwrap().to_owned() - } else { - path.to_owned() - }) - } - - pub fn join>(&self, path: P) -> Self { - PersistencePath::new(self.0.join(path)) - } - - pub fn tmp(&self) -> PathBuf { - Path::new(crate::TMP_DIR).join(&self.0) - } - - pub fn path(&self) -> PathBuf { - Path::new(crate::PERSISTENCE_DIR).join(&self.0) - } - - pub async fn lock(&self, for_update: bool) -> Result { - let path = self.path(); - let lock_path = format!("{}.lock", path.display()); - if tokio::fs::metadata(Path::new(&lock_path)).await.is_err() { - // !exists - tokio::fs::File::create(&lock_path) - .await - .with_context(|e| format!("{}: {}", lock_path, e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - } - let lock = lock_file(lock_path.clone(), for_update) - .await - .with_context(|e| format!("{}: {}", lock_path, e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - Ok(lock) - } - - pub async fn exists(&self) -> bool { - tokio::fs::metadata(self.path()).await.is_ok() - } - - pub async fn maybe_read(&self, for_update: bool) -> Option> { - if self.exists().await { - // exists - Some(self.read(for_update).await) - } else { - None - } - } - - pub async fn read(&self, for_update: bool) -> Result { - let path = self.path(); - let lock = self.lock(for_update).await?; - let file = File::open(&path) - .await - .with_context(|e| format!("{}: {}", path.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - Ok(PersistenceFile::new(file, lock, None)) - } - - pub async fn write(&self, lock: Option) -> Result { - let path = self.path(); - if let Some(parent) = path.parent() { - if tokio::fs::metadata(parent).await.is_err() { - // !exists - tokio::fs::create_dir_all(parent).await?; - } - } - let lock = if let Some(lock) = lock { - lock - } else { - self.lock(true).await? - }; - Ok({ - let path = self.tmp(); - if let Some(parent) = path.parent() { - if tokio::fs::metadata(parent).await.is_err() { - // !exists - tokio::fs::create_dir_all(parent).await?; - } - } - PersistenceFile::new(File::create(path).await?, lock, Some(self.clone())) - }) - } - - pub async fn for_update(self) -> Result, Error> { - UpdateHandle::new(self).await - } - - pub async fn delete(&self) -> Result<(), Error> { - match tokio::fs::remove_file(self.path()).await { - Ok(()) => Ok(()), - Err(k) if k.kind() == std::io::ErrorKind::NotFound => Ok(()), - e => e.with_code(crate::error::FILESYSTEM_ERROR), - } - } -} - -#[derive(Debug)] -pub struct PersistenceFile { - file: Option, - lock: Option, - needs_commit: Option, -} -impl PersistenceFile { - pub fn new(file: File, lock: FileLock, needs_commit: Option) -> Self { - PersistenceFile { - file: Some(file), - lock: Some(lock), - needs_commit, - } - } - - pub fn take_lock(&mut self) -> Option { - self.lock.take() - } - - /// Commits the file to the persistence directory. - /// If this fails, the file was not saved. - pub async fn commit(mut self) -> Result<(), Error> { - if let Some(mut file) = self.file.take() { - file.flush().await?; - file.shutdown().await?; - file.sync_all().await?; - drop(file); - } - if let Some(path) = self.needs_commit.take() { - tokio::fs::rename(path.tmp(), path.path()) - .await - .with_context(|e| { - format!( - "{} -> {}: {}", - path.tmp().display(), - path.path().display(), - e - ) - }) - .with_code(crate::error::FILESYSTEM_ERROR)?; - if let Some(lock) = self.lock.take() { - unlock(lock) - .await - .with_context(|e| format!("{}.lock: {}", path.path().display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - } - - Ok(()) - } else { - Ok(()) - } - } -} -impl std::ops::Deref for PersistenceFile { - type Target = File; - - fn deref(&self) -> &Self::Target { - self.file.as_ref().unwrap() - } -} -impl std::ops::DerefMut for PersistenceFile { - fn deref_mut(&mut self) -> &mut Self::Target { - self.file.as_mut().unwrap() - } -} -impl AsRef for PersistenceFile { - fn as_ref(&self) -> &File { - &*self - } -} -impl AsMut for PersistenceFile { - fn as_mut(&mut self) -> &mut File { - &mut *self - } -} -impl Drop for PersistenceFile { - fn drop(&mut self) { - if let Some(path) = &self.needs_commit { - log::warn!( - "{} was dropped without being committed.", - path.path().display() - ); - } - } -} - -pub trait UpdateHandleMode {} -pub struct ForRead; -impl UpdateHandleMode for ForRead {} -pub struct ForWrite; -impl UpdateHandleMode for ForWrite {} - -pub struct UpdateHandle { - path: PersistencePath, - file: PersistenceFile, - mode: PhantomData, -} -impl UpdateHandle { - pub async fn new(path: PersistencePath) -> Result { - if !path.path().exists() { - tokio::fs::File::create(path.path()).await?; - } - Ok(UpdateHandle { - file: path.read(true).await?, - path, - mode: PhantomData, - }) - } - - pub async fn into_writer(mut self) -> Result, Error> { - let lock = self.file.take_lock(); - Ok(UpdateHandle { - file: self.path.write(lock).await?, - path: self.path, - mode: PhantomData, - }) - } -} - -impl UpdateHandle { - pub async fn commit(self) -> Result<(), Error> { - self.file.commit().await - } -} - -impl tokio::io::AsyncRead for UpdateHandle { - fn poll_read( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &mut ReadBuf, - ) -> std::task::Poll> { - unsafe { self.map_unchecked_mut(|a| a.file.file.as_mut().unwrap()) }.poll_read(cx, buf) - } -} - -impl tokio::io::AsyncWrite for UpdateHandle { - fn poll_write( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &[u8], - ) -> std::task::Poll> { - tokio::io::AsyncWrite::poll_write( - unsafe { self.map_unchecked_mut(|a| a.file.file.as_mut().unwrap()) }, - cx, - buf, - ) - } - fn poll_flush( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - tokio::io::AsyncWrite::poll_flush( - unsafe { self.map_unchecked_mut(|a| a.file.file.as_mut().unwrap()) }, - cx, - ) - } - fn poll_shutdown( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - tokio::io::AsyncWrite::poll_shutdown( - unsafe { self.map_unchecked_mut(|a| a.file.file.as_mut().unwrap()) }, - cx, - ) - } -} - -pub struct YamlUpdateHandle serde::Deserialize<'de>> { - inner: T, - handle: UpdateHandle, - committed: bool, -} -impl YamlUpdateHandle -where - T: serde::Serialize + for<'de> serde::Deserialize<'de>, -{ - pub async fn new(path: PersistencePath) -> Result { - let mut handle = path.for_update().await?; - let inner = from_yaml_async_reader(&mut handle).await?; - Ok(YamlUpdateHandle { - inner, - handle, - committed: false, - }) - } - - pub async fn commit(mut self) -> Result<(), Error> { - let mut file = self.handle.into_writer().await?; - to_yaml_async_writer(&mut file, &self.inner) - .await - .no_code()?; - file.commit().await?; - self.committed = true; - Ok(()) - } -} - -impl YamlUpdateHandle -where - T: serde::Serialize + for<'de> serde::Deserialize<'de> + Default, -{ - pub async fn new_or_default(path: PersistencePath) -> Result { - if !path.path().exists() { - Ok(YamlUpdateHandle { - inner: Default::default(), - handle: path.for_update().await?, - committed: false, - }) - } else { - Self::new(path).await - } - } -} - -impl std::ops::Deref for YamlUpdateHandle -where - T: serde::Serialize + for<'de> serde::Deserialize<'de> + Default, -{ - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} -impl std::ops::DerefMut for YamlUpdateHandle -where - T: serde::Serialize + for<'de> serde::Deserialize<'de> + Default, -{ - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub enum Never {} -pub fn absurd(lol: Never) -> T { - match lol {} -} -impl fmt::Display for Never { - fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result { - absurd(self.clone()) +impl Never {} +impl Never { + pub fn absurd(self) -> T { + match self {} } } -impl failure::Fail for Never {} +impl std::fmt::Display for Never { + fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.absurd() + } +} +impl std::error::Error for Never {} #[derive(Clone, Debug)] pub struct AsyncCompat(pub T); @@ -455,14 +122,6 @@ where } } -pub async fn lock_file(filename: String, for_write: bool) -> std::io::Result { - tokio::task::spawn_blocking(move || FileLock::lock(&filename, true, for_write)).await? -} - -pub async fn unlock(lock: FileLock) -> std::io::Result<()> { - tokio::task::spawn_blocking(move || lock.unlock()).await? -} - pub async fn from_yaml_async_reader(mut reader: R) -> Result where T: for<'de> serde::Deserialize<'de>, @@ -471,8 +130,8 @@ where let mut buffer = Vec::new(); reader.read_to_end(&mut buffer).await?; serde_yaml::from_slice(&buffer) - .map_err(failure::Error::from) - .with_code(crate::error::SERDE_ERROR) + .map_err(anyhow::Error::from) + .with_kind(crate::ErrorKind::Deserialization) } pub async fn to_yaml_async_writer(mut writer: W, value: &T) -> Result<(), crate::Error> @@ -480,7 +139,7 @@ where T: serde::Serialize, W: AsyncWrite + Unpin, { - let mut buffer = serde_yaml::to_vec(value).with_code(crate::error::SERDE_ERROR)?; + let mut buffer = serde_yaml::to_vec(value).with_kind(crate::ErrorKind::Serialization)?; buffer.extend_from_slice(b"\n"); writer.write_all(&buffer).await?; Ok(()) @@ -494,8 +153,8 @@ where let mut buffer = Vec::new(); reader.read_to_end(&mut buffer).await?; serde_cbor::from_slice(&buffer) - .map_err(failure::Error::from) - .with_code(crate::error::SERDE_ERROR) + .map_err(anyhow::Error::from) + .with_kind(crate::ErrorKind::Deserialization) } pub async fn from_json_async_reader(mut reader: R) -> Result @@ -506,8 +165,8 @@ where let mut buffer = Vec::new(); reader.read_to_end(&mut buffer).await?; serde_json::from_slice(&buffer) - .map_err(failure::Error::from) - .with_code(crate::error::SERDE_ERROR) + .map_err(anyhow::Error::from) + .with_kind(crate::ErrorKind::Deserialization) } pub async fn to_json_async_writer(mut writer: W, value: &T) -> Result<(), crate::Error> @@ -515,7 +174,7 @@ where T: serde::Serialize, W: AsyncWrite + Unpin, { - let buffer = serde_json::to_string(value).with_code(crate::error::SERDE_ERROR)?; + let buffer = serde_json::to_string(value).with_kind(crate::ErrorKind::Serialization)?; writer.write_all(&buffer.as_bytes()).await?; Ok(()) } @@ -525,7 +184,8 @@ where T: serde::Serialize, W: AsyncWrite + Unpin, { - let mut buffer = serde_json::to_string_pretty(value).with_code(crate::error::SERDE_ERROR)?; + let mut buffer = + serde_json::to_string_pretty(value).with_kind(crate::ErrorKind::Serialization)?; buffer.push_str("\n"); writer.write_all(&buffer.as_bytes()).await?; Ok(()) @@ -533,16 +193,19 @@ where #[async_trait::async_trait] pub trait Invoke { - async fn invoke(&mut self, name: &str) -> Result, failure::Error>; + async fn invoke(&mut self, error_kind: crate::ErrorKind) -> Result, Error>; } #[async_trait::async_trait] impl Invoke for tokio::process::Command { - async fn invoke(&mut self, name: &str) -> Result, failure::Error> { + async fn invoke(&mut self, error_kind: crate::ErrorKind) -> Result, Error> { + self.stdout(Stdio::piped()); + self.stderr(Stdio::piped()); let res = self.output().await?; - ensure!( + crate::ensure_code!( res.status.success(), - "{} Error: {}", - name, + error_kind, + "{}: {}", + error_kind, std::str::from_utf8(&res.stderr).unwrap_or("Unknown Error") ); Ok(res.stdout) @@ -567,3 +230,544 @@ pub trait ApplyRef { impl Apply for T {} impl ApplyRef for T {} + +pub fn deserialize_from_str< + 'de, + D: serde::de::Deserializer<'de>, + T: FromStr, + E: std::fmt::Display, +>( + deserializer: D, +) -> std::result::Result { + struct Visitor, E>(std::marker::PhantomData); + impl<'de, T: FromStr, Err: std::fmt::Display> serde::de::Visitor<'de> + for Visitor + { + type Value = T; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "a parsable string") + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + v.parse().map_err(|e| serde::de::Error::custom(e)) + } + } + deserializer.deserialize_str(Visitor(std::marker::PhantomData)) +} + +pub fn deserialize_from_str_opt< + 'de, + D: serde::de::Deserializer<'de>, + T: FromStr, + E: std::fmt::Display, +>( + deserializer: D, +) -> std::result::Result, D::Error> { + struct Visitor, E>(std::marker::PhantomData); + impl<'de, T: FromStr, Err: std::fmt::Display> serde::de::Visitor<'de> + for Visitor + { + type Value = Option; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "a parsable string") + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + v.parse().map(Some).map_err(|e| serde::de::Error::custom(e)) + } + fn visit_some(self, deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + deserializer.deserialize_str(Visitor(std::marker::PhantomData)) + } + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + } + deserializer.deserialize_any(Visitor(std::marker::PhantomData)) +} + +pub fn serialize_display( + t: &T, + serializer: S, +) -> Result { + String::serialize(&t.to_string(), serializer) +} + +pub fn serialize_display_opt( + t: &Option, + serializer: S, +) -> Result { + Option::::serialize(&t.as_ref().map(|t| t.to_string()), serializer) +} + +pub async fn daemon Fut, Fut: Future + Send + 'static>( + f: F, + cooldown: std::time::Duration, +) -> Result { + loop { + match tokio::spawn(f()).await { + Err(e) if e.is_panic() => return Err(anyhow!("daemon panicked!")), + _ => (), + } + tokio::time::sleep(cooldown).await + } +} + +pub trait SOption {} +pub struct SSome(T); +impl SSome { + pub fn into(self) -> T { + self.0 + } +} +impl From for SSome { + fn from(t: T) -> Self { + SSome(t) + } +} +impl SOption for SSome {} +pub struct SNone(PhantomData); +impl SNone { + pub fn new() -> Self { + SNone(PhantomData) + } +} +impl SOption for SNone {} + +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum ValuePrimative { + Null, + Boolean(bool), + String(String), + Number(serde_json::Number), +} +impl<'de> serde::de::Deserialize<'de> for ValuePrimative { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = ValuePrimative; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a JSON primative value") + } + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Null) + } + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Null) + } + fn visit_bool(self, v: bool) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Boolean(v)) + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::String(v.to_owned())) + } + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::String(v)) + } + fn visit_f32(self, v: f32) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Number( + serde_json::Number::from_f64(v as f64).ok_or_else(|| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Float(v as f64), + &"a finite number", + ) + })?, + )) + } + fn visit_f64(self, v: f64) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Number( + serde_json::Number::from_f64(v).ok_or_else(|| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Float(v), + &"a finite number", + ) + })?, + )) + } + fn visit_u8(self, v: u8) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Number(v.into())) + } + fn visit_u16(self, v: u16) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Number(v.into())) + } + fn visit_u32(self, v: u32) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Number(v.into())) + } + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Number(v.into())) + } + fn visit_i8(self, v: i8) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Number(v.into())) + } + fn visit_i16(self, v: i16) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Number(v.into())) + } + fn visit_i32(self, v: i32) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Number(v.into())) + } + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + Ok(ValuePrimative::Number(v.into())) + } + } + deserializer.deserialize_any(Visitor) + } +} + +#[derive(Debug, Clone)] +pub struct Version { + version: emver::Version, + string: String, +} +impl Version { + pub fn as_str(&self) -> &str { + self.string.as_str() + } +} +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.string) + } +} +impl From for Version { + fn from(v: emver::Version) -> Self { + Version { + string: v.to_string(), + version: v, + } + } +} +impl From for emver::Version { + fn from(v: Version) -> Self { + v.version + } +} +impl Deref for Version { + type Target = emver::Version; + fn deref(&self) -> &Self::Target { + &self.version + } +} +impl AsRef for Version { + fn as_ref(&self) -> &emver::Version { + &self.version + } +} +impl AsRef for Version { + fn as_ref(&self) -> &str { + self.as_str() + } +} +impl PartialEq for Version { + fn eq(&self, other: &Version) -> bool { + self.version.eq(&other.version) + } +} +impl Eq for Version {} +impl Hash for Version { + fn hash(&self, state: &mut H) { + self.version.hash(state) + } +} +impl<'de> Deserialize<'de> for Version { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + let version = emver::Version::from_str(&string).map_err(serde::de::Error::custom)?; + Ok(Self { string, version }) + } +} +impl Serialize for Version { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.string.serialize(serializer) + } +} + +#[async_trait] +pub trait AsyncFileExt: Sized { + async fn maybe_open + Send + Sync>(path: P) -> std::io::Result>; + async fn delete + Send + Sync>(path: P) -> std::io::Result<()>; +} +#[async_trait] +impl AsyncFileExt for File { + async fn maybe_open + Send + Sync>(path: P) -> std::io::Result> { + match File::open(path).await { + Ok(f) => Ok(Some(f)), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(e), + } + } + async fn delete + Send + Sync>(path: P) -> std::io::Result<()> { + if let Ok(m) = tokio::fs::metadata(path.as_ref()).await { + if m.is_dir() { + tokio::fs::remove_dir_all(path).await + } else { + tokio::fs::remove_file(path).await + } + } else { + Ok(()) + } + } +} + +pub struct FmtWriter(W); +impl std::io::Write for FmtWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0 + .write_str( + std::str::from_utf8(buf) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?, + ) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + Ok(buf.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename = "kebab-case")] +pub enum IoFormat { + Json, + JsonPretty, + Yaml, + Cbor, + Toml, + TomlPretty, +} +impl Default for IoFormat { + fn default() -> Self { + IoFormat::JsonPretty + } +} +impl std::fmt::Display for IoFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use IoFormat::*; + match self { + Json => write!(f, "JSON"), + JsonPretty => write!(f, "JSON (pretty)"), + Yaml => write!(f, "YAML"), + Cbor => write!(f, "CBOR"), + Toml => write!(f, "TOML"), + TomlPretty => write!(f, "TOML (pretty)"), + } + } +} +impl std::str::FromStr for IoFormat { + type Err = Error; + fn from_str(s: &str) -> Result { + serde_json::from_value(Value::String(s.to_owned())) + .with_kind(crate::ErrorKind::Deserialization) + } +} +impl IoFormat { + pub fn to_writer( + &self, + mut writer: W, + value: &T, + ) -> Result<(), Error> { + match self { + IoFormat::Json => { + serde_json::to_writer(writer, value).with_kind(crate::ErrorKind::Serialization) + } + IoFormat::JsonPretty => serde_json::to_writer_pretty(writer, value) + .with_kind(crate::ErrorKind::Serialization), + IoFormat::Yaml => { + serde_yaml::to_writer(writer, value).with_kind(crate::ErrorKind::Serialization) + } + IoFormat::Cbor => { + serde_cbor::to_writer(writer, value).with_kind(crate::ErrorKind::Serialization) + } + IoFormat::Toml => writer + .write_all(&serde_toml::to_vec(value).with_kind(crate::ErrorKind::Serialization)?) + .with_kind(crate::ErrorKind::Serialization), + IoFormat::TomlPretty => writer + .write_all( + serde_toml::to_string_pretty(value) + .with_kind(crate::ErrorKind::Serialization)? + .as_bytes(), + ) + .with_kind(crate::ErrorKind::Serialization), + } + } + pub fn to_vec(&self, value: &T) -> Result, Error> { + match self { + IoFormat::Json => serde_json::to_vec(value).with_kind(crate::ErrorKind::Serialization), + IoFormat::JsonPretty => { + serde_json::to_vec_pretty(value).with_kind(crate::ErrorKind::Serialization) + } + IoFormat::Yaml => serde_yaml::to_vec(value).with_kind(crate::ErrorKind::Serialization), + IoFormat::Cbor => serde_cbor::to_vec(value).with_kind(crate::ErrorKind::Serialization), + IoFormat::Toml => serde_toml::to_vec(value).with_kind(crate::ErrorKind::Serialization), + IoFormat::TomlPretty => serde_toml::to_string_pretty(value) + .map(|s| s.into_bytes()) + .with_kind(crate::ErrorKind::Serialization), + } + } + pub fn from_reader Deserialize<'de>>( + &self, + mut reader: R, + ) -> Result { + match self { + IoFormat::Json | IoFormat::JsonPretty => { + serde_json::from_reader(reader).with_kind(crate::ErrorKind::Deserialization) + } + IoFormat::Yaml => { + serde_yaml::from_reader(reader).with_kind(crate::ErrorKind::Deserialization) + } + IoFormat::Cbor => { + serde_cbor::from_reader(reader).with_kind(crate::ErrorKind::Deserialization) + } + IoFormat::Toml | IoFormat::TomlPretty => { + let mut s = String::new(); + reader.read_to_string(&mut s); + serde_toml::from_str(&s).with_kind(crate::ErrorKind::Deserialization) + } + } + } + pub fn from_slice Deserialize<'de>>(&self, slice: &[u8]) -> Result { + match self { + IoFormat::Json | IoFormat::JsonPretty => { + serde_json::from_slice(slice).with_kind(crate::ErrorKind::Deserialization) + } + IoFormat::Yaml => { + serde_yaml::from_slice(slice).with_kind(crate::ErrorKind::Deserialization) + } + IoFormat::Cbor => { + serde_cbor::from_slice(slice).with_kind(crate::ErrorKind::Deserialization) + } + IoFormat::Toml | IoFormat::TomlPretty => { + serde_toml::from_slice(slice).with_kind(crate::ErrorKind::Deserialization) + } + } + } +} + +pub fn display_serializable(t: T, matches: &ArgMatches<'_>) { + let format = match matches.value_of("format").map(|f| f.parse()) { + Some(Ok(f)) => f, + Some(Err(e)) => { + eprintln!("unrecognized formatter"); + exit(1) + } + None => IoFormat::default(), + }; + format + .to_writer(std::io::stdout(), &t) + .expect("Error serializing result to stdout") +} + +pub fn display_none(_: T, _: &ArgMatches) { + () +} + +pub fn parse_stdin_deserializable Deserialize<'de>>( + stdin: &mut std::io::Stdin, + matches: &ArgMatches<'_>, +) -> Result { + let format = match matches.value_of("format").map(|f| f.parse()) { + Some(Ok(f)) => f, + Some(Err(e)) => { + eprintln!("unrecognized formatter"); + exit(1) + } + None => IoFormat::default(), + }; + format.from_reader(stdin) +} + +pub fn parse_duration(arg: &str, matches: &ArgMatches<'_>) -> Result { + let units_idx = arg.find(|c: char| c.is_alphabetic()).ok_or_else(|| { + Error::new( + anyhow!("Must specify units for duration"), + crate::ErrorKind::Deserialization, + ) + })?; + let (num, units) = arg.split_at(units_idx); + match units { + "d" if num.contains(".") => Ok(Duration::from_secs_f64(num.parse::()? * 86400_f64)), + "d" => Ok(Duration::from_secs(num.parse::()? * 86400)), + "h" if num.contains(".") => Ok(Duration::from_secs_f64(num.parse::()? * 3600_f64)), + "h" => Ok(Duration::from_secs(num.parse::()? * 3600)), + "m" if num.contains(".") => Ok(Duration::from_secs_f64(num.parse::()? * 60_f64)), + "m" => Ok(Duration::from_secs(num.parse::()? * 60)), + "s" if num.contains(".") => Ok(Duration::from_secs_f64(num.parse()?)), + "s" => Ok(Duration::from_secs(num.parse()?)), + "ms" => Ok(Duration::from_millis(num.parse()?)), + "us" => Ok(Duration::from_micros(num.parse()?)), + "ns" => Ok(Duration::from_nanos(num.parse()?)), + _ => Err(Error::new( + anyhow!("Invalid units for duration"), + crate::ErrorKind::Deserialization, + )), + } +} diff --git a/appmgr/src/version/mod.rs b/appmgr/src/version/mod.rs index 322bc690b..469437cbf 100644 --- a/appmgr/src/version/mod.rs +++ b/appmgr/src/version/mod.rs @@ -1,64 +1,63 @@ use std::cmp::Ordering; use async_trait::async_trait; -use failure::ResultExt as _; use futures::stream::TryStreamExt; +use lazy_static::lazy_static; +use patch_db::DbHandle; +use rpc_toolkit::command; use tokio_compat_02::FutureExt; -use crate::util::{to_yaml_async_writer, AsyncCompat, PersistencePath}; -use crate::Error; -use crate::ResultExt as _; +// mod v0_1_0; +// mod v0_1_1; +// mod v0_1_2; +// mod v0_1_3; +// mod v0_1_4; +// mod v0_1_5; +// mod v0_2_0; +// mod v0_2_1; +// mod v0_2_2; +// mod v0_2_3; +// mod v0_2_4; +// mod v0_2_5; +// mod v0_2_6; +// mod v0_2_7; +// mod v0_2_8; +// mod v0_2_9; -mod v0_1_0; -mod v0_1_1; -mod v0_1_2; -mod v0_1_3; -mod v0_1_4; -mod v0_1_5; -mod v0_2_0; -mod v0_2_1; -mod v0_2_2; -mod v0_2_3; -mod v0_2_4; -mod v0_2_5; -mod v0_2_6; -mod v0_2_7; -mod v0_2_8; -mod v0_2_9; +// mod v0_2_10; +// mod v0_2_11; +// mod v0_2_12; -mod v0_2_10; -mod v0_2_11; -mod v0_2_12; -mod v0_2_13; -mod v0_2_14; +// pub use v0_2_12::Version as Current; +pub type Current = (); -pub use v0_2_14::Version as Current; +use crate::context::{CliContext, EitherContext, RpcContext}; +use crate::util::{to_yaml_async_writer, AsyncCompat}; +use crate::{Error, ResultExt as _}; #[derive(serde::Serialize, serde::Deserialize)] #[serde(untagged)] enum Version { V0_0_0(Wrapper<()>), - V0_1_0(Wrapper), - V0_1_1(Wrapper), - V0_1_2(Wrapper), - V0_1_3(Wrapper), - V0_1_4(Wrapper), - V0_1_5(Wrapper), - V0_2_0(Wrapper), - V0_2_1(Wrapper), - V0_2_2(Wrapper), - V0_2_3(Wrapper), - V0_2_4(Wrapper), - V0_2_5(Wrapper), - V0_2_6(Wrapper), - V0_2_7(Wrapper), - V0_2_8(Wrapper), - V0_2_9(Wrapper), - V0_2_10(Wrapper), - V0_2_11(Wrapper), - V0_2_12(Wrapper), - V0_2_13(Wrapper), - V0_2_14(Wrapper), + // V0_1_0(Wrapper), + // V0_1_1(Wrapper), + // V0_1_2(Wrapper), + // V0_1_3(Wrapper), + // V0_1_4(Wrapper), + // V0_1_5(Wrapper), + // V0_2_0(Wrapper), + // V0_2_1(Wrapper), + // V0_2_2(Wrapper), + // V0_2_3(Wrapper), + // V0_2_4(Wrapper), + // V0_2_5(Wrapper), + // V0_2_6(Wrapper), + // V0_2_7(Wrapper), + // V0_2_8(Wrapper), + // V0_2_9(Wrapper), + // V0_2_10(Wrapper), + // V0_2_11(Wrapper), + // V0_2_12(Wrapper), Other(emver::Version), } @@ -69,39 +68,62 @@ where { type Previous: VersionT; fn new() -> Self; - fn semver(&self) -> &'static emver::Version; - async fn up(&self) -> Result<(), Error>; - async fn down(&self) -> Result<(), Error>; - async fn commit(&self) -> Result<(), Error> { - let mut out = PersistencePath::from_ref("version").write(None).await?; - to_yaml_async_writer(out.as_mut(), &self.semver()).await?; - out.commit().await?; + fn semver(&self) -> &'static crate::util::Version; + async fn up(&self, db: &mut Db) -> Result<(), Error>; + async fn down(&self, db: &mut Db) -> Result<(), Error>; + async fn commit(&self, db: &mut Db) -> Result<(), Error> { + crate::db::DatabaseModel::new() + .server_info() + .version() + .put(db, self.semver()) + .await?; + Ok(()) } - async fn migrate_to(&self, version: &V) -> Result<(), Error> { + async fn migrate_to( + &self, + version: &V, + db: &mut Db, + ) -> Result<(), Error> { match self.semver().cmp(version.semver()) { - Ordering::Greater => self.rollback_to_unchecked(version).await, - Ordering::Less => version.migrate_from_unchecked(self).await, + Ordering::Greater => self.rollback_to_unchecked(version, db).await, + Ordering::Less => version.migrate_from_unchecked(self, db).await, Ordering::Equal => Ok(()), } } - async fn migrate_from_unchecked(&self, version: &V) -> Result<(), Error> { + async fn migrate_from_unchecked( + &self, + version: &V, + db: &mut Db, + ) -> Result<(), Error> { let previous = Self::Previous::new(); if version.semver() != previous.semver() { - previous.migrate_from_unchecked(version).await?; + previous.migrate_from_unchecked(version, db).await?; } - log::info!("{} -> {}", previous.semver(), self.semver()); - self.up().await?; - self.commit().await?; + log::info!( + "{} -> {}", + previous.semver().as_str(), + self.semver().as_str() + ); + self.up(db).await?; + self.commit(db).await?; Ok(()) } - async fn rollback_to_unchecked(&self, version: &V) -> Result<(), Error> { + async fn rollback_to_unchecked( + &self, + version: &V, + db: &mut Db, + ) -> Result<(), Error> { let previous = Self::Previous::new(); - log::info!("{} -> {}", self.semver(), previous.semver()); - self.down().await?; - previous.commit().await?; + log::info!( + "{} -> {}", + self.semver().as_str(), + previous.semver().as_str() + ); + self.down(db).await?; + previous.commit(db).await?; if version.semver() != previous.semver() { - previous.rollback_to_unchecked(version).await?; + previous.rollback_to_unchecked(version, db).await?; } Ok(()) } @@ -120,7 +142,7 @@ where T: VersionT, { fn deserialize>(deserializer: D) -> Result { - let v = emver::Version::deserialize(deserializer)?; + let v = crate::util::Version::deserialize(deserializer)?; let version = T::new(); if &v == version.semver() { Ok(Wrapper(version)) @@ -129,162 +151,38 @@ where } } } -const V0_0_0: emver::Version = emver::Version::new(0, 0, 0, 0); +lazy_static! { + static ref V0_0_0: crate::util::Version = emver::Version::new(0, 0, 0, 0).into(); +} #[async_trait] impl VersionT for () { type Previous = (); fn new() -> Self { () } - fn semver(&self) -> &'static emver::Version { - &V0_0_0 + fn semver(&self) -> &'static crate::util::Version { + &*V0_0_0 } - async fn up(&self) -> Result<(), Error> { + async fn up(&self, db: &mut Db) -> Result<(), Error> { Ok(()) } - async fn down(&self) -> Result<(), Error> { + async fn down(&self, db: &mut Db) -> Result<(), Error> { Ok(()) } } -pub async fn init() -> Result<(), failure::Error> { - let _lock = PersistencePath::from_ref("").lock(true).await?; - let vpath = PersistencePath::from_ref("version"); - if let Some(mut f) = vpath.maybe_read(false).await.transpose()? { - let v: Version = crate::util::from_yaml_async_reader(&mut *f).await?; - match v { - Version::V0_0_0(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_1_0(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_1_1(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_1_2(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_1_3(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_1_4(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_1_5(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_0(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_1(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_2(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_3(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_4(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_5(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_6(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_7(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_8(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_9(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_10(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_11(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_12(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_13(v) => v.0.migrate_to(&Current::new()).await?, - Version::V0_2_14(v) => v.0.migrate_to(&Current::new()).await?, - Version::Other(_) => (), - // TODO find some way to automate this? - } - } else { - ().migrate_to(&Current::new()).await?; - } - Ok(()) +pub async fn init() -> Result<(), Error> { + todo!() } pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error> { - let req_str: String = format!("{}", requirement) - .chars() - .filter(|c| !c.is_whitespace()) - .collect(); - let url = format!("{}/appmgr?spec={}", &*crate::SYS_REGISTRY_URL, req_str); - log::info!("Fetching new version from {}", url); - let response = reqwest::get(&url) - .compat() - .await - .with_code(crate::error::NETWORK_ERROR)? - .error_for_status() - .with_code(crate::error::REGISTRY_ERROR)?; - let tmp_appmgr_path = PersistencePath::from_ref("appmgr").tmp(); - if let Some(parent) = tmp_appmgr_path.parent() { - if !parent.exists() { - tokio::fs::create_dir_all(parent) - .await - .with_code(crate::error::FILESYSTEM_ERROR)?; - } - } - let mut f = tokio::fs::OpenOptions::new() - .create(true) - .write(true) - .open(&tmp_appmgr_path) - .await - .with_context(|e| format!("{}: {}", tmp_appmgr_path.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - tokio::io::copy( - &mut AsyncCompat( - response - .bytes_stream() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) - .into_async_read(), - ), - &mut f, - ) - .await - .no_code()?; - drop(f); - crate::ensure_code!( - tokio::process::Command::new("chmod") - .arg("700") - .arg(&tmp_appmgr_path) - .output() - .await? - .status - .success(), - crate::error::FILESYSTEM_ERROR, - "chmod failed" - ); - let out = std::process::Command::new(&tmp_appmgr_path) - .arg("semver") - .stdout(std::process::Stdio::piped()) - .spawn()? - .wait_with_output() - .with_context(|e| format!("{} semver: {}", tmp_appmgr_path.display(), e)) - .no_code()?; - let out_str = std::str::from_utf8(&out.stdout).no_code()?; - log::info!("Migrating to version {}", out_str); - let v: Version = serde_yaml::from_str(out_str) - .with_context(|e| format!("{}: {:?}", e, out_str)) - .with_code(crate::error::SERDE_ERROR)?; - match v { - Version::V0_0_0(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_1_0(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_1_1(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_1_2(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_1_3(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_1_4(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_1_5(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_0(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_1(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_2(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_3(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_4(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_5(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_6(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_7(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_8(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_9(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_10(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_11(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_12(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_13(v) => Current::new().migrate_to(&v.0).await?, - Version::V0_2_14(v) => Current::new().migrate_to(&v.0).await?, - Version::Other(_) => (), - // TODO find some way to automate this? - }; - let cur_path = std::path::Path::new("/usr/local/bin/appmgr"); - tokio::fs::rename(&tmp_appmgr_path, &cur_path) - .await - .with_context(|e| { - format!( - "{} -> {}: {}", - tmp_appmgr_path.display(), - cur_path.display(), - e - ) - }) - .with_code(crate::error::FILESYSTEM_ERROR)?; - - Ok(()) + todo!() +} + +#[command(rename = "git-info", local)] +pub fn git_info(#[context] _ctx: EitherContext) -> Result { + Ok( + git_version::git_version!(args = ["--always", "--abbrev=40", "--dirty=-modified"]) + .to_owned(), + ) } diff --git a/appmgr/src/version/v0_1_0.rs b/appmgr/src/version/v0_1_0.rs deleted file mode 100644 index 6a6a9eea7..000000000 --- a/appmgr/src/version/v0_1_0.rs +++ /dev/null @@ -1,279 +0,0 @@ -use std::path::Path; - -use super::*; - -const V0_1_0: emver::Version = emver::Version::new(0, 1, 0, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = (); - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_1_0 - } - async fn up(&self) -> Result<(), Error> { - tokio::fs::create_dir_all(Path::new(crate::PERSISTENCE_DIR).join("tor")).await?; - tokio::fs::create_dir_all(Path::new(crate::PERSISTENCE_DIR).join("apps")).await?; - tokio::fs::create_dir_all(Path::new(crate::TMP_DIR).join("tor")).await?; - tokio::fs::create_dir_all(Path::new(crate::TMP_DIR).join("apps")).await?; - let mut outfile = legacy::util::PersistencePath::from_ref("tor/torrc") - .write() - .await?; - tokio::io::copy( - &mut AsyncCompat( - reqwest::get(&format!("{}/torrc?spec==0.0.0", &*crate::SYS_REGISTRY_URL)) - .compat() - .await - .with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e)) - .with_code(crate::error::NETWORK_ERROR)? - .error_for_status() - .with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e)) - .with_code(crate::error::REGISTRY_ERROR)? - .bytes_stream() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) - .into_async_read(), - ), - outfile.as_mut(), - ) - .await - .with_code(crate::error::FILESYSTEM_ERROR)?; - outfile.commit().await?; - legacy::tor::set_svc( - "start9-agent", - legacy::tor::Service { - ports: vec![5959], - hidden_service_version: Default::default(), - }, - ) - .await - .no_code()?; - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} - -mod legacy { - pub mod tor { - use failure::{Error, ResultExt}; - use linear_map::LinearMap; - use tokio::io::AsyncWriteExt; - - use crate::tor::HiddenServiceVersion; - - use super::util::PersistencePath; - - pub const ETC_TOR_RC: &'static str = "/etc/tor/torrc"; - pub const HIDDEN_SERVICE_DIR_ROOT: &'static str = "/var/lib/tor"; - - #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] - pub struct Service { - pub ports: Vec, - pub hidden_service_version: HiddenServiceVersion, - } - - async fn services_map(path: &PersistencePath) -> Result, Error> { - use crate::util::Apply; - Ok(path - .maybe_read() - .await - .transpose()? - .map(crate::util::from_yaml_async_reader) - .apply(futures::future::OptionFuture::from) - .await - .transpose()? - .unwrap_or_else(LinearMap::new)) - } - - pub async fn write_services( - hidden_services: &LinearMap, - ) -> Result<(), Error> { - tokio::fs::copy(crate::TOR_RC, ETC_TOR_RC) - .await - .with_context(|e| format!("{} -> {}: {}", crate::TOR_RC, ETC_TOR_RC, e))?; - let mut f = tokio::fs::OpenOptions::new() - .append(true) - .open(ETC_TOR_RC) - .await?; - f.write("\n".as_bytes()).await?; - for (name, service) in hidden_services { - f.write("\n".as_bytes()).await?; - f.write(format!("# HIDDEN SERVICE FOR {}\n", name).as_bytes()) - .await?; - f.write( - format!( - "HiddenServiceDir {}/app-{}/\n", - HIDDEN_SERVICE_DIR_ROOT, name - ) - .as_bytes(), - ) - .await?; - f.write(format!("{}\n", service.hidden_service_version).as_bytes()) - .await?; - for port in &service.ports { - f.write(format!("HiddenServicePort {} 127.0.0.1:{}\n", port, port).as_bytes()) - .await?; - } - f.write("\n".as_bytes()).await?; - } - Ok(()) - } - - pub async fn set_svc(name: &str, service: Service) -> Result<(), Error> { - log::info!( - "Adding Tor hidden service {} to {}.", - name, - crate::SERVICES_YAML - ); - let path = PersistencePath::from_ref(crate::SERVICES_YAML); - let mut hidden_services = services_map(&path).await?; - hidden_services.insert(name.to_owned(), service); - let mut services_yaml = path.write().await?; - crate::util::to_yaml_async_writer(services_yaml.as_mut(), &hidden_services).await?; - services_yaml.write_all("\n".as_bytes()).await?; - services_yaml.commit().await?; - log::info!("Adding Tor hidden service {} to {}.", name, ETC_TOR_RC); - write_services(&hidden_services).await?; - log::info!("Restarting Tor."); - let svc_exit = std::process::Command::new("service") - .args(&["tor", "restart"]) - .status()?; - ensure!( - svc_exit.success(), - "Failed to Restart Tor: {}", - svc_exit.code().unwrap_or(0) - ); - Ok(()) - } - } - - pub mod util { - use std::path::{Path, PathBuf}; - use tokio::fs::File; - - use crate::Error; - use crate::ResultExt as _; - use failure::ResultExt as _; - - #[derive(Clone, Debug)] - pub struct PersistencePath(PathBuf); - impl PersistencePath { - pub fn from_ref>(p: P) -> Self { - let path = p.as_ref(); - PersistencePath(if path.has_root() { - path.strip_prefix("/").unwrap().to_owned() - } else { - path.to_owned() - }) - } - - pub fn tmp(&self) -> PathBuf { - Path::new(crate::TMP_DIR).join(&self.0) - } - - pub fn path(&self) -> PathBuf { - Path::new(crate::PERSISTENCE_DIR).join(&self.0) - } - - pub async fn maybe_read(&self) -> Option> { - let path = self.path(); - if path.exists() { - Some( - File::open(&path) - .await - .with_context(|e| format!("{}: {}", path.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR), - ) - } else { - None - } - } - - pub async fn write(&self) -> Result { - let path = self.path(); - if let Some(parent) = path.parent() { - if !parent.exists() { - tokio::fs::create_dir_all(parent).await?; - } - } - Ok(if path.exists() { - let path = self.tmp(); - if let Some(parent) = path.parent() { - if !parent.exists() { - tokio::fs::create_dir_all(parent).await?; - } - } - PersistenceFile::new(File::create(path).await?, Some(self.clone())) - } else { - PersistenceFile::new(File::create(path).await?, None) - }) - } - } - - #[derive(Debug)] - pub struct PersistenceFile { - file: File, - needs_commit: Option, - } - impl PersistenceFile { - pub fn new(file: File, needs_commit: Option) -> Self { - PersistenceFile { file, needs_commit } - } - /// Commits the file to the persistence directory. - /// If this fails, the file was not saved. - pub async fn commit(mut self) -> Result<(), Error> { - if let Some(path) = self.needs_commit.take() { - tokio::fs::rename(path.tmp(), path.path()) - .await - .with_context(|e| { - format!( - "{} -> {}: {}", - path.tmp().display(), - path.path().display(), - e - ) - }) - .with_code(crate::error::FILESYSTEM_ERROR) - } else { - Ok(()) - } - } - } - impl std::ops::Deref for PersistenceFile { - type Target = File; - - fn deref(&self) -> &Self::Target { - &self.file - } - } - impl std::ops::DerefMut for PersistenceFile { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.file - } - } - impl AsRef for PersistenceFile { - fn as_ref(&self) -> &File { - &*self - } - } - impl AsMut for PersistenceFile { - fn as_mut(&mut self) -> &mut File { - &mut *self - } - } - impl Drop for PersistenceFile { - fn drop(&mut self) { - if let Some(path) = &self.needs_commit { - log::warn!( - "{} was dropped without being committed.", - path.path().display() - ); - } - } - } - } -} diff --git a/appmgr/src/version/v0_1_1.rs b/appmgr/src/version/v0_1_1.rs deleted file mode 100644 index fe7be909a..000000000 --- a/appmgr/src/version/v0_1_1.rs +++ /dev/null @@ -1,204 +0,0 @@ -use std::path::Path; - -use super::*; - -const V0_1_1: emver::Version = emver::Version::new(0, 1, 1, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_1_0::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_1_1 - } - async fn up(&self) -> Result<(), Error> { - log::info!("Update torrc"); - let mut outfile = crate::util::PersistencePath::from_ref("tor/torrc") - .write(None) - .await?; - tokio::io::copy( - &mut AsyncCompat( - reqwest::get(&format!("{}/torrc?spec==0.1.1", &*crate::SYS_REGISTRY_URL)) - .compat() - .await - .with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e)) - .with_code(crate::error::NETWORK_ERROR)? - .error_for_status() - .with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e)) - .with_code(crate::error::REGISTRY_ERROR)? - .bytes_stream() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) - .into_async_read(), - ), - outfile.as_mut(), - ) - .await - .with_code(crate::error::FILESYSTEM_ERROR)?; - outfile.commit().await?; - if !std::process::Command::new("docker") - .arg("network") - .arg("create") - .arg("-d") - .arg("bridge") - .arg("--subnet=172.18.0.0/16") - .arg("start9") - .stdout(std::process::Stdio::null()) - .status()? - .success() - { - log::warn!("Failed to Create Network") - } - - match tokio::fs::remove_file(Path::new(crate::PERSISTENCE_DIR).join(crate::SERVICES_YAML)) - .await - { - Ok(_) => Ok(()), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(e) => Err(e), - } - .with_context(|e| format!("{}/{}: {}", crate::PERSISTENCE_DIR, crate::SERVICES_YAML, e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - crate::tor::reload().await?; - - for app in crate::apps::list_info().await? { - legacy::update::update(&app.0).await?; - } - - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - let mut outfile = crate::util::PersistencePath::from_ref("tor/torrc") - .write(None) - .await?; - - tokio::io::copy( - &mut AsyncCompat( - reqwest::get(&format!("{}/torrc?spec==0.1.0", &*crate::SYS_REGISTRY_URL)) - .compat() - .await - .with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e)) - .no_code()? - .error_for_status() - .with_context(|e| format!("GET {}/torrc: {}", &*crate::SYS_REGISTRY_URL, e)) - .no_code()? - .bytes_stream() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) - .into_async_read(), - ), - outfile.as_mut(), - ) - .await - .with_code(crate::error::FILESYSTEM_ERROR)?; - outfile.commit().await?; - - for app in crate::apps::list_info().await? { - legacy::remove::remove(&app.0, false).await?; - } - let tor_svcs = crate::util::PersistencePath::from_ref(crate::SERVICES_YAML).path(); - if tor_svcs.exists() { - tokio::fs::remove_file(&tor_svcs) - .await - .with_context(|e| format!("{}: {}", tor_svcs.display(), e)) - .with_code(crate::error::FILESYSTEM_ERROR)?; - } - if !std::process::Command::new("docker") - .arg("network") - .arg("rm") - .arg("start9") - .stdout(std::process::Stdio::null()) - .status()? - .success() - { - log::warn!("Failed to Remove Network"); - } - - Ok(()) - } -} - -mod legacy { - pub mod remove { - use std::path::Path; - - use crate::Error; - - pub async fn remove(name: &str, purge: bool) -> Result<(), Error> { - log::info!("Removing app from manifest."); - crate::apps::remove(name).await?; - log::info!("Stopping docker container."); - if !tokio::process::Command::new("docker") - .args(&["stop", name]) - .stdout(std::process::Stdio::null()) - .stderr(match log::max_level() { - log::LevelFilter::Error => std::process::Stdio::null(), - _ => std::process::Stdio::inherit(), - }) - .status() - .await? - .success() - { - log::error!("Failed to Stop Docker Container"); - }; - log::info!("Removing docker container."); - if !tokio::process::Command::new("docker") - .args(&["rm", name]) - .stdout(std::process::Stdio::null()) - .stderr(match log::max_level() { - log::LevelFilter::Error => std::process::Stdio::null(), - _ => std::process::Stdio::inherit(), - }) - .status() - .await? - .success() - { - log::error!("Failed to Remove Docker Container"); - }; - if purge { - log::info!("Removing tor hidden service."); - crate::tor::rm_svc(name).await?; - log::info!("Removing app metadata."); - std::fs::remove_dir_all(Path::new(crate::PERSISTENCE_DIR).join("apps").join(name))?; - log::info!("Destroying mounted volume."); - std::fs::remove_dir_all(Path::new(crate::VOLUMES).join(name))?; - log::info!("Pruning unused docker images."); - crate::ensure_code!( - std::process::Command::new("docker") - .args(&["image", "prune", "-a", "-f"]) - .stdout(std::process::Stdio::null()) - .stderr(match log::max_level() { - log::LevelFilter::Error => std::process::Stdio::null(), - _ => std::process::Stdio::inherit(), - }) - .status()? - .success(), - 3, - "Failed to Prune Docker Images" - ); - }; - - Ok(()) - } - } - pub mod update { - use crate::Error; - pub async fn update(name_version: &str) -> Result<(), Error> { - let name = name_version - .split("@") - .next() - .ok_or_else(|| failure::format_err!("invalid app id"))?; - crate::install::download_name(name_version).await?; - super::remove::remove(name, false).await?; - crate::install::install_name(name_version, true).await?; - let config = crate::apps::config(name).await?; - if let Some(cfg) = config.config { - if config.spec.matches(&cfg).is_ok() { - crate::apps::set_configured(name, true).await?; - } - } - Ok(()) - } - } -} diff --git a/appmgr/src/version/v0_1_2.rs b/appmgr/src/version/v0_1_2.rs deleted file mode 100644 index 1f41809dd..000000000 --- a/appmgr/src/version/v0_1_2.rs +++ /dev/null @@ -1,104 +0,0 @@ -use futures::StreamExt; -use futures::TryStreamExt; -use linear_map::LinearMap; - -use super::*; - -const V0_1_2: emver::Version = emver::Version::new(0, 1, 2, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_1_1::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_1_2 - } - async fn up(&self) -> Result<(), Error> { - let app_info = legacy::apps::list_info().await?; - for (name, _) in &app_info { - let p = PersistencePath::from_ref("apps") - .join(name) - .join("manifest.yaml"); - let mut f = p.for_update().await?; - let manifest: crate::manifest::ManifestV0 = crate::util::from_yaml_async_reader(&mut f) - .await - .no_code()?; - let mut f = f.into_writer().await?; - crate::util::to_yaml_async_writer(&mut f, &crate::manifest::Manifest::V0(manifest)) - .await - .no_code()?; - f.commit().await?; - } - - let p = PersistencePath::from_ref("apps.yaml"); - let exists = p.path().exists(); - let mut f = p.for_update().await?; - let info: LinearMap = if exists { - crate::util::from_yaml_async_reader(&mut f) - .await - .no_code()? - } else { - LinearMap::new() - }; - let new_info: LinearMap = futures::stream::iter(info) - .then(|(name, i)| async move { - let title = crate::apps::manifest(&name).await?.title; - Ok::<_, Error>(( - name, - crate::apps::AppInfo { - title, - version: i.version, - tor_address: i.tor_address, - configured: i.configured, - recoverable: false, - needs_restart: false, - }, - )) - }) - .try_collect() - .await?; - let mut f = f.into_writer().await?; - crate::util::to_yaml_async_writer(&mut f, &new_info) - .await - .no_code()?; - f.commit().await?; - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} - -mod legacy { - pub mod apps { - use linear_map::LinearMap; - - use crate::util::from_yaml_async_reader; - use crate::util::Apply; - use crate::util::PersistencePath; - use crate::Error; - - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] - pub struct AppInfo { - pub version: emver::Version, - pub tor_address: Option, - pub configured: bool, - } - - pub async fn list_info() -> Result, Error> { - let apps_path = PersistencePath::from_ref("apps.yaml"); - Ok(apps_path - .maybe_read(false) - .await - .transpose()? - .map(|mut f| async move { from_yaml_async_reader(&mut *f).await }) - .apply(futures::future::OptionFuture::from) - .await - .transpose()? - .unwrap_or_else(LinearMap::new)) - } - } -} diff --git a/appmgr/src/version/v0_1_3.rs b/appmgr/src/version/v0_1_3.rs deleted file mode 100644 index 2a5084adf..000000000 --- a/appmgr/src/version/v0_1_3.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::*; - -const V0_1_3: emver::Version = emver::Version::new(0, 1, 3, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_1_2::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_1_3 - } - async fn up(&self) -> Result<(), Error> { - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_1_4.rs b/appmgr/src/version/v0_1_4.rs deleted file mode 100644 index df302c88c..000000000 --- a/appmgr/src/version/v0_1_4.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::*; - -const V0_1_4: emver::Version = emver::Version::new(0, 1, 4, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_1_3::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_1_4 - } - async fn up(&self) -> Result<(), Error> { - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_1_5.rs b/appmgr/src/version/v0_1_5.rs deleted file mode 100644 index 5ab455b58..000000000 --- a/appmgr/src/version/v0_1_5.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::*; - -const V0_1_5: emver::Version = emver::Version::new(0, 1, 5, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_1_4::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_1_5 - } - async fn up(&self) -> Result<(), Error> { - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_2_0.rs b/appmgr/src/version/v0_2_0.rs deleted file mode 100644 index db5607fd1..000000000 --- a/appmgr/src/version/v0_2_0.rs +++ /dev/null @@ -1,98 +0,0 @@ -use linear_map::LinearMap; - -use super::*; -use crate::util::{to_yaml_async_writer, PersistencePath}; - -const V0_2_0: emver::Version = emver::Version::new(0, 2, 0, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_1_5::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_0 - } - async fn up(&self) -> Result<(), Error> { - let app_info: LinearMap = legacy::apps::list_info() - .await? - .into_iter() - .map(|(id, ai)| { - ( - id, - crate::apps::AppInfo { - title: ai.title, - version: ai.version, - tor_address: ai.tor_address, - configured: ai.configured, - recoverable: ai.recoverable, - needs_restart: false, - }, - ) - }) - .collect(); - let mut apps_file = PersistencePath::from_ref("apps.yaml").write(None).await?; - to_yaml_async_writer(&mut *apps_file, &app_info).await?; - apps_file.commit().await?; - - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - let app_info: LinearMap = crate::apps::list_info() - .await? - .into_iter() - .map(|(id, ai)| { - ( - id, - legacy::apps::AppInfo { - title: ai.title, - version: ai.version, - tor_address: ai.tor_address, - configured: ai.configured, - recoverable: ai.recoverable, - }, - ) - }) - .collect(); - let mut apps_file = PersistencePath::from_ref("apps.yaml").write(None).await?; - to_yaml_async_writer(&mut *apps_file, &app_info).await?; - apps_file.commit().await?; - - Ok(()) - } -} - -mod legacy { - pub mod apps { - use linear_map::LinearMap; - - use crate::util::{from_yaml_async_reader, PersistencePath}; - use crate::Error; - - fn not(b: &bool) -> bool { - !b - } - - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] - pub struct AppInfo { - pub title: String, - pub version: emver::Version, - pub tor_address: Option, - pub configured: bool, - #[serde(default)] - #[serde(skip_serializing_if = "not")] - pub recoverable: bool, - } - - pub async fn list_info() -> Result, Error> { - let apps_path = PersistencePath::from_ref("apps.yaml"); - let mut f = match apps_path.maybe_read(false).await.transpose()? { - Some(a) => a, - None => return Ok(LinearMap::new()), - }; - from_yaml_async_reader(&mut *f).await - } - } -} diff --git a/appmgr/src/version/v0_2_1.rs b/appmgr/src/version/v0_2_1.rs deleted file mode 100644 index e4b8ae954..000000000 --- a/appmgr/src/version/v0_2_1.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::*; - -const V0_2_1: emver::Version = emver::Version::new(0, 2, 1, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_2_0::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_1 - } - async fn up(&self) -> Result<(), Error> { - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_2_10.rs b/appmgr/src/version/v0_2_10.rs deleted file mode 100644 index fbd53bca1..000000000 --- a/appmgr/src/version/v0_2_10.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::*; - -const V0_2_10: emver::Version = emver::Version::new(0, 2, 10, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_2_9::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_10 - } - async fn up(&self) -> Result<(), Error> { - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_2_11.rs b/appmgr/src/version/v0_2_11.rs deleted file mode 100644 index 9c26dfaf6..000000000 --- a/appmgr/src/version/v0_2_11.rs +++ /dev/null @@ -1,38 +0,0 @@ -use super::*; -use std::os::unix::process::ExitStatusExt; - -const V0_2_11: emver::Version = emver::Version::new(0, 2, 11, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_2_10::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_11 - } - async fn up(&self) -> Result<(), Error> { - crate::tor::write_lan_services( - &crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?, - ) - .await?; - let svc_exit = std::process::Command::new("service") - .args(&["nginx", "reload"]) - .status()?; - crate::ensure_code!( - svc_exit.success(), - crate::error::GENERAL_ERROR, - "Failed to Reload Nginx: {}", - svc_exit - .code() - .or_else(|| { svc_exit.signal().map(|a| 128 + a) }) - .unwrap_or(0) - ); - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_2_12.rs b/appmgr/src/version/v0_2_12.rs deleted file mode 100644 index d2be542a8..000000000 --- a/appmgr/src/version/v0_2_12.rs +++ /dev/null @@ -1,38 +0,0 @@ -use super::*; -use std::os::unix::process::ExitStatusExt; - -const V0_2_12: emver::Version = emver::Version::new(0, 2, 12, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_2_11::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_12 - } - async fn up(&self) -> Result<(), Error> { - crate::tor::write_lan_services( - &crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?, - ) - .await?; - let svc_exit = std::process::Command::new("service") - .args(&["nginx", "reload"]) - .status()?; - crate::ensure_code!( - svc_exit.success(), - crate::error::GENERAL_ERROR, - "Failed to Reload Nginx: {}", - svc_exit - .code() - .or_else(|| { svc_exit.signal().map(|a| 128 + a) }) - .unwrap_or(0) - ); - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_2_2.rs b/appmgr/src/version/v0_2_2.rs deleted file mode 100644 index 322fc814e..000000000 --- a/appmgr/src/version/v0_2_2.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::*; - -const V0_2_2: emver::Version = emver::Version::new(0, 2, 2, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_2_1::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_2 - } - async fn up(&self) -> Result<(), Error> { - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_2_3.rs b/appmgr/src/version/v0_2_3.rs deleted file mode 100644 index 6be1c670d..000000000 --- a/appmgr/src/version/v0_2_3.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::*; - -const V0_2_3: emver::Version = emver::Version::new(0, 2, 3, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_2_2::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_3 - } - async fn up(&self) -> Result<(), Error> { - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_2_4.rs b/appmgr/src/version/v0_2_4.rs deleted file mode 100644 index eb8274f52..000000000 --- a/appmgr/src/version/v0_2_4.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::*; - -const V0_2_4: emver::Version = emver::Version::new(0, 2, 4, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_2_3::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_4 - } - async fn up(&self) -> Result<(), Error> { - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_2_5.rs b/appmgr/src/version/v0_2_5.rs deleted file mode 100644 index 5b127a198..000000000 --- a/appmgr/src/version/v0_2_5.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::*; - -const V0_2_5: emver::Version = emver::Version::new(0, 2, 5, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_2_4::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_5 - } - async fn up(&self) -> Result<(), Error> { - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_2_6.rs b/appmgr/src/version/v0_2_6.rs deleted file mode 100644 index 33c0d8133..000000000 --- a/appmgr/src/version/v0_2_6.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::*; - -const V0_2_6: emver::Version = emver::Version::new(0, 2, 6, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_2_5::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_6 - } - async fn up(&self) -> Result<(), Error> { - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_2_7.rs b/appmgr/src/version/v0_2_7.rs deleted file mode 100644 index c860dc0df..000000000 --- a/appmgr/src/version/v0_2_7.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::*; -use crate::util::Invoke; - -const V0_2_7: emver::Version = emver::Version::new(0, 2, 7, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_2_6::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_7 - } - async fn up(&self) -> Result<(), Error> { - for (app_id, _) in crate::apps::list_info().await? { - tokio::process::Command::new("docker") - .arg("stop") - .arg(&app_id) - .invoke("Docker") - .await?; - tokio::process::Command::new("docker") - .arg("update") - .arg("--restart") - .arg("no") - .arg(&app_id) - .invoke("Docker") - .await?; - } - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_2_8.rs b/appmgr/src/version/v0_2_8.rs deleted file mode 100644 index ecdf26309..000000000 --- a/appmgr/src/version/v0_2_8.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::*; -use crate::util::Invoke; - -const V0_2_8: emver::Version = emver::Version::new(0, 2, 8, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_2_7::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_8 - } - async fn up(&self) -> Result<(), Error> { - for (app_id, _) in crate::apps::list_info().await? { - tokio::process::Command::new("docker") - .arg("stop") - .arg(&app_id) - .invoke("Docker") - .await?; - tokio::process::Command::new("docker") - .arg("update") - .arg("--restart") - .arg("no") - .arg(&app_id) - .invoke("Docker") - .await?; - } - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - Ok(()) - } -} diff --git a/appmgr/src/version/v0_2_9.rs b/appmgr/src/version/v0_2_9.rs deleted file mode 100644 index d02feaf01..000000000 --- a/appmgr/src/version/v0_2_9.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::os::unix::process::ExitStatusExt; - -use super::*; - -const V0_2_9: emver::Version = emver::Version::new(0, 2, 9, 0); - -pub struct Version; -#[async_trait] -impl VersionT for Version { - type Previous = v0_2_8::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> &'static emver::Version { - &V0_2_9 - } - async fn up(&self) -> Result<(), Error> { - crate::tor::write_lan_services( - &crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?, - ) - .await?; - tokio::fs::os::unix::symlink( - crate::tor::ETC_NGINX_SERVICES_CONF, - "/etc/nginx/sites-enabled/start9-services.conf", - ) - .await - .or_else(|e| { - if e.kind() == std::io::ErrorKind::AlreadyExists { - Ok(()) - } else { - Err(e) - } - })?; - let svc_exit = std::process::Command::new("service") - .args(&["nginx", "reload"]) - .status()?; - crate::ensure_code!( - svc_exit.success(), - crate::error::GENERAL_ERROR, - "Failed to Reload Nginx: {}", - svc_exit - .code() - .or_else(|| { svc_exit.signal().map(|a| 128 + a) }) - .unwrap_or(0) - ); - Ok(()) - } - async fn down(&self) -> Result<(), Error> { - tokio::fs::remove_file("/etc/nginx/sites-enabled/start9-services.conf") - .await - .or_else(|e| match e { - e if e.kind() == std::io::ErrorKind::NotFound => Ok(()), - e => Err(e), - })?; - tokio::fs::remove_file(crate::tor::ETC_NGINX_SERVICES_CONF) - .await - .or_else(|e| match e { - e if e.kind() == std::io::ErrorKind::NotFound => Ok(()), - e => Err(e), - })?; - let svc_exit = std::process::Command::new("service") - .args(&["nginx", "reload"]) - .status()?; - crate::ensure_code!( - svc_exit.success(), - crate::error::GENERAL_ERROR, - "Failed to Reload Nginx: {}", - svc_exit - .code() - .or_else(|| { svc_exit.signal().map(|a| 128 + a) }) - .unwrap_or(0) - ); - Ok(()) - } -} diff --git a/appmgr/src/volume/disk.rs b/appmgr/src/volume/disk.rs new file mode 100644 index 000000000..97e6af3a2 --- /dev/null +++ b/appmgr/src/volume/disk.rs @@ -0,0 +1,245 @@ +use std::path::Path; + +use anyhow::anyhow; +use futures::future::try_join_all; +use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::util::Invoke; +use crate::{Error, ResultExt as _}; + +pub const ROOT_DISK: &'static str = "/dev/mmcblk0"; +pub const MAIN_DISK: &'static str = "/dev/sda"; + +pub struct Disks(IndexMap); + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct DiskInfo { + pub size: String, + pub description: Option, + pub partitions: IndexMap, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct PartitionInfo { + pub is_mounted: bool, + pub size: Option, + pub label: Option, +} + +pub async fn list() -> Result { + let output = tokio::process::Command::new("parted") + .arg("-lm") + .invoke(crate::ErrorKind::GParted) + .await?; + let output_str = std::str::from_utf8(&output)?; + let disks = output_str + .split("\n\n") + .filter_map(|s| -> Option<(String, DiskInfo)> { + let mut lines = s.split("\n"); + let has_size = lines.next()? == "BYT;"; + let disk_info_line = lines.next()?; + let mut disk_info_iter = disk_info_line.split(":"); + let logicalname = disk_info_iter.next()?.to_owned(); + let partition_prefix = if logicalname.ends_with(|c: char| c.is_digit(10)) { + logicalname.clone() + "p" + } else { + logicalname.clone() + }; + let size = disk_info_iter.next()?.to_owned(); + disk_info_iter.next()?; // transport-type + disk_info_iter.next()?; // logical-sector-size + disk_info_iter.next()?; // physical-sector-size + disk_info_iter.next()?; // partition-table-type + let description = disk_info_iter.next()?; + let description = if description.is_empty() { + None + } else { + Some(description.to_owned()) + }; + Some(( + logicalname, + DiskInfo { + size, + description, + partitions: lines + .filter_map(|partition_info_line| -> Option<(String, PartitionInfo)> { + let mut partition_info_iter = partition_info_line.split(":"); + let partition_idx = partition_info_iter.next()?; + let logicalname = partition_prefix.clone() + partition_idx; + let size = if has_size { + partition_info_iter.next()?; // begin + partition_info_iter.next()?; // end + Some(partition_info_iter.next()?.to_owned()) + } else { + None + }; + Some(( + logicalname, + PartitionInfo { + is_mounted: false, + size, + label: None, + }, + )) + }) + .collect(), + }, + )) + }); + Ok(Disks( + try_join_all(disks.map(|(logicalname, disk)| async move { + Ok::<_, Error>(( + logicalname, + DiskInfo { + partitions: try_join_all(disk.partitions.into_iter().map( + |(logicalname, mut partition)| async move { + let mut blkid_command = tokio::process::Command::new("blkid"); + let (blkid_res, findmnt_status) = futures::join!( + blkid_command + .arg(&logicalname) + .arg("-s") + .arg("LABEL") + .arg("-o") + .arg("value") + .invoke(crate::ErrorKind::Blkid), + tokio::process::Command::new("findmnt") + .arg(&logicalname) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + ); + let blkid_output = blkid_res?; + let label = std::str::from_utf8(&blkid_output)?.trim(); + if !label.is_empty() { + partition.label = Some(label.to_owned()); + } + if findmnt_status?.success() { + partition.is_mounted = true; + } + Ok::<_, Error>((logicalname, partition)) + }, + )) + .await? + .into_iter() + .collect(), + ..disk + }, + )) + })) + .await? + .into_iter() + .collect(), + )) +} + +pub async fn mount>(logicalname: &str, mount_point: P) -> Result<(), Error> { + let is_mountpoint = tokio::process::Command::new("mountpoint") + .arg(mount_point.as_ref()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .await?; + if is_mountpoint.success() { + unmount(mount_point.as_ref()).await?; + } + tokio::fs::create_dir_all(&mount_point).await?; + let mount_output = tokio::process::Command::new("mount") + .arg(logicalname) + .arg(mount_point.as_ref()) + .output() + .await?; + crate::ensure_code!( + mount_output.status.success(), + crate::ErrorKind::Filesystem, + "Error Mounting Drive: {}", + std::str::from_utf8(&mount_output.stderr).unwrap_or("Unknown Error") + ); + Ok(()) +} + +pub async fn mount_encfs, P1: AsRef>( + src: P0, + dst: P1, + password: &str, +) -> Result<(), Error> { + let mut encfs = tokio::process::Command::new("encfs") + .arg("--standard") + .arg("--public") + .arg("-S") + .arg(src.as_ref()) + .arg(dst.as_ref()) + .stdin(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn()?; + let mut stdin = encfs.stdin.take().unwrap(); + let mut stderr = encfs.stderr.take().unwrap(); + stdin.write_all(password.as_bytes()).await?; + stdin.flush().await?; + stdin.shutdown().await?; + drop(stdin); + let mut err = String::new(); + stderr.read_to_string(&mut err).await?; + if !encfs.wait().await?.success() { + Err(Error::new(anyhow!("{}", err), crate::ErrorKind::Filesystem)) + } else { + Ok(()) + } +} + +pub async fn unmount>(mount_point: P) -> Result<(), Error> { + log::info!("Unmounting {}.", mount_point.as_ref().display()); + let umount_output = tokio::process::Command::new("umount") + .arg(mount_point.as_ref()) + .output() + .await?; + crate::ensure_code!( + umount_output.status.success(), + crate::ErrorKind::Filesystem, + "Error Unmounting Drive: {}: {}", + mount_point.as_ref().display(), + std::str::from_utf8(&umount_output.stderr).unwrap_or("Unknown Error") + ); + tokio::fs::remove_dir_all(mount_point.as_ref()) + .await + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + format!("rm {}", mount_point.as_ref().display()), + ) + })?; + Ok(()) +} + +#[must_use] +pub struct MountGuard> { + path: Option

, +} +impl> MountGuard

{ + pub async fn new(logicalname: &str, mount_point: P) -> Result { + mount(logicalname, mount_point.as_ref()).await?; + Ok(Self { + path: Some(mount_point), + }) + } + pub async fn unmount(mut self) -> Result<(), Error> { + if let Some(ref path) = self.path { + unmount(path).await?; + self.path = None; + } + Ok(()) + } +} +impl> Drop for MountGuard

{ + fn drop(&mut self) { + if let Some(ref path) = self.path { + tokio::runtime::Runtime::new() + .unwrap() + .block_on(unmount(path)) + .unwrap() + } + } +} diff --git a/appmgr/src/volume/mod.rs b/appmgr/src/volume/mod.rs new file mode 100644 index 000000000..49d33d09a --- /dev/null +++ b/appmgr/src/volume/mod.rs @@ -0,0 +1,188 @@ +use std::borrow::Borrow; +use std::ops::{Deref, DerefMut}; +use std::path::{Path, PathBuf}; + +use indexmap::IndexMap; +use patch_db::{HasModel, Map, MapModel}; +use serde::{Deserialize, Deserializer, Serialize}; + +use crate::id::{Id, IdUnchecked, InterfaceId}; +use crate::s9pk::manifest::PackageId; + +pub mod disk; + +pub const PKG_VOLUME_DIR: &'static str = "/mnt/embassy-os/volumes/package-data"; +pub const BACKUP_DIR: &'static str = "/mnt/embassy-os-backups/EmbassyBackups"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] +#[serde(untagged)] +pub enum VolumeId = String> { + #[serde(rename = "BACKUP")] + Backup, + Custom(Id), +} +impl> std::fmt::Display for VolumeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VolumeId::Backup => write!(f, "BACKUP"), + VolumeId::Custom(id) => write!(f, "{}", id), + } + } +} +impl> AsRef for VolumeId { + fn as_ref(&self) -> &str { + match self { + VolumeId::Backup => "BACKUP", + VolumeId::Custom(id) => id.as_ref(), + } + } +} +impl> Borrow for VolumeId { + fn borrow(&self) -> &str { + self.as_ref() + } +} +impl> AsRef for VolumeId { + fn as_ref(&self) -> &Path { + AsRef::::as_ref(self).as_ref() + } +} +impl<'de, S> Deserialize<'de> for VolumeId +where + S: AsRef, + IdUnchecked: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let unchecked: IdUnchecked = Deserialize::deserialize(deserializer)?; + Ok(match unchecked.0.as_ref() { + "BACKUP" => VolumeId::Backup, + _ => VolumeId::Custom(Id::try_from(unchecked.0).map_err(serde::de::Error::custom)?), + }) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] +pub struct CustomVolumeId = String>(Id); + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Volumes(IndexMap); +impl Volumes { + pub fn get_path_for(&self, pkg_id: &PackageId, volume_id: &VolumeId) -> Option { + self.0 + .get(volume_id) + .map(|volume| volume.path_for(pkg_id, volume_id)) + } + pub fn to_readonly(&self) -> Self { + Volumes( + self.0 + .iter() + .map(|(id, volume)| { + let mut volume = volume.clone(); + volume.set_readonly(); + (id.clone(), volume) + }) + .collect(), + ) + } +} +impl Deref for Volumes { + type Target = IndexMap; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for Volumes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +impl Map for Volumes { + type Key = VolumeId; + type Value = Volume; + fn get(&self, key: &Self::Key) -> Option<&Self::Value> { + self.0.get(key) + } +} +impl HasModel for Volumes { + type Model = MapModel; +} + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] +#[serde(tag = "type")] +#[serde(rename_all = "kebab-case")] +pub enum Volume { + #[serde(rename_all = "kebab-case")] + Data { + #[serde(skip)] + readonly: bool, + }, + #[serde(rename_all = "kebab-case")] + Pointer { + package_id: PackageId, + volume_id: VolumeId, + path: PathBuf, + readonly: bool, + }, + #[serde(rename_all = "kebab-case")] + Certificate { interface_id: InterfaceId }, + #[serde(rename_all = "kebab-case")] + HiddenService { interface_id: InterfaceId }, + #[serde(rename_all = "kebab-case")] + #[serde(skip)] + Backup { readonly: bool }, +} +impl Volume { + pub fn path_for(&self, pkg_id: &PackageId, volume_id: &VolumeId) -> PathBuf { + match self { + Volume::Data { .. } => Path::new(PKG_VOLUME_DIR) + .join(pkg_id) + .join("volumes") + .join(volume_id), + Volume::Pointer { + package_id, + volume_id, + path, + .. + } => Path::new(PKG_VOLUME_DIR) + .join(package_id) + .join("volumes") + .join(volume_id) + .join(path), + Volume::Certificate { interface_id } => Path::new(PKG_VOLUME_DIR) + .join(pkg_id) + .join("certificates") + .join(interface_id), + Volume::HiddenService { interface_id } => Path::new(PKG_VOLUME_DIR) + .join(pkg_id) + .join("hidden-services") + .join(interface_id), + Volume::Backup { .. } => Path::new(BACKUP_DIR).join(pkg_id), + } + } + pub fn set_readonly(&mut self) { + match self { + Volume::Data { readonly } => { + *readonly = true; + } + Volume::Pointer { readonly, .. } => { + *readonly = true; + } + Volume::Backup { readonly } => { + *readonly = true; + } + _ => (), + } + } + pub fn readonly(&self) -> bool { + match self { + Volume::Data { readonly } => *readonly, + Volume::Pointer { readonly, .. } => *readonly, + Volume::Certificate { .. } => true, + Volume::HiddenService { .. } => true, + Volume::Backup { readonly } => *readonly, + } + } +}