diff --git a/Cargo.lock b/Cargo.lock index c241986..51eb857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,19 +2,65 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "anyhow" version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "binding_tool" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "clap", + "hex", + "serial_test", + "sha2", + "temp-env", "tempfile", + "toml", + "ureq", + "url", ] [[package]] @@ -23,21 +69,192 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chunked_transfer" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" + [[package]] name = "clap" -version = "2.33.3" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "12e8611f9ae4e068fa3e56931fded356ff745e70987ff76924a6e0ab1c8ef2e3" dependencies = [ "bitflags", + "indexmap", + "lazy_static", + "os_str_bytes", "textwrap", - "unicode-width", +] + +[[package]] +name = "const_fn" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" + +[[package]] +name = "cookie" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3f7034c0932dc36f5bd8ec37368d971346809435824f277cb3b8299fc56167c" +dependencies = [ + "cookie", + "idna", + "indexmap", + "log", + "publicsuffix", + "serde", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2209c310e29876f7f0b2721e7e26b84aff178aa3da5d091f9bfbf47669e60e3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +dependencies = [ + "block-buffer", + "crypto-common", + "generic-array", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", ] [[package]] @@ -51,11 +268,166 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "js-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" -version = "0.2.103" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" +checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "ppv-lite86" @@ -63,6 +435,48 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "psl-types" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8eda7c62d9ecaafdf8b62374c006de0adf61666ae96a96ba74a37134aa4e470" + +[[package]] +name = "publicsuffix" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "292972edad6bbecc137ab84c5e36421a4a6c979ea31d3cc73540dd04315b33e1" +dependencies = [ + "byteorder", + "hashbrown", + "idna", + "psl-types", +] + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rand" version = "0.8.4" @@ -118,7 +532,315 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi", + "winapi 0.3.9", +] + +[[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 0.3.9", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca9ebdfa27d3fc180e42879037b5338ab1c040c06affd00d8338598e7800943" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d09d3c15d814eda1d6a836f2f2b56a6abc1446c8a34351cb3180d3db92ffe4ce" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e90dd10c41c6bfc633da6e0c659bd25d31e0791e5974ac42970267d59eba87f7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.135" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cf9235533494ea2ddcdb794665461814781c53f19d87b76e571a1c35acbad2b" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.135" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dcde03d87d4c973c04be249e7d8f0b35db1c848c487bd43032808e59dd8328d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serial_test" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" +dependencies = [ + "lazy_static", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "socks" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30f86c7635fadf2814201a4f67efefb0007588ae7422ce299f354ab5c97f61ae" +dependencies = [ + "byteorder", + "libc", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "temp-env" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45107136c2ddf8c4b87453c02294fd0adf41751796e81e8ba3f7fd951977ab57" +dependencies = [ + "once_cell", ] [[package]] @@ -132,23 +854,149 @@ dependencies = [ "rand", "redox_syscall", "remove_dir_all", - "winapi", + "winapi 0.3.9", ] [[package]] name = "textwrap" -version = "0.11.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" dependencies = [ - "unicode-width", + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check", + "winapi 0.3.9", ] [[package]] -name = "unicode-width" -version = "0.1.9" +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "ureq" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5" +dependencies = [ + "base64", + "brotli-decompressor", + "chunked_transfer", + "cookie", + "cookie_store", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "socks", + "url", + "webpki", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" @@ -156,6 +1004,95 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wasm-bindgen" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" + +[[package]] +name = "web-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449" +dependencies = [ + "webpki", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -166,6 +1103,12 @@ 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" @@ -177,3 +1120,13 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] diff --git a/Cargo.toml b/Cargo.toml index 85a2d43..de79434 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,22 @@ description = "Generate Kubernetes service bindings for use with Cloud Native Bu [dependencies] anyhow = "1.0" +toml = "0.5" +url = "2.2" +sha2 = "0.10" +hex = "0.4" [dependencies.clap] -version = "2.33" +version = "3.0" default-features = false +features = ["std", "cargo"] + +[dependencies.ureq] +version = "2.4" +default-features = false +features = ["tls", "native-certs", "cookies", "gzip", "brotli", "socks-proxy"] [dev_dependencies] tempfile = "3" +temp-env = "0.2" +serial_test = "0.5" diff --git a/README.md b/README.md index b507efc..1c73229 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,65 @@ # Binding Tool -A tool for generating [Kubernetes service bindings](https://github.com/servicebinding/spec)for use with Cloud Native Buildpacks. The initial implementation focuses on creating bindings for use locally with `pack` and Docker (or similar tools). +A tool to make generating and consuming [Kubernetes service bindings](https://github.com/servicebinding/spec) easier to use with Cloud Native Buildpacks. + +The initial implementation focuses on creating bindings for use locally with `pack` and Docker (or similar tools). ## Usage ``` -> bt --help -binding_tool 0.1.0 +binding_tool 0.6.0 Daniel Mikusa -Generates bindings for use with Cloud Native Buildpacks +Generate Kubernetes service bindings for use with Cloud Native Buildpacks USAGE: - bt [FLAGS] [OPTIONS] --param ... --type - -FLAGS: - -f, --force force update if key exists - -h, --help Prints help information - -V, --version Prints version information + bt [SUBCOMMAND] OPTIONS: - -n, --name optional name for the binding, name defaults to the type - -p, --param ... key/value to set for the type - -t, --type type of binding + -h, --help Print help information + -V, --version Print version information -Param should be in the format key=value, where key is the -name of the binding and value is the contents of the -binding. If you wish to pull the contents of a binding -from a file, you may do so by inserting an `@` symbol at -the beginning of the contents and specifying a path. Full -paths or relative paths from the current working directory -are accepted. +SUBCOMMANDS: + add Add or modify a binding + args Convenience that generates binding args for `pack build` and `docker + run` + ca-certs Convenience for adding `ca-certificates` bindings + delete Delete a binding + dependency-mapping Convenience for adding `dependency-mapping` bindings + help Print this message or the help of the given subcommand(s) +``` -Ex: `-p my_cert=@path/to/my_cert.pem` +## Examples -By default bindings will be generated under `./bindings`, -however you may set `SERVICE_BINDING_ROOT` to change this -location. +### Creating Dependency Mapping Bindings -All types and param key names must be valid file names. -``` +1. Create dependency mappings and download dependencies for all dependencies in a buildpack: `bt dependency-mapping -b paketo-buildpacks/bellsoft-liberica` +2. Run again with a second buildpack. It'll update the dependency mappings and download dependencies. You can even use `dm` for short. `bt dm -b paketo-buildpacks/apache-tomcat`. +3. If you have the `buildpack.toml` file locally, you can `bt dm -t path/to/buildpack.toml` and it will download all dependencies from that file and create dependency mappings for them. -## Examples +### Creating CA Certificate Bindings -1. Create a `ca-certificates` binding: `bt -t 'ca-certificates' -p "VMware Root.pem=@$HOME/VMware Root.pem"` -2. Add another certificate to the binding: `bt -t 'ca-certificates' -p "VMware Support Labs Root.pem=@$HOME/VMware Support Labs.pem"` -3. Add a dependency mapping: `bt -t 'dependency-mapping' -p '23628d2945e54fc9c013a538d8902cfd371ff12ac57df390869e492002999418=file:///deps/bellsoft-jdk8u302+8-linux-amd64.tar.gz'` -4. Add another dependency mapping: `bt -t 'dependency-mapping' -p '43400304ef7ca9934b9c208df3c07f958b17ad5a9bbf5d59c73809a6cb2cadee=file:///deps/bellsoft-jre8u302+8-linux-amd64.tar.gz'` +1. Create a ca-certificate binding: `bt ca-certs -c "VMware Root.pem=@$HOME/VMware Root.pem"`. +2. Add another certificate binding this time using the short cut: `bt cc -c -p "VMware Support Labs Root.pem=@$HOME/VMware Support Labs.pem"`. -This results in: +### Add any type of Binding -``` -> tree bindings/ -bindings/ -├── ca-certificates -│   ├── VMware\ Root.pem -│   ├── VMware\ Support\ Labs\ Root.pem -│   └── type -└── dependency-mapping - ├── 23628d2945e54fc9c013a538d8902cfd371ff12ac57df390869e492002999418 - ├── 43400304ef7ca9934b9c208df3c07f958b17ad5a9bbf5d59c73809a6cb2cadee - └── type - -2 directories, 6 files -``` +1. Create a `ca-certificates` binding manually: `bt add -t 'ca-certificates' -p "VMware Root.pem=@$HOME/VMware Root.pem"` +2. Add a dependency mapping manually: `bt add -t 'dependency-mapping' -p '23628d2945e54fc9c013a538d8902cfd371ff12ac57df390869e492002999418=file:///deps/bellsoft-jdk8u302+8-linux-amd64.tar.gz'` +3. Add a random type, you can also add multiple binding entries by repeating the `-p` argument: `bt add -t some-type -p key1=value1 -p key2=val2 -p key3=val3`. +4. You can delete bindings manually, just remove the files. You can also `bt delete -n ca-certificates`, which would delete all the binding entries under the ca-certificates binding. To delete a specific binding entry, `bt delete -n ca-certificates -k "VMware Root.pem"`. + +### Consuming Bindings + +Creating the bindings is only one-half of the fun. The other half is consuming them at build and launch time. The `bt` tool has the `bt init ` command to make this easier. + +Add `eval "$(bt init bash)"` to `~/.bashrc` for Bash, or add `eval (bt init fish)` to `~/.config/fish/config.fish` for Fish. Then reload your shell. + +This will add two wrapper functions to your shell. They wrap the `docker` and `pack` commands. If a `docker run` or `pack build` are executed, then the script will append the additional arguments required for your bindings to the command. If any other subcommand of `docker` or `pack` are executed, all args are passed through unchanged. + +## Binding Storage + +By default, the `bt` tool will expect bindings to exist `$PWD/bindings`. This generally works well as you'll be running `pack build` and `docker run` from the root of your project directory. Your bindings are stored with each project. + +If you are using dependency mappings or CA certificates that you might want to share across multiple projects, you can set a value for `SERVICE_BINDING_ROOT` that points to a shared location. Then `bt` will generate args to this shared location. + +For example: `SERVICE_BINDING_ROOT=~/.bt/bindings`. This will store bindings in a shared folder. diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..8c94124 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,325 @@ +use clap::{app_from_crate, App, Arg, ArgGroup}; +use std::ffi::OsString; + +pub struct Parser<'a> { + app: clap::App<'a>, +} + +impl<'a, 'b> Parser<'a> { + /// Parse application arguments + /// + /// ### Examples + /// + /// Basic: Add a single parameter without a name + /// + /// ``` + /// let args = binding_tool::args::Parser::new().parse_args(vec!["bt", "add", "-t", "binding", "-p", "foo=bar"]); + /// let cmd = args.subcommand_matches("add").unwrap(); + /// + /// assert_eq!(cmd.value_of("TYPE").unwrap(), "binding"); + /// + /// let params:Vec<_> = cmd.values_of("PARAM").unwrap().collect(); + /// assert_eq!(params, vec!["foo=bar"]); + /// assert_eq!(cmd.value_of("NAME"), None); + /// ``` + /// + /// More Advanced: Add with multiple parameters and a name + /// + /// ``` + /// let args = binding_tool::args::Parser::new().parse_args(vec!["bt", "add", "-f", "-n", "better_name", "-t", "binding", "-p", "foo=bar", "-p", "gorilla=banana"]); + /// let cmd = args.subcommand_matches("add").unwrap(); + /// + /// assert_eq!(cmd.value_of("TYPE").unwrap(), "binding"); + /// + /// let params:Vec<_> = cmd.values_of("PARAM").unwrap().collect(); + /// assert_eq!(params, vec!["foo=bar", "gorilla=banana"]); + /// assert_eq!(cmd.value_of("NAME").unwrap(), "better_name"); + /// assert_eq!(cmd.is_present("FORCE"), true); + /// ``` + /// + /// Basic: Delete an entire binding + /// + /// ``` + /// let args = binding_tool::args::Parser::new().parse_args(vec!["bt", "delete", "-n", "binding"]); + /// let cmd = args.subcommand_matches("delete").unwrap(); + /// + /// assert_eq!(cmd.value_of("NAME").unwrap(), "binding"); + /// ``` + /// + /// More Advanced: Delete parts of a binding + /// + /// ``` + /// let args = binding_tool::args::Parser::new().parse_args(vec!["bt", "delete", "-f", "-n", "better_name", "-k", "foo"]); + /// let cmd = args.subcommand_matches("delete").unwrap(); + /// + /// let keys:Vec<_> = cmd.values_of("KEY").unwrap().collect(); + /// assert_eq!(keys, vec!["foo"]); + /// assert_eq!(cmd.value_of("NAME").unwrap(), "better_name"); + /// assert_eq!(cmd.is_present("FORCE"), true); + /// ``` + /// + /// Convenience: add ca-certificates + /// + /// ``` + /// let args = binding_tool::args::Parser::new().parse_args(vec!["bt", "ca-certs", "-f", "-n", "my-certs", "-c", "/path/to/ca.crt"]); + /// let cmd = args.subcommand_matches("ca-certs").unwrap(); + /// + /// + /// let certs:Vec<_> = cmd.values_of("CERT").unwrap().collect(); + /// assert_eq!(certs, vec!["/path/to/ca.crt"]); + /// assert_eq!(cmd.value_of("NAME").unwrap(), "my-certs"); + /// assert_eq!(cmd.is_present("FORCE"), true); + /// ``` + /// + /// Convenience: add dependency-mappings + /// + /// ``` + /// let args = binding_tool::args::Parser::new().parse_args(vec!["bt", "dependency-mapping", "-n", "my-deps", "-t", "/path/to/file.zip"]); + /// let cmd = args.subcommand_matches("dependency-mapping").unwrap(); + /// + /// let files:Vec<_> = cmd.values_of("TOML").unwrap().collect(); + /// assert_eq!(files, vec!["/path/to/file.zip"]); + /// assert_eq!(cmd.value_of("NAME").unwrap(), "my-deps"); + /// ``` + /// + /// Convenience: add dependency-mappings from buildpack + /// + /// ``` + /// let args = binding_tool::args::Parser::new().parse_args(vec!["bt", "dependency-mapping", "-b", "buildpack/id-1:v1.0.1", "-b", "buildpack/id-2:v2.1.0"]); + /// let cmd = args.subcommand_matches("dependency-mapping").unwrap(); + /// + /// let bps:Vec<_> = cmd.values_of("BUILDPACK").unwrap().collect(); + /// assert_eq!(bps, vec!["buildpack/id-1:v1.0.1", "buildpack/id-2:v2.1.0"]); + /// ``` + /// + /// Convenience: configure bash + /// + /// ``` + /// let args = binding_tool::args::Parser::new().parse_args(vec!["bt", "init", "bash"]); + /// let cmd = args.subcommand_matches("init").unwrap(); + /// + /// assert_eq!(cmd.value_of("SHELL").unwrap(), "bash"); + /// ``` + /// + /// Convenience: don't set the type of args and fails + /// + /// ``` + /// let res = binding_tool::args::Parser::new().try_parse_args(vec!["bt", "init"]); + /// assert!(res.is_err(), "should require a argument"); + /// ``` + /// + /// + /// Convenience: add arguments for docker run + /// + /// ``` + /// let args = binding_tool::args::Parser::new().parse_args(vec!["bt", "args", "-d"]); + /// let cmd = args.subcommand_matches("args").unwrap(); + /// + /// assert_eq!(cmd.is_present("DOCKER"), true); + /// assert_eq!(cmd.is_present("PACK"), false); + /// ``` + /// + /// Convenience: add arguments for pack build + /// + /// ``` + /// let args = binding_tool::args::Parser::new().parse_args(vec!["bt", "args", "-p"]); + /// let cmd = args.subcommand_matches("args").unwrap(); + /// + /// assert_eq!(cmd.is_present("DOCKER"), false); + /// assert_eq!(cmd.is_present("PACK"), true); + /// ``` + /// + /// Convenience: don't set the type of args and fails + /// + /// ``` + /// let res = binding_tool::args::Parser::new().try_parse_args(vec!["bt", "args"]); + /// assert!(res.is_err(), "should require a argument"); + /// ``` + /// + pub fn parse_args(self, args: I) -> clap::ArgMatches + where + I: IntoIterator, + T: Into + Clone, + { + self.app.get_matches_from(args) + } + + pub fn try_parse_args(self, args: I) -> clap::Result + where + I: IntoIterator, + T: Into + Clone, + { + self.app.try_get_matches_from(args) + } + + pub fn new() -> Parser<'a> { + let force = Arg::new("FORCE") + .short('f') + .long("force") + .takes_value(false) + .help("force update if key exists"); + + Parser { + app: app_from_crate!() + .subcommand( + App::new("add") + .alias("a") + .arg(&force) + .arg( + Arg::new("NAME") + .short('n') + .long("name") + .value_name("name") + .required(false) + .help("optional name for the binding,\nname defaults to the type"), + ) + .arg( + Arg::new("TYPE") + .short('t') + .long("type") + .value_name("type") + .help("type of binding") + .required(true), + ) + .arg( + Arg::new("PARAM") + .short('p') + .long("param") + .value_name("key=val") + .multiple_occurrences(true) + .required(true) + .help("key/value to set for the type"), + ) + .about("Add or modify a binding") + .after_help( include_str!("help/additional_help_param.txt")), + ) + .subcommand( + App::new("delete") + .alias("d") + .arg(&force) + .arg( + Arg::new("NAME") + .short('n') + .long("name") + .value_name("name") + .required(true) + .help("name for the binding"), + ) + .arg( + Arg::new("KEY") + .short('k') + .long("key") + .value_name("key") + .multiple_occurrences(true) + .required(false) + .help("specific key to delete"), + ) + .about("Delete a binding") + .after_help(include_str!("help/additional_help_binding.txt")), + ) + .subcommand( + App::new("ca-certs") + .alias("cc") + .arg(&force) + .arg( + Arg::new("NAME") + .short('n') + .long("name") + .value_name("name") + .required(false) + .help("optional name for the binding,\nname defaults to the type"), + ) + .arg( + Arg::new("CERT") + .short('c') + .long("cert") + .value_name("cert") + .required(true) + .multiple_occurrences(true) + .help("path to a CA certificate to add"), + ) + .about("Convenience for adding `ca-certificates` bindings") + .after_help(include_str!("help/additional_help_binding.txt")), + ) + .subcommand( + App::new("dependency-mapping") + .alias("dm") + .arg(&force) + .arg( + Arg::new("NAME") + .short('n') + .long("name") + .value_name("name") + .required(false) + .help("optional name for the binding,\nname defaults to the type"), + ) + .arg( + Arg::new("TOML") + .short('t') + .long("toml") + .value_name("toml") + .multiple_occurrences(true) + .conflicts_with("BUILDPACK") + .help("path to local buildpack.toml file with metadata dependencies"), + ) + .arg( + Arg::new("BUILDPACK") + .short('b') + .long("buildpack") + .value_name("buildpack") + .multiple_occurrences(true) + .conflicts_with("TOML") + .help("buildpack ID from which dependencies will be loaded"), + ) + .about("Convenience for adding `dependency-mapping` bindings") + .after_help(include_str!("help/additional_help_binding.txt")), + ) + .subcommand( + App::new("init") + .arg( + Arg::new("SHELL") + .value_name("shell") + .required(true) + .possible_values(["bash", "fish"]) + .help("type of shell script to generate")) + .about( + "Generates shell wrappers that make using `pack build` and `docker run` easier", + ), + ) + .subcommand( + App::new("args") + .arg( + Arg::new("DOCKER") + .short('d') + .long("docker") + .takes_value(false) + .conflicts_with("PACK") + .help("generates binding args for `docker run`"), + ) + .arg( + Arg::new("PACK") + .short('p') + .long("pack") + .takes_value(false) + .conflicts_with("DOCKER") + .help("generates binding args for `pack build`"), + ) + .group( + ArgGroup::new("TYPES") + .args(&["DOCKER", "PACK"]) + .required(true) + ) + .about( + "Convenience that generates binding args for `pack build` and `docker run`", + ) + .after_help(include_str!("help/additional_help_binding.txt")), + ) + } + } +} + +impl<'a, 'b> Default for Parser<'a> { + fn default() -> Self { + Self::new() + } +} diff --git a/src/bin/bt.rs b/src/bin/bt.rs index 16f40bc..8ae25c7 100644 --- a/src/bin/bt.rs +++ b/src/bin/bt.rs @@ -1,45 +1,6 @@ use anyhow::Result; -use std::env; +use binding_tool::BT; fn main() -> Result<()> { - let matches = binding_tool::parse_args(env::args()); - - // required (it's OK to unwrap) - let binding_type = matches.value_of("TYPE").unwrap(); - let binding_key_vals = matches.values_of("PARAM").unwrap(); - - // optional (it's not OK to unwrap) - let binding_name = matches.value_of("NAME"); - - // binding root = SERVICE_BINDING_ROOT (or default to "./bindings") - let bindings_home = match env::var("SERVICE_BINDING_ROOT") { - Ok(root) => root, - Err(_) => env::current_dir() - .unwrap() - .join("bindings") - .to_str() - .unwrap() - .into(), - }; - - // process bindings - return if matches.is_present("FORCE") { - let btp = binding_tool::BindingProcessor::new( - &bindings_home, - binding_type, - binding_name, - binding_tool::TrueBindingConfirmer {}, - ); - - btp.process_bindings(binding_key_vals) - } else { - let btp = binding_tool::BindingProcessor::new( - &bindings_home, - binding_type, - binding_name, - binding_tool::ConsoleBindingConfirmer {}, - ); - - btp.process_bindings(binding_key_vals) - }; + BT {}.exec() } diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..4735007 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,977 @@ +use std::io::{prelude::*, stdin, Stdout}; +use std::str::FromStr; +use std::{env, fs, path, str}; + +use anyhow::{anyhow, bail, ensure, Context, Result}; +use clap::ArgMatches; + +use crate::{args, deps}; + +pub struct BT {} + +impl BT { + pub fn exec(self) -> Result<()> { + let matcher = args::Parser::new(); + let matches = matcher.parse_args(env::args()); + let executed_command = matches.subcommand_name().unwrap_or("help"); + let args = matches.subcommand_matches(executed_command); + + match Command::from_str(executed_command) { + Ok(Command::Add(mut handler)) => handler.handle(args), + Ok(Command::Args(mut handler)) => handler.handle(args), + Ok(Command::CaCerts(mut handler)) => handler.handle(args), + Ok(Command::Delete(mut handler)) => handler.handle(args), + Ok(Command::DependencyMapping(mut handler)) => handler.handle(args), + Ok(Command::Init(mut handler)) => handler.handle(args), + Err(err) => Err(err), + } + } +} + +fn service_binding_root() -> String { + // binding root = SERVICE_BINDING_ROOT (or default to "./bindings") + match env::var("SERVICE_BINDING_ROOT") { + Ok(root) => root, + Err(_) => env::current_dir() + .unwrap() + .join("bindings") + .to_str() + .unwrap() + .into(), + } +} + +trait BindingConfirmer { + fn confirm(&self, msg: &str) -> bool; +} + +enum BindingConfirmers { + Console, + Always, + Never, +} + +impl BindingConfirmers { + fn confirm(&self, msg: &str) -> bool { + match self { + BindingConfirmers::Always => AlwaysBindingConfirmer {}.confirm(msg), + BindingConfirmers::Never => NeverBindingConfirmer {}.confirm(msg), + BindingConfirmers::Console => ConsoleBindingConfirmer {}.confirm(msg), + } + } +} + +struct ConsoleBindingConfirmer {} + +impl BindingConfirmer for ConsoleBindingConfirmer { + fn confirm(&self, msg: &str) -> bool { + println!("{} (yes or no)", msg); + + let mut input: String = String::new(); + let res = stdin().lock().read_line(&mut input); + let input = input.trim().to_lowercase(); + res.is_ok() && (input == "y" || input == "yes") + } +} + +struct AlwaysBindingConfirmer {} + +impl BindingConfirmer for AlwaysBindingConfirmer { + fn confirm(&self, _: &str) -> bool { + true + } +} + +struct NeverBindingConfirmer {} + +impl BindingConfirmer for NeverBindingConfirmer { + fn confirm(&self, _: &str) -> bool { + false + } +} + +struct BindingProcessor<'a> { + bindings_home: &'a str, + binding_type: Option<&'a str>, + binding_name: Option<&'a str>, + confirmer: BindingConfirmers, +} + +impl<'a> BindingProcessor<'a> { + fn new( + bindings_home: &'a str, + binding_type: Option<&'a str>, + binding_name: Option<&'a str>, + confirmer: BindingConfirmers, + ) -> BindingProcessor<'a> { + BindingProcessor { + bindings_home, + binding_type, + binding_name, + confirmer, + } + } + + fn delete_bindings + Clone>( + self: &BindingProcessor<'a>, + binding_keys: I, + ) -> Result<()> { + let root = path::Path::new(self.bindings_home); + ensure!(root.is_dir(), "bindings home must be a directory"); + + let binding_path = path::Path::new(self.bindings_home).join(self.binding_name.unwrap()); + + for binding_key in binding_keys.clone() { + let binding_key_path = binding_path.join(binding_key); + if binding_key_path.exists() { + let result = &self.confirmer.confirm(&format!( + "Are you sure you want to delete {}?", + binding_key_path.to_string_lossy() + )); + + anyhow::ensure!(result, "confirmation declined, exiting"); + fs::remove_file(binding_key_path)?; + } + } + + if binding_keys.count() == 0 { + let result = &self.confirmer.confirm(&format!( + "Are you sure you want to delete {}?", + binding_path.to_string_lossy() + )); + + anyhow::ensure!(result, "confirmation declined, exiting"); + fs::remove_dir_all(binding_path)? + } + + Ok(()) + } + + fn add_bindings>( + self: &BindingProcessor<'a>, + binding_key_vals: I, + ) -> Result<()> { + for binding_key_val in binding_key_vals { + self.add_binding(binding_key_val)?; + } + + Ok(()) + } + + fn add_binding>(self: &BindingProcessor<'a>, binding_key_val: S) -> Result<()> { + ensure!( + self.binding_type.is_some(), + "binding type is required when adding a binding" + ); + let binding_type = self.binding_type.unwrap(); + let binding_path = + path::Path::new(self.bindings_home).join(self.binding_name.unwrap_or(binding_type)); + + if let Some((binding_key, binding_value)) = binding_key_val.as_ref().split_once("=") { + let writer = BindingWriter::new(binding_path, binding_type, binding_key, binding_value); + + if writer.binding_key_path().exists() { + let result = &self + .confirmer + .confirm("The binding alread exists, do you wish to continue?"); + + anyhow::ensure!(result, "binding already exists"); + } + + writer.write() + } else { + Err(anyhow!( + "could not parse key/value -> {}", + binding_key_val.as_ref() + )) + } + } +} + +struct BindingWriter<'a, P> { + path: P, + b_type: &'a str, + key: &'a str, + value: &'a str, +} + +impl<'a, P> BindingWriter<'a, P> +where + P: AsRef, +{ + fn new(path: P, b_type: &'a str, key: &'a str, value: &'a str) -> BindingWriter<'a, P> { + BindingWriter { + path, + b_type, + key, + value, + } + } + + fn binding_key_path(&self) -> path::PathBuf { + self.path.as_ref().join(self.key) + } + + fn write(&self) -> Result<()> { + fs::create_dir_all(self.path.as_ref()) + .with_context(|| format!("{}", self.path.as_ref().to_string_lossy()))?; + + self.write_type()?; + + if self.value.starts_with('@') { + self.write_key_as_file()?; + } else { + self.write_key_as_value()?; + } + + Ok(()) + } + + fn write_type(&self) -> Result<()> { + let mut type_file = fs::File::create(self.path.as_ref().join("type")) + .with_context(|| "cannot open type file")?; + type_file + .write_all(self.b_type.as_bytes()) + .with_context(|| "cannot write the type file") + } + + fn write_key_as_file(&self) -> Result { + let src = self.value.trim_start_matches('@'); + let src_path = path::Path::new(src) + .canonicalize() + .with_context(|| format!("cannot canonicalize path to source file: {}", src))?; + fs::copy(&src_path, self.binding_key_path()).with_context(|| { + format!( + "failed to copy {} to {}", + src_path.to_string_lossy(), + self.binding_key_path().to_string_lossy() + ) + }) + } + + fn write_key_as_value(&self) -> Result<()> { + let mut binding_file = fs::File::create(self.binding_key_path()).with_context(|| { + format!( + "cannot open binding key path: {}", + self.binding_key_path().to_string_lossy() + ) + })?; + binding_file + .write_all(self.value.as_bytes()) + .with_context(|| { + format!( + "cannot write to binding key path: {}", + self.binding_key_path().to_string_lossy() + ) + }) + } +} + +trait CommandHandler<'a> { + fn handle(&mut self, args: Option<&ArgMatches>) -> Result<()>; +} + +enum Command { + Add(AddCommandHandler), + Args(ArgsCommandHandler), + CaCerts(CaCertsCommandHandler), + Delete(DeleteCommandHandler), + DependencyMapping(DependencyMappingCommandHandler), + Init(InitCommandHandler), +} + +impl str::FromStr for Command { + type Err = anyhow::Error; + + fn from_str(input: &str) -> Result { + match input { + "add" => Ok(Command::Add(AddCommandHandler {})), + "delete" => Ok(Command::Delete(DeleteCommandHandler {})), + "ca-certs" => Ok(Command::CaCerts(CaCertsCommandHandler {})), + "dependency-mapping" => Ok(Command::DependencyMapping( + DependencyMappingCommandHandler {}, + )), + "args" => Ok(Command::Args(ArgsCommandHandler { + output: std::io::stdout(), + })), + "init" => Ok(Command::Init(InitCommandHandler { + output: std::io::stdout(), + })), + _ => bail!("could not part argument"), + } + } +} + +struct AddCommandHandler {} + +impl<'a> CommandHandler<'a> for AddCommandHandler { + fn handle(&mut self, args: Option<&ArgMatches>) -> Result<()> { + ensure!(args.is_some(), "missing required args"); + let args = args.unwrap(); + + let binding_key_vals = args.values_of("PARAM"); + ensure!( + binding_key_vals.is_some(), + "binding parameter (key=val) is required" + ); + + let binding_type = args.value_of("TYPE"); + let binding_name = args.value_of("NAME"); + let bindings_home = service_binding_root(); + + let confirmer = if args.is_present("FORCE") { + BindingConfirmers::Always + } else { + BindingConfirmers::Console + }; + + // process bindings + let btp = BindingProcessor::new(&bindings_home, binding_type, binding_name, confirmer); + btp.add_bindings(binding_key_vals.unwrap()) + } +} + +struct DeleteCommandHandler {} + +impl<'a> CommandHandler<'a> for DeleteCommandHandler { + fn handle(&mut self, args: Option<&ArgMatches>) -> Result<()> { + ensure!(args.is_some(), "missing required args"); + let args = args.unwrap(); + + // required (it's OK to unwrap) + let binding_name = args.value_of("NAME"); + ensure!(binding_name.is_some(), "binding name is required"); + + // not required, but OK to use default (empty iterator) + let binding_key_vals = args.values_of("KEY").unwrap_or_default(); + + // binding root = SERVICE_BINDING_ROOT (or default to "./bindings") + let bindings_home = service_binding_root(); + + let confirmer = if args.is_present("FORCE") { + BindingConfirmers::Never + } else { + BindingConfirmers::Console + }; + + // process bindings + let btp = BindingProcessor::new(&bindings_home, None, binding_name, confirmer); + btp.delete_bindings(binding_key_vals) + } +} + +struct CaCertsCommandHandler {} + +impl<'a> CommandHandler<'a> for CaCertsCommandHandler { + fn handle(&mut self, args: Option<&ArgMatches>) -> Result<()> { + ensure!(args.is_some(), "missing required args"); + let args = args.unwrap(); + + let bindings_home = service_binding_root(); + let binding_name = args.value_of("NAME").unwrap_or("ca-certificates"); + let certs = args.values_of("CERT"); + + let confirmer = if args.is_present("FORCE") { + BindingConfirmers::Always + } else { + BindingConfirmers::Console + }; + + // process bindings + let btp = BindingProcessor::new( + &bindings_home, + Some("ca-certificates"), + Some(binding_name), + confirmer, + ); + + let cert_args: Vec = certs + .unwrap() + .enumerate() + .map(|(i, c)| match path::Path::new(c).file_name() { + Some(file_name) => format!("{}=@{}", file_name.to_string_lossy(), c), + None => format!("cert-{}=@{}", i, c), + }) + .collect(); + + btp.add_bindings(cert_args.iter().map(|s| &s[..])) + } +} + +struct DependencyMappingCommandHandler {} + +impl<'a> CommandHandler<'a> for DependencyMappingCommandHandler { + fn handle(&mut self, args: Option<&ArgMatches>) -> Result<()> { + // TODO: add support for id & version filters + ensure!(args.is_some(), "missing required args"); + let args = args.unwrap(); + + let buildpack = args.value_of("BUILDPACK"); + let toml_file = args.value_of("TOML"); + + let bindings_home = service_binding_root(); + let binding_name = args.value_of("NAME").unwrap_or("dependency-mapping"); + let confirmer = if args.is_present("FORCE") { + BindingConfirmers::Always + } else { + BindingConfirmers::Console + }; + + // process bindings + let btp = BindingProcessor::new( + &bindings_home, + Some("dependency-mapping"), + Some(binding_name), + confirmer, + ); + + let deps = if let Some(buildpack) = buildpack { + deps::parse_buildpack_toml_from_network(buildpack) + } else if let Some(toml_file) = toml_file { + deps::parse_buildpack_toml_from_disk(path::Path::new(toml_file)) + } else { + Err(anyhow!("must have a buildpack.toml file")) + }?; + + let binding_path = path::Path::new(&bindings_home).join(binding_name); + fs::create_dir_all(binding_path.join("binaries"))?; + deps::download_dependencies(deps.clone(), binding_path)?; + + let deps_args: Vec = deps + .iter() + .filter_map(|d| { + if let Ok(filename) = d.filename() { + Some(format!( + "{}=file:///bindings/{}/binaries/{}", + d.sha256, binding_name, filename + )) + } else { + None + } + }) + .collect(); + btp.add_bindings(deps_args.iter().map(|s| &s[..])) + } +} + +struct ArgsCommandHandler { + output: T, +} + +impl<'a, T> CommandHandler<'a> for ArgsCommandHandler +where + T: Write, +{ + fn handle(&mut self, args: Option<&ArgMatches>) -> Result<()> { + ensure!(args.is_some(), "missing required args"); + let args = args.unwrap(); + + // binding root = SERVICE_BINDING_ROOT (or default to "./bindings") + let bindings_root = service_binding_root(); + let bindings_home = path::Path::new(&bindings_root); + + if !bindings_home.exists() { + return Ok(()); + } + + let binding_count = bindings_home + .read_dir()? + .filter_map(|res| res.ok()) + .filter(|entry| entry.path().is_dir() && entry.path().join("type").exists()) + .count(); + if binding_count == 0 { + return Ok(()); + } + + match (args.is_present("DOCKER"), args.is_present("PACK")) { + (false, true) => write!( + self.output, + r#"--volume {}:/bindings --env SERVICE_BINDING_ROOT=/bindings"#, + bindings_root + )?, + (true, false) => write!( + self.output, + r#"--volume {}:/bindings --env SERVICE_BINDING_ROOT=/bindings"#, + bindings_root + )?, + _ => bail!("cannot have both docker and pack flags"), + }; + + Ok(()) + } +} + +struct InitCommandHandler { + output: T, +} + +impl<'a, T> CommandHandler<'a> for InitCommandHandler +where + T: Write, +{ + fn handle(&mut self, args: Option<&ArgMatches>) -> Result<()> { + ensure!(args.is_some(), "missing required args"); + let args = args.unwrap(); + + let shell = args.value_of("SHELL").unwrap(); // required, should not fail + + writeln!( + self.output, + "{}", + match shell { + "fish" => include_str!("scripts/fish.sh"), + "bash" => include_str!("scripts/bash.sh"), + _ => bail!("unsupported shell {}", shell), + } + ) + .map_err(|e| anyhow!(e)) + } +} + +#[cfg(test)] +mod tests { + use serial_test::serial; + use std::str::Utf8Error; + + use super::*; + + struct TestBuffer { + buffer: Vec, + } + + impl Write for TestBuffer { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.buffer.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.buffer.flush() + } + } + + impl TestBuffer { + fn new() -> TestBuffer { + TestBuffer { buffer: vec![] } + } + + fn writer(&mut self) -> &mut impl Write { + &mut self.buffer + } + + fn string(&self) -> Result<&str, Utf8Error> { + str::from_utf8(&self.buffer) + } + } + + #[test] + #[serial(requires_cwd)] + fn given_no_bindings_root_set_it_returns_current_working_directory() { + temp_env::with_var_unset("SERVICE_BINDING_ROOT", || { + let root = super::service_binding_root(); + assert!(root.starts_with(env::current_dir().unwrap().to_str().unwrap())); + }); + } + + #[test] + fn given_bindings_root_set_it_returns_bindings_root_dir() { + temp_env::with_var("SERVICE_BINDING_ROOT", Some("/bindings"), || { + let root = super::service_binding_root(); + assert!(root.starts_with("/bindings")); + }); + } + + #[test] + fn given_binding_args_it_creates_binding() { + let tmpdir = tempfile::tempdir().unwrap(); + let tmppath = tmpdir.path().to_string_lossy(); + + let bp = BindingProcessor::new(&tmppath, Some("testType"), None, BindingConfirmers::Never); + let res = bp.add_binding("key=val"); + + assert!(res.is_ok()); + assert!(tmpdir.path().join("testType/type").exists()); + assert!(tmpdir.path().join("testType/key").exists()); + + let data = fs::read(tmpdir.path().join("testType/type")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"testType"); + + let data = fs::read(tmpdir.path().join("testType/key")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"val"); + } + + #[test] + fn given_duplicate_binding_key_it_doesnt_overwrite_binding() { + let tmpdir = tempfile::tempdir().unwrap(); + let tmppath = tmpdir.path().to_string_lossy(); + + let bp1 = BindingProcessor::new(&tmppath, Some("testType"), None, BindingConfirmers::Never); + let res = bp1.add_binding("key=val"); + + assert!(res.is_ok()); + assert!(tmpdir.path().join("testType/type").exists()); + assert!(tmpdir.path().join("testType/key").exists()); + + let bp1 = BindingProcessor::new(&tmppath, Some("testType"), None, BindingConfirmers::Never); + let res = bp1.add_binding("key=other_val"); + assert!(res.is_err()); + + let data = fs::read(tmpdir.path().join("testType/type")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"testType"); + + let data = fs::read(tmpdir.path().join("testType/key")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"val"); + } + + #[test] + fn given_duplicate_binding_but_different_key_adds_key_to_binding() { + let tmpdir = tempfile::tempdir().unwrap(); + let tmppath = tmpdir.path().to_string_lossy(); + + let bp1 = BindingProcessor::new(&tmppath, Some("testType"), None, BindingConfirmers::Never); + let res = bp1.add_binding("key=val"); + + assert!(res.is_ok()); + assert!(tmpdir.path().join("testType/type").exists()); + assert!(tmpdir.path().join("testType/key").exists()); + + let bp1 = BindingProcessor::new(&tmppath, Some("testType"), None, BindingConfirmers::Never); + let res = bp1.add_binding("other_key=other_val"); + assert!(res.is_ok()); + assert!(tmpdir.path().join("testType/other_key").exists()); + + let data = fs::read(tmpdir.path().join("testType/type")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"testType"); + + let data = fs::read(tmpdir.path().join("testType/other_key")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"other_val"); + } + + #[test] + fn given_duplicate_binding_and_same_key_confirm_updates_key() { + let tmpdir = tempfile::tempdir().unwrap(); + let tmppath = tmpdir.path().to_string_lossy(); + + let bp1 = BindingProcessor::new(&tmppath, Some("testType"), None, BindingConfirmers::Never); + let res = bp1.add_binding("key=val"); + + assert!(res.is_ok()); + assert!(tmpdir.path().join("testType/type").exists()); + assert!(tmpdir.path().join("testType/key").exists()); + + let bp1 = + BindingProcessor::new(&tmppath, Some("testType"), None, BindingConfirmers::Always); + let res = bp1.add_binding("key=new_val"); + assert!(res.is_ok()); + assert!(tmpdir.path().join("testType/key").exists()); + + let data = fs::read(tmpdir.path().join("testType/type")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"testType"); + + let data = fs::read(tmpdir.path().join("testType/key")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"new_val"); + } + + #[test] + fn given_binding_args_with_name_it_creates_binding_using_name() { + let tmpdir = tempfile::tempdir().unwrap(); + let tmppath = tmpdir.path().to_string_lossy(); + + let bp = BindingProcessor::new( + &tmppath, + Some("testType"), + Some("diff-name"), + BindingConfirmers::Never, + ); + let res = bp.add_binding("key=val"); + + assert!(res.is_ok()); + assert!(tmpdir.path().join("diff-name/type").exists()); + assert!(tmpdir.path().join("diff-name/key").exists()); + + let data = fs::read(tmpdir.path().join("diff-name/type")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"testType"); + + let data = fs::read(tmpdir.path().join("diff-name/key")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"val"); + } + + #[test] + #[serial(requires_cwd)] + fn given_binding_args_with_value_relative_file_creates_binding_using_file_contents() { + let tmpdir = tempfile::tempdir().unwrap(); + let tmppath = tmpdir.path().to_string_lossy(); + + let res = fs::write(tmpdir.path().join("val"), "actual value"); + assert!(res.is_ok()); + + let cur_dir = env::current_dir(); + assert!(res.is_ok()); + + let res = env::set_current_dir(&tmpdir); + assert!(res.is_ok()); + + let bp = BindingProcessor::new(&tmppath, Some("testType"), None, BindingConfirmers::Never); + let res = bp.add_binding("key=@val"); + + { + let res = env::set_current_dir(cur_dir.unwrap()); + assert!(res.is_ok()); + } + + assert!(res.is_ok(), "{}", res.unwrap_err()); + assert!(tmpdir.path().join("testType/type").exists()); + assert!(tmpdir.path().join("testType/key").exists()); + + let data = fs::read(tmpdir.path().join("testType/type")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"testType"); + + let data = fs::read(tmpdir.path().join("testType/key")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"actual value"); + } + + #[test] + fn given_binding_args_with_value_full_file_path_creates_binding_using_file_contents() { + let tmpdir = tempfile::tempdir().unwrap(); + let tmppath = tmpdir.path().to_string_lossy(); + + let res = fs::create_dir_all(tmpdir.path().join("test")); + assert!(res.is_ok()); + + let val_path = tmpdir.path().join("test/val"); + let res = fs::write(tmpdir.path().join("test/val"), "actual value"); + assert!(res.is_ok()); + + let bp = BindingProcessor::new(&tmppath, Some("testType"), None, BindingConfirmers::Never); + let res = bp.add_binding(format!("key=@{}", val_path.to_string_lossy())); + + assert!(res.is_ok(), "{}", res.unwrap_err()); + assert!(tmpdir.path().join("testType/type").exists()); + assert!(tmpdir.path().join("testType/key").exists()); + + let data = fs::read(tmpdir.path().join("testType/type")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"testType"); + + let data = fs::read(tmpdir.path().join("testType/key")); + assert!(data.is_ok()); + assert_eq!(data.unwrap(), b"actual value"); + } + + #[test] + fn given_binding_it_deletes_the_binding() { + let tmpdir = tempfile::tempdir().unwrap(); + let tmppath = tmpdir.path().to_string_lossy(); + + let bp = BindingProcessor::new( + &tmppath, + Some("some-type"), + Some("diff-name"), + BindingConfirmers::Always, + ); + let res = bp.add_binding("key=val"); + + assert!(res.is_ok()); + assert!(tmpdir.path().join("diff-name/type").exists()); + assert!(tmpdir.path().join("diff-name/key").exists()); + + let tmp: Vec<&str> = vec![]; + let res = bp.delete_bindings(tmp.into_iter()); + assert!(res.is_ok()); + assert!(!tmpdir.path().join("diff-name/type").exists()); + assert!(!tmpdir.path().join("diff-name/key").exists()); + } + + #[test] + fn given_a_binding_and_user_declines_it_doesnt_delete_the_binding() { + let tmpdir = tempfile::tempdir().unwrap(); + let tmppath = tmpdir.path().to_string_lossy(); + + let bp = BindingProcessor::new( + &tmppath, + Some("some-type"), + Some("diff-name"), + BindingConfirmers::Never, + ); + let res = bp.add_binding("key=val"); + + assert!(res.is_ok()); + assert!(tmpdir.path().join("diff-name/type").exists()); + assert!(tmpdir.path().join("diff-name/key").exists()); + + let tmp: Vec<&str> = vec![]; + let res = bp.delete_bindings(tmp.into_iter()); + assert!(res.is_err()); + assert!(tmpdir.path().join("diff-name/type").exists()); + assert!(tmpdir.path().join("diff-name/key").exists()); + } + + #[test] + fn given_binding_and_key_it_deletes_the_specific_binding_key_only() { + let tmpdir = tempfile::tempdir().unwrap(); + let tmppath = tmpdir.path().to_string_lossy(); + + let bp = BindingProcessor::new( + &tmppath, + Some("some-type"), + Some("diff-name"), + BindingConfirmers::Always, + ); + let res = bp.add_binding("key1=val1"); + assert!(res.is_ok()); + + let res = bp.add_binding("key2=val2"); + assert!(res.is_ok()); + + assert!(tmpdir.path().join("diff-name/type").exists()); + assert!(tmpdir.path().join("diff-name/key1").exists()); + assert!(tmpdir.path().join("diff-name/key2").exists()); + + let tmp: Vec<&str> = vec!["key1"]; + let res = bp.delete_bindings(tmp.into_iter()); + assert!(res.is_ok()); + assert!(tmpdir.path().join("diff-name/type").exists()); + assert!(!tmpdir.path().join("diff-name/key1").exists()); + assert!(tmpdir.path().join("diff-name/key2").exists()); + } + + #[test] + fn given_binding_and_key_and_user_declines_it_doesnt_delete_the_specific_binding_key() { + let tmpdir = tempfile::tempdir().unwrap(); + let tmppath = tmpdir.path().to_string_lossy(); + + let bp = BindingProcessor::new( + &tmppath, + Some("some-type"), + Some("diff-name"), + BindingConfirmers::Never, + ); + let res = bp.add_binding("key1=val1"); + assert!(res.is_ok()); + + let res = bp.add_binding("key2=val2"); + assert!(res.is_ok()); + + assert!(tmpdir.path().join("diff-name/type").exists()); + assert!(tmpdir.path().join("diff-name/key1").exists()); + assert!(tmpdir.path().join("diff-name/key2").exists()); + + let tmp: Vec<&str> = vec!["key1"]; + let res = bp.delete_bindings(tmp.into_iter()); + assert!(res.is_err()); + assert!(tmpdir.path().join("diff-name/type").exists()); + assert!(tmpdir.path().join("diff-name/key1").exists()); + assert!(tmpdir.path().join("diff-name/key2").exists()); + } + + #[test] + fn given_a_binding_init_outputs_fish_script() { + // check args + let args = args::Parser::new().parse_args(vec!["bt", "init", "fish"]); + let cmd = args.subcommand_matches("init").unwrap(); + let mut tb = TestBuffer::new(); + let res = InitCommandHandler { + output: tb.writer(), + } + .handle(Some(cmd)); + assert!(res.is_ok(), "init handler should succeed"); + assert_eq!( + tb.string().unwrap().trim_end(), + include_str!("scripts/fish.sh") + ); + } + + #[test] + fn given_a_binding_init_outputs_bash_script() { + // check args + let args = args::Parser::new().parse_args(vec!["bt", "init", "bash"]); + let cmd = args.subcommand_matches("init").unwrap(); + let mut tb = TestBuffer::new(); + let res = InitCommandHandler { + output: tb.writer(), + } + .handle(Some(cmd)); + assert!(res.is_ok(), "init handler should succeed"); + assert_eq!( + tb.string().unwrap().trim_end(), + include_str!("scripts/bash.sh"), + ); + } + + #[test] + fn given_a_binding_args_outputs() { + let tmpdir = tempfile::tempdir().unwrap(); + let tmppath = tmpdir.path().to_string_lossy(); + + temp_env::with_var("SERVICE_BINDING_ROOT", Some(tmpdir.as_ref()), || { + // make some bindings, required + + let bp = BindingProcessor::new( + &tmppath, + Some("some-type"), + Some("diff-name"), + BindingConfirmers::Never, + ); + let res = bp.add_binding("key1=val1"); + assert!(res.is_ok()); + + // check args + let args = args::Parser::new().parse_args(vec!["bt", "args", "--docker"]); + let cmd = args.subcommand_matches("args").unwrap(); + let mut tb = TestBuffer::new(); + let res = ArgsCommandHandler { + output: tb.writer(), + } + .handle(Some(cmd)); + assert!(res.is_ok(), "args handler should succeed"); + assert_eq!( + tb.string().unwrap(), + format!( + r#"--volume {}:/bindings --env SERVICE_BINDING_ROOT=/bindings"#, + tmppath + ) + ); + }); + } + + #[test] + fn write_to_test_buffer() { + struct Junk<'t, T> + where + T: Write, + { + output: &'t mut T, + } + + impl<'t, T> Junk<'t, T> + where + T: Write, + { + fn do_stuff(&mut self) { + write!(self.output, "Hello World!").unwrap(); + } + } + + let mut tb = TestBuffer::new(); + let mut j = Junk { + output: tb.writer(), + }; + j.do_stuff(); + assert_eq!(tb.string().unwrap(), "Hello World!"); + + let mut buf = std::io::stdout(); + let mut j = Junk { output: &mut buf }; + j.do_stuff(); + } +} diff --git a/src/deps.rs b/src/deps.rs new file mode 100644 index 0000000..2860960 --- /dev/null +++ b/src/deps.rs @@ -0,0 +1,319 @@ +use anyhow::{anyhow, Context, Result}; +use sha2::{Digest, Sha256}; +use std::fs::File; +use std::io::{self, prelude::*}; +use std::sync::{Arc, Mutex}; +use std::thread::JoinHandle; +use std::{path, thread}; +use toml::Value as Toml; +use url::Url; + +#[derive(Clone)] +pub(super) struct Dependency { + pub(super) sha256: String, + pub(super) uri: String, +} + +impl Dependency { + pub(super) fn filename(&self) -> Result { + Url::parse(&self.uri)? + .path_segments() + .ok_or_else(|| anyhow!("no path segments for {}", &self.uri)) + .map(|s| { + s.last() + .map(|s| s.to_owned()) + .ok_or_else(|| anyhow!("no path for {}", &self.uri)) + })? + } + + pub(super) fn checksum_matches(&self, binding_path: &path::Path) -> Result { + let dest = binding_path.join("binaries").join(self.filename()?); + if !dest.exists() { + return Ok(false); + } + + let mut fp = File::open(&dest).with_context(|| format!("cannot open file {:?}", dest))?; + + let mut hasher = Sha256::new(); + io::copy(&mut fp, &mut hasher)?; + let hash = hex::encode(hasher.finalize()); + + Ok(hash == self.sha256) + } + + pub(super) fn download(&self, agent: &ureq::Agent, binding_path: &path::Path) -> Result<()> { + if self.checksum_matches(binding_path)? { + return Ok(()); + } + + let dest = binding_path.join("binaries").join(self.filename()?); + let mut fp = File::create(&dest).with_context(|| format!("cannot open file {:?}", dest))?; + + let mut reader = agent.get(&self.uri).call()?.into_reader(); + + std::io::copy(&mut reader, &mut fp).with_context(|| "copy failed")?; + Ok(()) + } +} + +pub(super) fn parse_buildpack_toml_from_disk(path: &path::Path) -> Result> { + let mut input = String::new(); + + File::open(path) + .and_then(|mut f| f.read_to_string(&mut input)) + .unwrap(); + + transform(input.parse()?) +} + +pub(super) fn parse_buildpack_toml_from_network(buildpack: &str) -> Result> { + let uri = format!( + "https://raw.githubusercontent.com/{}/main/buildpack.toml", + buildpack + ); + + let agent = configure_agent()?; + let res = agent + .get(&uri) + .call() + .with_context(|| format!("failed on url {}", uri))? + .into_string() + .with_context(|| format!("failed on url {}", uri))?; + + transform(res.parse()?) +} + +pub(super) fn download_dependencies( + deps: Vec, + binding_path: path::PathBuf, +) -> Result<()> { + let max_simult: usize = std::env::var("BT_MAX_SIMULTANEOUS") + .unwrap_or_else(|_| String::from("5")) + .parse()?; + + let agent = Arc::new(configure_agent()?); + let binding_path = Arc::new(binding_path); + let deps = Arc::new(Mutex::new(deps)); + + let mut join_handles: Vec> = vec![]; + + for _i in 0..max_simult { + let agent = Arc::clone(&agent); + let binding_path = Arc::clone(&binding_path); + let deps = Arc::clone(&deps); + + join_handles.push(thread::spawn(move || { + while let Some(d) = deps.lock().expect("unable to get lock").pop() { + match d.download(&agent, &binding_path) { + Ok(_) => (), + Err(err) => panic!("Download of {} failed with error {}", d.uri, err), + } + } + })) + } + + for handle in join_handles { + if let Err(err) = handle.join() { + if let Ok(msg) = err.downcast::() { + return Err(anyhow!("thread panic: {}", msg)); + } + } + } + + Ok(()) +} + +fn configure_agent() -> Result { + let conn_timeout: u64 = std::env::var("BT_CONN_TIMEOUT") + .unwrap_or_else(|_| String::from("5")) + .parse()?; + + let timeout: u64 = std::env::var("BT_REQ_TIMEOUT") + .unwrap_or_else(|_| String::from("30")) + .parse()?; + + Ok(ureq::builder() + .timeout_connect(std::time::Duration::from_secs(conn_timeout)) + .timeout(std::time::Duration::from_secs(timeout)) + .build()) +} + +fn transform(toml: Toml) -> Result> { + let bp_toml = toml + .as_table() + .with_context(|| "buildpack.toml format is invalid")?; + + let metadata = bp_toml + .get("metadata") + .with_context(|| "no metadata present in buildpack.toml")? + .as_table() + .with_context(|| "metadata should be a table")?; + + let deps_metadata = metadata + .get("dependencies") + .with_context(|| "no dependencies present")? + .as_array() + .with_context(|| "dependencies should be an array")?; + + let mut deps = vec![]; + + for d in deps_metadata { + let table = d + .as_table() + .with_context(|| "dependency should be a table")?; + + let sha256 = table + .get("sha256") + .with_context(|| "sha256 field is required")? + .as_str() + .with_context(|| "sha256 should be a string")? + .to_owned(); + let uri = table + .get("uri") + .with_context(|| "uri field is required")? + .as_str() + .with_context(|| "uri should be a string")? + .to_owned(); + + deps.push(Dependency { sha256, uri }) + } + + Ok(deps) +} + +#[cfg(test)] +mod tests { + use super::{transform, Dependency}; + + #[test] + fn dependency_filename() { + assert_eq!( + "filename", + Dependency { + sha256: "".into(), + uri: "https://example.com/filename".into(), + } + .filename() + .unwrap() + ); + } + + #[test] + #[should_panic(expected = "no path segments for")] + fn dependency_filename_no_path() { + assert_eq!( + "filename", + Dependency { + sha256: "".into(), + uri: "data:text/plain,HelloWorld".into(), + } + .filename() + .unwrap() + ); + } + + #[test] + #[should_panic(expected = "no metadata present in buildpack.toml")] + fn transform_no_metadata() { + transform(toml::from_str(r#"foo = "bar""#).unwrap()).unwrap(); + } + + #[test] + #[should_panic(expected = "metadata should be a table")] + fn transform_metadata_not_a_table() { + transform(toml::from_str(r#"metadata = "bar""#).unwrap()).unwrap(); + } + + #[test] + #[should_panic(expected = "no dependencies present")] + fn transform_metadata_not_dependency() { + transform( + toml::from_str( + r#"[[metadata.configurations]] + foo = "bar""#, + ) + .unwrap(), + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "dependencies should be an array")] + fn transform_metadata_dependencies_should_be_an_array() { + transform( + toml::from_str( + r#"[metadata] + dependencies = "foo""#, + ) + .unwrap(), + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "dependency should be a table")] + fn transform_metadata_dependency_should_be_a_table() { + transform( + toml::from_str( + r#"[metadata] + dependencies = [1, 2, 3]"#, + ) + .unwrap(), + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "sha256 field is required")] + fn transform_metadata_dependency_should_have_an_sha256() { + transform( + toml::from_str( + r#"[[metadata.dependencies]] + foo = "bar""#, + ) + .unwrap(), + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "sha256 should be a string")] + fn transform_metadata_dependency_sha256_should_be_str() { + transform( + toml::from_str( + r#"[[metadata.dependencies]] + sha256 = 1"#, + ) + .unwrap(), + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "uri field is required")] + fn transform_metadata_dependency_should_have_an_uri() { + transform( + toml::from_str( + r#"[[metadata.dependencies]] + sha256 = "sha256" + foo = "bar""#, + ) + .unwrap(), + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "uri should be a string")] + fn transform_metadata_dependency_uri_should_be_str() { + transform( + toml::from_str( + r#"[[metadata.dependencies]] + sha256 = "sha256" + uri = 1"#, + ) + .unwrap(), + ) + .unwrap(); + } +} diff --git a/src/help/additional_help_binding.txt b/src/help/additional_help_binding.txt new file mode 100644 index 0000000..e6a0159 --- /dev/null +++ b/src/help/additional_help_binding.txt @@ -0,0 +1,3 @@ +By default bindings will be generated under `./bindings`, +however you may set `SERVICE_BINDING_ROOT` to change this +location. \ No newline at end of file diff --git a/src/help/additional_help.txt b/src/help/additional_help_param.txt similarity index 88% rename from src/help/additional_help.txt rename to src/help/additional_help_param.txt index e78af80..7d3790a 100644 --- a/src/help/additional_help.txt +++ b/src/help/additional_help_param.txt @@ -8,8 +8,8 @@ are accepted. Ex: `-p my_cert=@path/to/my_cert.pem` +All types and param key names must be valid file names. + By default bindings will be generated under `./bindings`, however you may set `SERVICE_BINDING_ROOT` to change this -location. - -All types and param key names must be valid file names. \ No newline at end of file +location. \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 007f243..0dd8c79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,405 +1,6 @@ -use std::ffi::OsString; -use std::io::{prelude::*, stdin}; -use std::{env, fs, path}; +pub mod args; +mod command; +mod deps; -use anyhow::{anyhow, Context, Result}; -use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg, Values}; - -/// Parse application arguments -/// -/// ### Examples -/// -/// Basic: Single parameter without a name -/// -/// ``` -/// let args = binding_tool::parse_args(vec!["bt", "-t", "binding", "-p", "foo=bar"]); -/// -/// assert_eq!(args.value_of("TYPE").unwrap(), "binding"); -/// -/// let params:Vec<_> = args.values_of("PARAM").unwrap().collect(); -/// assert_eq!(params, vec!["foo=bar"]); -/// -/// assert_eq!(args.value_of("NAME"), None); -/// ``` -/// -/// More Advanced: Multiple parameters and a name -/// -/// ``` -/// let args = binding_tool::parse_args(vec!["bt", "-f", "-t", "binding", "-n", "better_name", "-p", "foo=bar", "-p", "gorilla=banana"]); -/// -/// assert_eq!(args.value_of("TYPE").unwrap(), "binding"); -/// -/// let params:Vec<_> = args.values_of("PARAM").unwrap().collect(); -/// assert_eq!(params, vec!["foo=bar", "gorilla=banana"]); -/// -/// assert_eq!(args.value_of("NAME").unwrap(), "better_name"); -/// -/// assert_eq!(args.is_present("FORCE"), true); -/// ``` -/// -pub fn parse_args<'a, I, T>(args: I) -> clap::ArgMatches<'a> -where - I: IntoIterator, - T: Into + Clone, -{ - return App::new(crate_name!()) - .version(crate_version!()) - .author(crate_authors!()) - .about(crate_description!()) - .after_help(include_str!("help/additional_help.txt")) - .arg( - Arg::with_name("FORCE") - .short("f") - .long("force") - .value_name("force") - .takes_value(false) - .help("force update if key exists"), - ) - .arg( - Arg::with_name("TYPE") - .short("t") - .long("type") - .value_name("type") - .help("type of binding") - .required(true), - ) - .arg( - Arg::with_name("PARAM") - .short("p") - .long("param") - .value_name("key=val") - .multiple(true) - .required(true) - .help("key/value to set for the type"), - ) - .arg( - Arg::with_name("NAME") - .short("n") - .long("name") - .value_name("name") - .required(false) - .help("optional name for the binding, name defaults to the type"), - ) - .get_matches_from(args); -} - -pub trait BindingConfirmer { - fn confirm(&self) -> bool; -} - -pub struct ConsoleBindingConfirmer {} - -impl BindingConfirmer for ConsoleBindingConfirmer { - fn confirm(&self) -> bool { - println!("The binding alread exists, do you wish to continue? (yes or no)"); - - let mut input: String = String::new(); - let res = stdin().lock().read_line(&mut input); - let input = input.trim().to_lowercase(); - res.is_ok() && (input == "y" || input == "yes") - } -} - -pub struct TrueBindingConfirmer {} - -impl BindingConfirmer for TrueBindingConfirmer { - fn confirm(&self) -> bool { - true - } -} - -pub struct BindingProcessor<'a, T> -where - T: BindingConfirmer, -{ - bindings_home: &'a str, - binding_type: &'a str, - binding_name: Option<&'a str>, - confirmer: T, -} - -impl<'a, T: BindingConfirmer> BindingProcessor<'a, T> -where - T: BindingConfirmer, -{ - pub fn new( - bindings_home: &'a str, - binding_type: &'a str, - binding_name: Option<&'a str>, - confirmer: T, - ) -> BindingProcessor<'a, T> { - BindingProcessor { - bindings_home, - binding_type, - binding_name, - confirmer, - } - } - - pub fn process_bindings( - self: &BindingProcessor<'a, T>, - binding_key_vals: Values<'a>, - ) -> Result<()> { - for binding_key_val in binding_key_vals.clone() { - self.process_binding(binding_key_val)?; - } - - Ok(()) - } - - fn process_binding>( - self: &BindingProcessor<'a, T>, - binding_key_val: S, - ) -> Result<()> { - let binding_path = path::Path::new(self.bindings_home) - .join(self.binding_name.unwrap_or(self.binding_type)); - - fs::create_dir_all(&binding_path) - .with_context(|| format!("{}", binding_path.to_string_lossy()))?; - - if let Some((binding_key, binding_value)) = binding_key_val.as_ref().split_once("=") { - let binding_key_path = binding_path.join(binding_key); - - if binding_key_path.exists() { - anyhow::ensure!(self.confirmer.confirm(), "binding already exists"); - } - - let mut type_file = fs::File::create(&binding_path.join("type")) - .with_context(|| "cannot open type file")?; - type_file - .write_all(self.binding_type.as_bytes()) - .with_context(|| "cannot write the type file")?; - - if binding_value.starts_with('@') { - let src = binding_value.trim_start_matches('@'); - let src_path = path::Path::new(src) - .canonicalize() - .with_context(|| format!("cannot canonicalize path to source file: {}", src))?; - fs::copy(&src_path, &binding_key_path).with_context(|| { - format!( - "failed to copy {} to {}", - src_path.to_string_lossy(), - binding_key_path.to_string_lossy() - ) - })?; - } else { - let mut binding_file = fs::File::create(&binding_key_path).with_context(|| { - format!( - "cannot open binding key path: {}", - &binding_key_path.to_string_lossy() - ) - })?; - binding_file - .write_all(binding_value.as_bytes()) - .with_context(|| { - format!( - "cannot write to binding key path: {}", - binding_key_path.to_string_lossy() - ) - })?; - } - - Ok(()) - } else { - Err(anyhow!( - "could not parse key/value -> {}", - binding_key_val.as_ref() - )) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - pub struct FalseBindingConfirmer {} - - impl BindingConfirmer for FalseBindingConfirmer { - fn confirm(&self) -> bool { - false - } - } - - #[test] - fn given_binding_args_it_creates_binding() { - let tmpdir = tempfile::tempdir().unwrap(); - let tmppath = tmpdir.path().to_string_lossy(); - - let bp = BindingProcessor::new(&tmppath, "testType", None, FalseBindingConfirmer {}); - let res = bp.process_binding("key=val"); - - assert!(res.is_ok()); - assert!(tmpdir.path().join("testType/type").exists()); - assert!(tmpdir.path().join("testType/key").exists()); - - let data = fs::read(tmpdir.path().join("testType/type")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"testType"); - - let data = fs::read(tmpdir.path().join("testType/key")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"val"); - } - - #[test] - fn given_duplicate_binding_key_it_doesnt_overwrite_binding() { - let tmpdir = tempfile::tempdir().unwrap(); - let tmppath = tmpdir.path().to_string_lossy(); - - let bp1 = BindingProcessor::new(&tmppath, "testType", None, FalseBindingConfirmer {}); - let res = bp1.process_binding("key=val"); - - assert!(res.is_ok()); - assert!(tmpdir.path().join("testType/type").exists()); - assert!(tmpdir.path().join("testType/key").exists()); - - let bp1 = BindingProcessor::new(&tmppath, "testType", None, FalseBindingConfirmer {}); - let res = bp1.process_binding("key=other_val"); - assert!(res.is_err()); - - let data = fs::read(tmpdir.path().join("testType/type")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"testType"); - - let data = fs::read(tmpdir.path().join("testType/key")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"val"); - } - - #[test] - fn given_duplicate_binding_but_different_key_adds_key_to_binding() { - let tmpdir = tempfile::tempdir().unwrap(); - let tmppath = tmpdir.path().to_string_lossy(); - - let bp1 = BindingProcessor::new(&tmppath, "testType", None, FalseBindingConfirmer {}); - let res = bp1.process_binding("key=val"); - - assert!(res.is_ok()); - assert!(tmpdir.path().join("testType/type").exists()); - assert!(tmpdir.path().join("testType/key").exists()); - - let bp1 = BindingProcessor::new(&tmppath, "testType", None, FalseBindingConfirmer {}); - let res = bp1.process_binding("other_key=other_val"); - assert!(res.is_ok()); - assert!(tmpdir.path().join("testType/other_key").exists()); - - let data = fs::read(tmpdir.path().join("testType/type")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"testType"); - - let data = fs::read(tmpdir.path().join("testType/other_key")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"other_val"); - } - - #[test] - fn given_duplicate_binding_and_same_key_confirm_updates_key() { - let tmpdir = tempfile::tempdir().unwrap(); - let tmppath = tmpdir.path().to_string_lossy(); - - let bp1 = BindingProcessor::new(&tmppath, "testType", None, FalseBindingConfirmer {}); - let res = bp1.process_binding("key=val"); - - assert!(res.is_ok()); - assert!(tmpdir.path().join("testType/type").exists()); - assert!(tmpdir.path().join("testType/key").exists()); - - let bp1 = BindingProcessor::new(&tmppath, "testType", None, TrueBindingConfirmer {}); - let res = bp1.process_binding("key=new_val"); - assert!(res.is_ok()); - assert!(tmpdir.path().join("testType/key").exists()); - - let data = fs::read(tmpdir.path().join("testType/type")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"testType"); - - let data = fs::read(tmpdir.path().join("testType/key")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"new_val"); - } - - #[test] - fn given_binding_args_with_name_it_creates_binding_using_name() { - let tmpdir = tempfile::tempdir().unwrap(); - let tmppath = tmpdir.path().to_string_lossy(); - - let bp = BindingProcessor::new( - &tmppath, - "testType", - Some("diff-name"), - FalseBindingConfirmer {}, - ); - let res = bp.process_binding("key=val"); - - assert!(res.is_ok()); - assert!(tmpdir.path().join("diff-name/type").exists()); - assert!(tmpdir.path().join("diff-name/key").exists()); - - let data = fs::read(tmpdir.path().join("diff-name/type")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"testType"); - - let data = fs::read(tmpdir.path().join("diff-name/key")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"val"); - } - - #[test] - fn given_binding_args_with_value_relative_file_creates_binding_using_file_contents() { - let tmpdir = tempfile::tempdir().unwrap(); - let tmppath = tmpdir.path().to_string_lossy(); - - let res = fs::write(tmpdir.path().join("val"), "actual value"); - assert!(res.is_ok()); - - let res = env::set_current_dir(&tmpdir); - assert!(res.is_ok()); - - let bp = BindingProcessor::new(&tmppath, "testType", None, FalseBindingConfirmer {}); - let res = bp.process_binding("key=@val"); - - assert!(res.is_ok(), "{}", res.unwrap_err()); - assert!(tmpdir.path().join("testType/type").exists()); - assert!(tmpdir.path().join("testType/key").exists()); - - let data = fs::read(tmpdir.path().join("testType/type")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"testType"); - - let data = fs::read(tmpdir.path().join("testType/key")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"actual value"); - } - - #[test] - fn given_binding_args_with_value_full_file_path_creates_binding_using_file_contents() { - let tmpdir = tempfile::tempdir().unwrap(); - let tmppath = tmpdir.path().to_string_lossy(); - - let res = env::set_current_dir(&tmpdir); - assert!(res.is_ok()); - - let res = fs::create_dir_all(tmpdir.path().join("test")); - assert!(res.is_ok()); - - let val_path = tmpdir.path().join("test/val"); - let res = fs::write(tmpdir.path().join("test/val"), "actual value"); - assert!(res.is_ok()); - - let bp = BindingProcessor::new(&tmppath, "testType", None, FalseBindingConfirmer {}); - let res = bp.process_binding(format!("key=@{}", val_path.to_string_lossy())); - - assert!(res.is_ok(), "{}", res.unwrap_err()); - assert!(tmpdir.path().join("testType/type").exists()); - assert!(tmpdir.path().join("testType/key").exists()); - - let data = fs::read(tmpdir.path().join("testType/type")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"testType"); - - let data = fs::read(tmpdir.path().join("testType/key")); - assert!(data.is_ok()); - assert_eq!(data.unwrap(), b"actual value"); - } -} +#[doc(hidden)] +pub use command::BT; diff --git a/src/scripts/bash.sh b/src/scripts/bash.sh new file mode 100644 index 0000000..2c60745 --- /dev/null +++ b/src/scripts/bash.sh @@ -0,0 +1,21 @@ +function docker { + DOCKER=$(which docker) + + if [ "$1" == "run" ]; then + shift + $DOCKER run $(bt args -d) "$@" + else + $DOCKER "$@" + fi +} + +function pack { + PACK=$(which pack) + + if [ "$1" == "build" ]; then + shift + $PACK build $(bt args -p) "$@" + else + $PACK "$@" + fi +} \ No newline at end of file diff --git a/src/scripts/fish.sh b/src/scripts/fish.sh new file mode 100644 index 0000000..34276c9 --- /dev/null +++ b/src/scripts/fish.sh @@ -0,0 +1,17 @@ +function docker; + set DOCKER (which docker); + if test "$argv[1]" = "run"; + $DOCKER run (bt args -d | string split -n ' ') $argv[2..]; + else; + $DOCKER $argv[1..]; + end; +end; + +function pack; + set PACK (which pack); + if test "$argv[1]" = "build"; + $PACK build $argv[2..] (bt args -p | string split -n ' '); + else; + $PACK $argv[1..]; + end; +end; \ No newline at end of file