From a8018a82d5247df14ba1694d5c4bfea9f805c131 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Thu, 29 Sep 2022 12:39:51 -0700 Subject: [PATCH 01/36] deploy to the Cloud Signed-off-by: Matthew Fisher --- Cargo.lock | 897 ++++++++++++++---- Cargo.toml | 4 + crates/cloud/Cargo.toml | 42 + crates/cloud/src/client.rs | 249 +++++ crates/cloud/src/config.rs | 9 + crates/cloud/src/lib.rs | 4 + crates/cloud/src/registry.rs | 5 + crates/cloud/src/registry/bindle.rs | 138 +++ .../src/registry/bindle/bindle_pusher.rs | 37 + .../src/registry/bindle/bindle_writer.rs | 158 +++ crates/cloud/src/registry/bindle/expander.rs | 420 ++++++++ src/commands/deploy.rs | 156 ++- src/opts.rs | 2 + 13 files changed, 1905 insertions(+), 216 deletions(-) create mode 100644 crates/cloud/Cargo.toml create mode 100644 crates/cloud/src/client.rs create mode 100644 crates/cloud/src/config.rs create mode 100644 crates/cloud/src/lib.rs create mode 100644 crates/cloud/src/registry.rs create mode 100644 crates/cloud/src/registry/bindle.rs create mode 100644 crates/cloud/src/registry/bindle/bindle_pusher.rs create mode 100644 crates/cloud/src/registry/bindle/bindle_writer.rs create mode 100644 crates/cloud/src/registry/bindle/expander.rs diff --git a/Cargo.lock b/Cargo.lock index 6787377fed..9812695fe9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,9 +36,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ "memchr", ] @@ -75,9 +75,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.63" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26fa4d7e3f2eebadf743988fc8aec9fa9a9e82611acafd77c1462ed6262440a" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "anymap2" @@ -190,11 +190,11 @@ dependencies = [ "oauth2", "rand 0.7.3", "reqwest", - "semver 1.0.13", + "semver 1.0.14", "serde", "serde_cbor", "serde_json", - "sha2 0.10.3", + "sha2 0.10.6", "sled", "tempfile", "thiserror", @@ -215,6 +215,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.9.0" @@ -226,9 +232,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] @@ -276,9 +282,9 @@ checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] name = "cap-fs-ext" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e142bbbe9d5d6a2dd0387f887a000b41f4c82fb1226316dfb4cc8dbc3b1a29" +checksum = "438ca7f5bb15c799ea146429e4f8b7bfd25ff1eb05319024549a7728de45800c" dependencies = [ "cap-primitives", "cap-std", @@ -288,12 +294,11 @@ dependencies = [ [[package]] name = "cap-primitives" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f22f4975282dd4f2330ee004f001c4e22f420da9fb474ea600e9af330f1e548" +checksum = "ba063daa90ed40882bb288ac4ecaa942d655d15cf74393d41d2267b5d7daf120" dependencies = [ "ambient-authority", - "errno", "fs-set-times", "io-extras", "io-lifetimes", @@ -307,9 +312,9 @@ dependencies = [ [[package]] name = "cap-rand" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef643f8defef7061c395bb3721b6a80d39c1baaa8ee2e42edf2917fa05584e7f" +checksum = "c720808e249f0ae846ec647fe48cef3cea67e4e5026cf869c041c278b7dcae45" dependencies = [ "ambient-authority", "rand 0.8.5", @@ -317,9 +322,9 @@ dependencies = [ [[package]] name = "cap-std" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95624bb0abba6b6ff6fad2e02a7d3945d093d064ac5a3477a308c29fbe3bfd49" +checksum = "0e3a603c9f3bd2181ed128ab3cd32fbde7cff76afc64a3576662701c4aee7e2b" dependencies = [ "cap-primitives", "io-extras", @@ -330,9 +335,9 @@ dependencies = [ [[package]] name = "cap-time-ext" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a2d284862edf6e431e9ad4e109c02855157904cebaceae6f042b124a1a21e2" +checksum = "da76e64f3e46f8c8479e392a7fe3faa2e76b8c1cea4618bae445276fdec12082" dependencies = [ "cap-primitives", "once_cell", @@ -363,6 +368,12 @@ dependencies = [ "jobserver", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.0" @@ -407,9 +418,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.19" +version = "3.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68d43934757334b5c0519ff882e1ab9647ac0258b47c24c4f490d78e42697fd5" +checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" dependencies = [ "atty", "bitflags", @@ -419,7 +430,7 @@ dependencies = [ "once_cell", "strsim", "termcolor", - "textwrap 0.15.0", + "textwrap 0.15.1", ] [[package]] @@ -444,6 +455,65 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clipboard-win" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" +dependencies = [ + "lazy-bytes-cast", + "winapi", +] + +[[package]] +name = "cloud" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bindle", + "clap 3.2.22", + "cloud-openapi", + "colored", + "dialoguer 0.9.0", + "dirs 4.0.0", + "dunce", + "env_logger", + "futures", + "glob", + "itertools", + "log", + "mime_guess", + "path-absolutize", + "regex", + "reqwest", + "semver 1.0.14", + "serde", + "serde_json", + "sha2 0.9.9", + "spin-loader", + "spin-publish", + "tempfile", + "tokio", + "tokio-util 0.7.4", + "toml", + "tracing", + "uuid", +] + +[[package]] +name = "cloud-openapi" +version = "0.1.0" +source = "git+https://github.com/fermyon/cloud-openapi#2eba9b67688771d9d44a065a3c437ae261f662d8" +dependencies = [ + "reqwest", + "serde", + "serde_derive", + "serde_json", + "url", + "uuid", +] + [[package]] name = "colored" version = "2.0.0" @@ -466,7 +536,7 @@ dependencies = [ "memchr", "pin-project-lite", "tokio", - "tokio-util 0.7.3", + "tokio-util 0.7.4", ] [[package]] @@ -483,18 +553,32 @@ dependencies = [ [[package]] name = "console" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" dependencies = [ "encode_unicode", + "lazy_static", "libc", - "once_cell", "terminal_size", "unicode-width", "winapi", ] +[[package]] +name = "copypasta" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7216b5c1e9ad3867252505995b02d01c6fa7e6db0d8abd42634352ef377777e" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "smithay-clipboard", + "x11-clipboard", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -522,9 +606,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] @@ -695,26 +779,24 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", - "once_cell", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -797,6 +879,41 @@ dependencies = [ "zeroize", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dialoguer" version = "0.9.0" @@ -831,11 +948,11 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ - "block-buffer 0.10.2", + "block-buffer 0.10.3", "crypto-common", "subtle", ] @@ -890,6 +1007,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "dlib" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +dependencies = [ + "libloading", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -898,13 +1024,19 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dotenvy" -version = "0.15.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3db6fcad7c1fc4abdd99bf5276a4db30d6a819127903a709ed41e5ff016e84" +checksum = "ed9155c8f4dc55c7470ae9da3f63c6785245093b3f6aeb0f5bf2e968efbba314" dependencies = [ "dirs 4.0.0", ] +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + [[package]] name = "dunce" version = "1.0.2" @@ -977,9 +1109,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" dependencies = [ "atty", "humantime", @@ -1079,11 +1211,10 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] @@ -1303,7 +1434,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.3", + "tokio-util 0.7.4", "tracing", ] @@ -1357,9 +1488,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897cd85af6387be149f55acf168e41be176a02de7872403aaab184afc2f327e6" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] @@ -1371,7 +1502,7 @@ source = "git+https://github.com/deislabs/hippo-cli?tag=v0.16.1#73315f55fadd2bb0 dependencies = [ "anyhow", "async-trait", - "clap 3.2.19", + "clap 3.2.22", "colored", "dialoguer 0.9.0", "dirs 4.0.0", @@ -1414,7 +1545,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -1509,14 +1640,13 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.47" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c495f162af0bf17656d0014a0eded5f3cd2f365fdd204548c2869db89359dc7" +checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0" dependencies = [ "android_system_properties", "core-foundation-sys", "js-sys", - "once_cell", "wasm-bindgen", "winapi", ] @@ -1527,13 +1657,18 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] @@ -1590,7 +1725,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d508111813f9af3afd2f92758f77e4ed2cc9371b642112c6a48d22eb73105c5" dependencies = [ - "hermit-abi 0.2.5", + "hermit-abi 0.2.6", "io-lifetimes", "rustix", "windows-sys", @@ -1598,9 +1733,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] @@ -1626,20 +1761,40 @@ dependencies = [ "cc", ] +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -1667,6 +1822,12 @@ dependencies = [ "serde", ] +[[package]] +name = "lazy-bytes-cast" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" + [[package]] name = "lazy_static" version = "1.4.0" @@ -1681,9 +1842,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.132" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "libgit2-sys" @@ -1697,6 +1858,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libz-sys" version = "1.1.8" @@ -1775,9 +1946,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", @@ -1810,6 +1981,15 @@ dependencies = [ "libc", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1819,12 +1999,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "maybe-owned" version = "0.3.4" @@ -1833,11 +2007,11 @@ checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" [[package]] name = "md-5" -version = "0.10.2" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "274fd6bd98a3c75c9515d9393b063099f60f9b47f09ee20a34fd76287fd017f4" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -1855,6 +2029,15 @@ dependencies = [ "rustix", ] +[[package]] +name = "memmap2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95af15f345b17af2efc8ead6080fb8bc376f8cec1b35277b935637595fe77498" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.6.5" @@ -1880,11 +2063,17 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] @@ -1925,6 +2114,62 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-glue" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d0c4a7b83860226e6b4183edac21851f05d5a51756e97a1144b7f5a6b63e65f" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + [[package]] name = "nix" version = "0.24.2" @@ -1949,6 +2194,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-bigint" version = "0.4.3" @@ -1989,6 +2244,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num_threads" version = "0.1.6" @@ -2013,11 +2289,40 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", - "sha2 0.10.3", + "sha2 0.10.6", "thiserror", "url", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.28.4" @@ -2041,9 +2346,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "oorandom" @@ -2059,9 +2364,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.41" +version = "0.10.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" +checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" dependencies = [ "bitflags", "cfg-if", @@ -2091,9 +2396,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.75" +version = "0.9.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" +checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce" dependencies = [ "autocfg", "cc", @@ -2258,15 +2563,15 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4" +checksum = "dbc7bc69c062e492337d74d59b120c274fd3d261b6bf6d3207d499b4b379c41a" dependencies = [ "thiserror", "ucd-trie", @@ -2274,9 +2579,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "905708f7f674518498c1f8d644481440f476d39ca6ecae83319bba7c6c12da91" +checksum = "60b75706b9642ebcb34dab3bc7750f811609a0eb1dd8b88c2d15bf628c1c65b2" dependencies = [ "pest", "pest_generator", @@ -2284,9 +2589,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5803d8284a629cc999094ecd630f55e91b561a1d1ba75e233b00ae13b91a69ad" +checksum = "f4f9272122f5979a6511a749af9db9bfc810393f63119970d7085fed1c4ea0db" dependencies = [ "pest", "pest_meta", @@ -2297,13 +2602,13 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1538eb784f07615c6d9a8ab061089c6c54a344c5b4301db51990ca1c241e8c04" +checksum = "4c8717927f9b79515e565a64fe46c38b8cd0427e64c40680b14a7365ab09ac8d" dependencies = [ "once_cell", "pest", - "sha-1", + "sha1 0.10.5", ] [[package]] @@ -2364,9 +2669,9 @@ checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "plotters" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "716b4eeb6c4a1d3ecc956f75b43ec2e8e8ba80026413e70a3f41fd3313d3492b" +checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" dependencies = [ "num-traits", "plotters-backend", @@ -2404,7 +2709,7 @@ dependencies = [ "md-5", "memchr", "rand 0.8.5", - "sha2 0.10.3", + "sha2 0.10.6", "stringprep", ] @@ -2425,6 +2730,17 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "proc-macro-crate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +dependencies = [ + "once_cell", + "thiserror", + "toml", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2457,9 +2773,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] @@ -2490,9 +2806,9 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f446d0a6efba22928558c4fb4ce0b3fd6c89b0061343e390bf01a703742b8125" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" dependencies = [ "cc", ] @@ -2508,6 +2824,15 @@ dependencies = [ "unicase", ] +[[package]] +name = "quick-xml" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.21" @@ -2538,7 +2863,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2558,7 +2883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -2572,9 +2897,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.7", ] @@ -2626,9 +2951,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "ryu", - "sha1", + "sha1 0.6.1", "tokio", - "tokio-util 0.7.3", + "tokio-util 0.7.4", "url", ] @@ -2713,9 +3038,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" dependencies = [ "base64", "bytes", @@ -2730,11 +3055,11 @@ dependencies = [ "hyper-tls", "ipnet", "js-sys", - "lazy_static", "log", "mime", "mime_guess", "native-tls", + "once_cell", "percent-encoding", "pin-project-lite", "rustls", @@ -2745,7 +3070,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", - "tokio-util 0.7.3", + "tokio-util 0.7.4", "tower-service", "url", "wasm-bindgen", @@ -2778,9 +3103,9 @@ checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] name = "rustix" -version = "0.35.9" +version = "0.35.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c825b8aa8010eb9ee99b75f05e10180b9278d161583034d7574c9d617aeada" +checksum = "fbb2fda4666def1433b1b05431ab402e42a1084285477222b72d6c564c417cef" dependencies = [ "bitflags", "errno", @@ -2863,6 +3188,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + [[package]] name = "scopeguard" version = "1.1.0" @@ -2914,9 +3245,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" dependencies = [ "serde", ] @@ -2932,9 +3263,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] @@ -2951,9 +3282,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -2993,23 +3324,23 @@ dependencies = [ ] [[package]] -name = "sha-1" -version = "0.10.0" +name = "sha1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.3", + "sha1_smol", ] [[package]] name = "sha1" -version = "0.6.1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ - "sha1_smol", + "cfg-if", + "cpufeatures", + "digest 0.10.5", ] [[package]] @@ -3033,13 +3364,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.3" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899bf02746a2c92bf1053d9327dadb252b01af1f81f90cdb902411f518bc7215" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -3092,9 +3423,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.6.0" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ea32af43239f0d353a7dd75a22d94c329c8cdaafdcb4c1c1335aa10c298a4a" +checksum = "deb766570a2825fa972bceff0d195727876a9cdf2460ab2e52d455dc2de47fd9" [[package]] name = "simple_asn1" @@ -3147,9 +3478,37 @@ checksum = "03b634d87b960ab1a38c4fe143b508576f075e7c978bfad18217645ebfdfa2ec" [[package]] name = "smallvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "smithay-client-toolkit" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f307c47d32d2715eb2e0ece5589057820e0e5e70d07c247d1063e844e107f454" +dependencies = [ + "bitflags", + "dlib", + "lazy_static", + "log", + "memmap2", + "nix 0.24.2", + "pkg-config", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + +[[package]] +name = "smithay-clipboard" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a345c870a1fae0b1b779085e81b51e614767c239e93503588e54c5b17f4b0e8" +dependencies = [ + "smithay-client-toolkit", + "wayland-client", +] [[package]] name = "socket2" @@ -3173,10 +3532,10 @@ version = "0.5.0" dependencies = [ "anyhow", "cap-std", - "clap 3.2.19", + "clap 3.2.22", "rand 0.8.5", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", "serde", "serde_json", "tempfile", @@ -3222,8 +3581,11 @@ dependencies = [ "bindle", "bytes", "cargo-target-dep", - "clap 3.2.19", + "clap 3.2.22", + "cloud", + "cloud-openapi", "comfy-table", + "copypasta", "ctrlc", "dialoguer 0.10.2", "dirs 4.0.0", @@ -3241,10 +3603,10 @@ dependencies = [ "path-absolutize", "regex", "reqwest", - "semver 1.0.13", + "semver 1.0.14", "serde", "serde_json", - "sha2 0.10.3", + "sha2 0.10.6", "spin-build", "spin-config", "spin-http", @@ -3264,6 +3626,7 @@ dependencies = [ "uuid", "vergen", "wasmtime", + "webbrowser", "which", ] @@ -3304,7 +3667,7 @@ version = "0.2.0" dependencies = [ "anyhow", "async-trait", - "clap 3.2.19", + "clap 3.2.22", "criterion", "futures", "futures-util", @@ -3344,7 +3707,7 @@ dependencies = [ "regex", "reqwest", "serde", - "sha2 0.10.3", + "sha2 0.10.6", "spin-config", "spin-manifest", "tempfile", @@ -3391,10 +3754,10 @@ dependencies = [ "flate2", "log", "reqwest", - "semver 1.0.13", + "semver 1.0.14", "serde", "serde_json", - "sha2 0.10.3", + "sha2 0.10.6", "tar", "tempfile", "thiserror", @@ -3415,9 +3778,9 @@ dependencies = [ "mime_guess", "path-absolutize", "reqwest", - "semver 1.0.13", + "semver 1.0.14", "serde", - "sha2 0.10.3", + "sha2 0.10.6", "spin-loader", "tempfile", "tokio", @@ -3478,9 +3841,9 @@ dependencies = [ "path-absolutize", "pathdiff", "regex", - "semver 1.0.13", + "semver 1.0.14", "serde", - "sha2 0.10.3", + "sha2 0.10.6", "symlink", "tempfile", "tokio", @@ -3528,7 +3891,7 @@ version = "0.2.0" dependencies = [ "anyhow", "async-trait", - "clap 3.2.19", + "clap 3.2.22", "ctrlc", "dirs 4.0.0", "futures", @@ -3616,9 +3979,9 @@ checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", @@ -3714,24 +4077,24 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" [[package]] name = "thiserror" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0a539a918745651435ac7db7a18761589a94cd7e94cd56999f828bf73c8a57" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c251e90f708e16c49a16f4917dc2131e75222b72edfa9cb7f7c58ae56aae0c09" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -3818,9 +4181,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.20.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg", "bytes", @@ -3828,7 +4191,6 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", @@ -3879,7 +4241,7 @@ dependencies = [ "postgres-types", "socket2", "tokio", - "tokio-util 0.7.3", + "tokio-util 0.7.4", ] [[package]] @@ -3895,9 +4257,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af" dependencies = [ "futures-core", "pin-project-lite", @@ -3935,9 +4297,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ "bytes", "futures-core", @@ -4049,9 +4411,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "unicase" @@ -4070,36 +4432,36 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "unicode-normalization" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "untrusted" @@ -4109,13 +4471,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", "serde", ] @@ -4264,9 +4625,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4274,9 +4635,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", "log", @@ -4289,9 +4650,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" dependencies = [ "cfg-if", "js-sys", @@ -4301,9 +4662,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4311,9 +4672,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -4324,15 +4685,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "wasm-encoder" -version = "0.16.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d443c5a7daae71697d97ec12ad70b4fe8766d3a0f4db16158ac8b781365892f7" +checksum = "c64ac98d5d61192cc45c701b7e4bd0b9aff91e2edfc7a088406cfe2288581e2c" dependencies = [ "leb128", ] @@ -4556,9 +4917,9 @@ dependencies = [ [[package]] name = "wast" -version = "46.0.0" +version = "47.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0ab19660e3ea6891bba69167b9be40fad00fb1fe3dd39c5eebcee15607131b" +checksum = "02b98502f3978adea49551e801a6687678e6015317d7d9470a67fe813393f2a8" dependencies = [ "leb128", "memchr", @@ -4568,23 +4929,110 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f775282def4d5bffd94d60d6ecd57bfe6faa46171cdbf8d32bd5458842b1e3e" +checksum = "7aab4e20c60429fbba9670a6cae0fff9520046ba0aa3e6d0b1cd2653bea14898" dependencies = [ - "wast 46.0.0", + "wast 47.0.1", +] + +[[package]] +name = "wayland-client" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" +dependencies = [ + "bitflags", + "downcast-rs", + "libc", + "nix 0.24.2", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys", +] + +[[package]] +name = "wayland-commons" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" +dependencies = [ + "nix 0.24.2", + "once_cell", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-cursor" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" +dependencies = [ + "nix 0.24.2", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" +dependencies = [ + "bitflags", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" +dependencies = [ + "dlib", + "lazy_static", + "pkg-config", ] [[package]] name = "web-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "webbrowser" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a3cffdb686fbb24d9fb8f03a213803277ed2300f11026a3afe1f108dc021b" +dependencies = [ + "jni", + "ndk-glue", + "url", + "web-sys", + "widestring", + "winapi", +] + [[package]] name = "webpki" version = "0.22.0" @@ -4597,9 +5045,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.4" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" dependencies = [ "webpki", ] @@ -4615,6 +5063,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + [[package]] name = "wiggle" version = "0.39.1" @@ -4859,6 +5313,15 @@ dependencies = [ "wast 35.0.2", ] +[[package]] +name = "x11-clipboard" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a7468a5768fea473e6c8c0d4b60d6d7001a64acceaac267207ca0281e1337e8" +dependencies = [ + "xcb", +] + [[package]] name = "xattr" version = "0.2.3" @@ -4868,6 +5331,32 @@ dependencies = [ "libc", ] +[[package]] +name = "xcb" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b127bf5bfe9dbb39118d6567e3773d4bbc795411a8e1ef7b7e056bccac0011a9" +dependencies = [ + "bitflags", + "libc", + "quick-xml", +] + +[[package]] +name = "xcursor" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" +dependencies = [ + "nom", +] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + [[package]] name = "zeroize" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 55585590e3..81b93b95a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,10 @@ atty = "0.2" bindle = { git = "https://github.com/fermyon/bindle", tag = "v0.8.1", default-features = false, features = ["client"] } bytes = "1.1" clap = { version = "3.1.15", features = ["derive", "env"] } +cloud = { path = "crates/cloud" } +cloud-openapi = { git = "https://github.com/fermyon/cloud-openapi" } comfy-table = "5.0" +copypasta = "0.8.1" ctrlc = { version = "3.2", features = ["termination"] } dialoguer = "0.10" dirs = "4.0" @@ -49,6 +52,7 @@ tracing-subscriber = { version = "0.3.7", features = [ "env-filter" ] } url = "2.2.2" uuid = "^1.0" wasmtime = "0.39.1" +webbrowser = "0.7.1" [target.'cfg(target_os = "linux")'.dependencies] # This needs to be an explicit dependency to enable diff --git a/crates/cloud/Cargo.toml b/crates/cloud/Cargo.toml new file mode 100644 index 0000000000..e0a0c39a13 --- /dev/null +++ b/crates/cloud/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "cloud" +version = "0.1.0" +edition = "2021" +authors = [ "Fermyon Engineering " ] + +[dependencies] +anyhow = "1.0" +async-trait = "0.1" +clap = { version = "3.0", features = ["derive", "env"] } +colored = "2.0.0" +dialoguer = "0.9" +dirs = "4.0" +dunce = "1.0" +env_logger = "0.9" +futures = "0.3.14" +glob = "0.3.0" +cloud-openapi = { git = "https://github.com/fermyon/cloud-openapi" } +itertools = "0.10.0" +log = "0.4" +mime_guess = { version = "2.0" } +path-absolutize = "3.0.11" +regex = "1.5" +reqwest = { version = "0.11", features = ["stream"] } +semver = "1.0" +serde = {version = "1.0", features = ["derive"]} +serde_json = "1.0" +sha2 = "0.9" +spin-loader = { path = "../loader" } +spin-publish = { path = "../publish" } +tempfile = "3.3.0" +tokio = { version = "1.17", features = ["full"] } +tokio-util = { version = "0.7.3", features = ["codec"] } +tracing = { version = "0.1", features = [ "log" ] } +toml = "0.5" +uuid = "1" + +[dependencies.bindle] +git = "https://github.com/fermyon/bindle" +tag = "v0.8.1" +default-features = false +features = ["client"] diff --git a/crates/cloud/src/client.rs b/crates/cloud/src/client.rs new file mode 100644 index 0000000000..9911c576ba --- /dev/null +++ b/crates/cloud/src/client.rs @@ -0,0 +1,249 @@ +use anyhow::{Context, Result}; +use cloud_openapi::{ + apis::{ + accounts_api::api_accounts_post, + apps_api::{api_apps_get, api_apps_id_delete, api_apps_post}, + auth_tokens_api::api_auth_tokens_post, + channels_api::{ + api_channels_get, api_channels_id_delete, api_channels_id_get, + api_channels_id_logs_get, api_channels_post, + }, + configuration::{ApiKey, Configuration}, + device_codes_api::api_device_codes_post, + Error, + }, + models::{ + AppItemPage, ChannelItem, ChannelItemPage, ChannelRevisionSelectionStrategy, + CreateAccountCommand, CreateAppCommand, CreateChannelCommand, CreateDeviceCodeCommand, + CreateTokenCommand, DeviceCodeItem, GetChannelLogsVm, TokenInfo, + }, +}; +use reqwest::header; +use semver::BuildMetadata; +use serde::Deserialize; +use std::{collections::HashMap, path::Path}; +use tracing::log; +use uuid::Uuid; + +use crate::config::ConnectionConfig; + +const JSON_MIME_TYPE: &str = "application/json"; + +pub struct Client { + configuration: Configuration, +} + +impl Client { + pub fn new(conn_info: ConnectionConfig) -> Self { + let mut headers = header::HeaderMap::new(); + headers.insert(header::ACCEPT, JSON_MIME_TYPE.parse().unwrap()); + headers.insert(header::CONTENT_TYPE, JSON_MIME_TYPE.parse().unwrap()); + + let base_path = match conn_info.url.strip_suffix('/') { + Some(s) => s.to_owned(), + None => conn_info.url, + }; + + let configuration = Configuration { + base_path, + user_agent: Some(format!( + "{}/{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )), + client: reqwest::Client::builder() + .danger_accept_invalid_certs(conn_info.insecure) + .default_headers(headers) + .build() + .unwrap(), + basic_auth: None, + oauth_access_token: None, + bearer_access_token: None, + api_key: conn_info.token.token.map(|t| ApiKey { + prefix: Some("Bearer".to_owned()), + key: t, + }), + }; + + Self { configuration } + } + + pub async fn create_device_code(&self, client_id: Uuid) -> Result { + api_device_codes_post( + &self.configuration, + Some(CreateDeviceCodeCommand { client_id }), + ) + .await + .map_err(format_response_error) + } + + pub async fn register(&self, username: String, password: String) -> Result { + api_accounts_post( + &self.configuration, + Some(CreateAccountCommand { + user_name: username, + password, + }), + ) + .await + .map_err(format_response_error) + } + + pub async fn login(&self, token: String) -> Result { + // When the new OpenAPI specification is released, manually crafting + // the request should no longer be necessary. + let response = self + .configuration + .client + .post(format!("{}/api/auth-tokens", self.configuration.base_path)) + .body( + serde_json::json!( + { + "provider": "DeviceFlow", + "clientId": "583e63e9-461f-4fbe-a246-23e0fb1cad10", + "providerCode": token, + } + ) + .to_string(), + ) + .send() + .await?; + + serde_json::from_reader(response.bytes().await?.as_ref()) + .context("Failed to parse response") + } + + pub async fn create_application( + &self, + name: Option, + path: impl AsRef, + buildinfo: Option, + connection: ConnectionConfig, + ) -> Result { + let (storage_name, version) = super::registry::publish(path, buildinfo, connection).await?; + let name = match name { + Some(n) => n, + None => storage_name, + }; + + let app = self.add_app(&name, &name).await?; + let id = self + .add_channel( + app, + name.to_owned(), + None, + ChannelRevisionSelectionStrategy::UseRangeRule, + None, + None, + None, + ) + .await?; + + println!("Deployed {} version {}", name.clone(), version); + let channel = self.get_channel_by_id(&id.to_string()).await?; + println!("Application is running at {}", channel.domain); + + Ok(app) + } + + pub(crate) async fn add_app(&self, name: &str, storage_id: &str) -> Result { + api_apps_post( + &self.configuration, + Some(CreateAppCommand { + name: name.to_string(), + storage_id: storage_id.to_string(), + }), + ) + .await + .map_err(format_response_error) + } + + pub(crate) async fn remove_app(&self, id: String) -> Result<()> { + api_apps_id_delete(&self.configuration, &id) + .await + .map_err(format_response_error) + } + + pub(crate) async fn list_apps(&self) -> Result { + api_apps_get(&self.configuration, None, None, None, None, None) + .await + .map_err(format_response_error) + } + + pub(crate) async fn get_channel_by_id(&self, id: &str) -> Result { + api_channels_id_get(&self.configuration, id) + .await + .map_err(format_response_error) + } + + pub(crate) async fn list_channels(&self) -> Result { + api_channels_get( + &self.configuration, + Some(""), + None, + None, + Some("Name"), + None, + ) + .await + .map_err(format_response_error) + } + + #[allow(clippy::too_many_arguments)] + pub async fn add_channel( + &self, + app_id: Uuid, + name: String, + domain: Option, + revision_selection_strategy: ChannelRevisionSelectionStrategy, + range_rule: Option, + active_revision_id: Option, + certificate_id: Option, + ) -> anyhow::Result { + let command = CreateChannelCommand { + app_id, + name, + domain, + revision_selection_strategy, + range_rule, + active_revision_id, + certificate_id, + }; + api_channels_post(&self.configuration, Some(command)) + .await + .map_err(format_response_error) + } + + pub(crate) async fn remove_channel(&self, id: String) -> Result<()> { + api_channels_id_delete(&self.configuration, &id) + .await + .map_err(format_response_error) + } + + pub(crate) async fn channel_logs(&self, id: String) -> Result { + api_channels_id_logs_get(&self.configuration, &id) + .await + .map_err(format_response_error) + } +} + +#[derive(Deserialize, Debug)] +struct ValidationExceptionMessage { + title: String, + errors: HashMap>, +} + +fn format_response_error(e: Error) -> anyhow::Error { + match e { + Error::ResponseError(r) => { + match serde_json::from_str::(&r.content) { + Ok(m) => anyhow::anyhow!("{} {:?}", m.title, m.errors), + _ => anyhow::anyhow!(r.content), + } + } + Error::Serde(err) => { + anyhow::anyhow!(format!("could not parse JSON object: {}", err)) + } + _ => anyhow::anyhow!(e.to_string()), + } +} diff --git a/crates/cloud/src/config.rs b/crates/cloud/src/config.rs new file mode 100644 index 0000000000..6cc258104a --- /dev/null +++ b/crates/cloud/src/config.rs @@ -0,0 +1,9 @@ +use cloud_openapi::models::TokenInfo; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct ConnectionConfig { + pub insecure: bool, + pub token: TokenInfo, + pub url: String, +} diff --git a/crates/cloud/src/lib.rs b/crates/cloud/src/lib.rs new file mode 100644 index 0000000000..f576d2f855 --- /dev/null +++ b/crates/cloud/src/lib.rs @@ -0,0 +1,4 @@ +#[allow(unused)] +pub mod client; +pub mod config; +pub mod registry; diff --git a/crates/cloud/src/registry.rs b/crates/cloud/src/registry.rs new file mode 100644 index 0000000000..edd2158d1e --- /dev/null +++ b/crates/cloud/src/registry.rs @@ -0,0 +1,5 @@ +pub mod bindle; + +// Currently, the default way of publishing an application to the Fermyon +// platform is using the Platform's Bindle server. +pub use self::bindle::publish; diff --git a/crates/cloud/src/registry/bindle.rs b/crates/cloud/src/registry/bindle.rs new file mode 100644 index 0000000000..9ef2715643 --- /dev/null +++ b/crates/cloud/src/registry/bindle.rs @@ -0,0 +1,138 @@ +#![deny(missing_docs)] + +//! Functions for publishing Spin applications& to Bindle. + +mod bindle_pusher; +mod bindle_writer; +mod expander; + +pub use bindle_writer::write; +pub use expander::expand_manifest; + +const BINDLE_REGISTRY_URL_PATH: &str = "api/registry"; + +use std::{path::Path, sync::Arc}; + +use anyhow::{Context, Result}; +use bindle::client::{ + tokens::{LongLivedToken, TokenManager}, + Client, ClientBuilder, +}; +use semver::BuildMetadata; +use tracing::log; + +use crate::config::ConnectionConfig; + +use self::bindle_pusher::push_all; + +/// Publish the application to the Cloud's Bindle server. +pub async fn publish( + path: impl AsRef, + buildinfo: Option, + connection: ConnectionConfig, +) -> Result<(String, String)> { + let source_dir = path + .as_ref() + .parent() + .context("Failed to get source directory")?; + + let info = BindleConnectionInfo::new( + format!("{}/{}", connection.url, BINDLE_REGISTRY_URL_PATH), + connection.insecure, + connection.token.token.context("Failed to get token")?, + ); + + log::trace!( + "Deploying application from {:?} to {}", + source_dir, + info.base_url + ); + + let tmp = tempfile::tempdir().context("Cannot create temporary directory")?; + let dest_dir = tmp.path(); + + let (mut invoice, sources) = spin_publish::expand_manifest(&path, buildinfo, &dest_dir) + .await + .with_context(|| format!("Failed to expand '{:?}' to a bindle", &dest_dir))?; + + // This is intended to make sure all applications are namespaced using the Fermyon user account. + // TODO: This should check whether the invoice is already namespaced. + invoice.bindle.id = format!( + "{}/{}", + invoice.bindle.id.name(), + invoice.bindle.id.version() + ) + .parse()?; + + let bindle_id = &invoice.bindle.id; + + spin_publish::write(&source_dir, &dest_dir, &invoice, &sources) + .await + .with_context(|| write_failed_msg(bindle_id, dest_dir))?; + + push_all(&dest_dir, bindle_id, info.clone()).await?; + + log::trace!("Published to {:?}", invoice.bindle.id); + + Ok((bindle_id.name().into(), bindle_id.version_string())) +} + +/// BindleConnectionInfo holds the details of a connection to a +/// Bindle server, including url, insecure configuration and an +/// auth token manager +#[derive(Clone)] +pub(crate) struct BindleConnectionInfo { + pub(crate) base_url: String, + pub(crate) allow_insecure: bool, + pub(crate) token_manager: AnyAuth, +} + +impl BindleConnectionInfo { + /// Generates a new BindleConnectionInfo instance using the provided + /// base_url, allow_insecure setting and token. + pub(crate) fn new>(base_url: I, allow_insecure: bool, token: I) -> Self { + let token_manager: Box = + Box::new(LongLivedToken::new(&token.into())); + + Self { + base_url: base_url.into(), + allow_insecure, + token_manager: AnyAuth { + token_manager: Arc::new(token_manager), + }, + } + } + + /// Returns a client based on this instance's configuration + pub(crate) fn client(&self) -> bindle::client::Result> { + let builder = ClientBuilder::default() + .http2_prior_knowledge(false) + .danger_accept_invalid_certs(self.allow_insecure); + builder.build(&self.base_url, self.token_manager.clone()) + } +} + +/// AnyAuth wraps an authentication token manager which applies +/// the appropriate auth header per its configuration +#[derive(Clone)] +pub struct AnyAuth { + token_manager: Arc>, +} + +#[async_trait::async_trait] +impl TokenManager for AnyAuth { + async fn apply_auth_header( + &self, + builder: reqwest::RequestBuilder, + ) -> bindle::client::Result { + self.token_manager.apply_auth_header(builder).await + } +} + +pub(crate) fn write_failed_msg(bindle_id: &bindle::Id, dest_dir: &Path) -> String { + format!( + "Failed to write bindle '{}' to {}", + bindle_id, + dest_dir.display() + ) +} diff --git a/crates/cloud/src/registry/bindle/bindle_pusher.rs b/crates/cloud/src/registry/bindle/bindle_pusher.rs new file mode 100644 index 0000000000..7e24b0137f --- /dev/null +++ b/crates/cloud/src/registry/bindle/bindle_pusher.rs @@ -0,0 +1,37 @@ +#![deny(missing_docs)] + +use anyhow::{Context, Result}; +use bindle::{standalone::StandaloneRead, Id}; +use std::path::Path; + +/// Pushes a standalone bindle to a Bindle server. +pub(crate) async fn push_all( + path: impl AsRef, + bindle_id: &Id, + bindle_connection_info: super::BindleConnectionInfo, +) -> Result<()> { + let reader = StandaloneRead::new(&path, bindle_id).await?; + let client = &bindle_connection_info.client().with_context(|| { + format!( + "Failed to create a bindle client for server '{}'", + &bindle_connection_info.base_url + ) + })?; + + if client.get_yanked_invoice(bindle_id).await.is_ok() { + anyhow::bail!("Bindle {} already exists on the server", bindle_id); + } + + reader + .push(client) + .await + .with_context(|| push_failed_msg(path, &bindle_connection_info.base_url)) +} + +fn push_failed_msg(path: impl AsRef, server_url: &str) -> String { + format!( + "Failed to push bindle from '{}' to server at '{}'", + path.as_ref().display(), + server_url + ) +} diff --git a/crates/cloud/src/registry/bindle/bindle_writer.rs b/crates/cloud/src/registry/bindle/bindle_writer.rs new file mode 100644 index 0000000000..bd581acea0 --- /dev/null +++ b/crates/cloud/src/registry/bindle/bindle_writer.rs @@ -0,0 +1,158 @@ +#![deny(missing_docs)] + +use anyhow::{Context, Result}; +use bindle::{Invoice, Parcel}; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, +}; + +struct BindleWriter { + source_dir: PathBuf, + dest_dir: PathBuf, + invoice: Invoice, + parcel_sources: ParcelSources, +} + +/// Writes an invoice and supporting parcels out as a standalone bindle. +pub async fn write( + source_dir: impl AsRef, + dest_dir: impl AsRef, + invoice: &Invoice, + parcel_sources: &ParcelSources, +) -> Result<()> { + let writer = BindleWriter { + source_dir: source_dir.as_ref().to_owned(), + dest_dir: dest_dir.as_ref().to_owned(), + invoice: invoice.clone(), + parcel_sources: parcel_sources.clone(), + }; + writer.write().await +} + +impl BindleWriter { + async fn write(&self) -> Result<()> { + // This is very similar to bindle::StandaloneWrite::write but... not quite the same + let bindle_id_hash = self.invoice.bindle.id.sha(); + let bindle_dir = self.dest_dir.join(bindle_id_hash); + let parcels_dir = bindle_dir.join("parcels"); + tokio::fs::create_dir_all(&parcels_dir).await?; + + self.write_invoice_file(&bindle_dir).await?; + self.write_parcel_files(&parcels_dir).await?; + Ok(()) + } + + async fn write_invoice_file(&self, bindle_dir: &Path) -> Result<()> { + let invoice_text = toml::to_string_pretty(&self.invoice)?; + let invoice_file = bindle_dir.join("invoice.toml"); + tokio::fs::write(&invoice_file, &invoice_text) + .await + .with_context(|| format!("Failed to write invoice to '{}'", invoice_file.display()))?; + Ok(()) + } + + async fn write_parcel_files(&self, parcels_dir: &Path) -> Result<()> { + let parcels = match &self.invoice.parcel { + Some(p) => p, + None => return Ok(()), + }; + + let parcel_writes = parcels + .iter() + .map(|parcel| self.write_one_parcel(parcels_dir, parcel)); + futures::future::join_all(parcel_writes) + .await + .into_iter() + .collect::>>()?; + Ok(()) + } + + async fn write_one_parcel(&self, parcels_dir: &Path, parcel: &Parcel) -> Result<()> { + let source_file = match self.parcel_sources.source(&parcel.label.sha256) { + Some(path) => path.clone(), + None => self.source_dir.join(&parcel.label.name), + }; + let hash = &parcel.label.sha256; + let dest_file = parcels_dir.join(format!("{}.dat", hash)); + tokio::fs::copy(&source_file, &dest_file) + .await + .with_context(|| copy_parcel_failed_msg(&source_file, &dest_file))?; + + if has_annotation(parcel, DELETE_ON_WRITE) { + tokio::fs::remove_file(&source_file).await.ignore_errors(); // Leaking a temp file is sad but not a reason to fail + } + + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct ParcelSource { + digest: String, + source_path: PathBuf, +} + +#[derive(Debug, Clone)] +pub struct ParcelSources { + sources: Vec, +} + +impl ParcelSources { + pub fn source(&self, digest: &str) -> Option<&PathBuf> { + self.sources + .iter() + .find(|s| s.digest == digest) + .map(|s| &s.source_path) + } + + pub fn single(digest: &str, source: impl AsRef) -> Self { + let parcel_source = ParcelSource { + digest: digest.to_owned(), + source_path: source.as_ref().to_owned(), + }; + Self { + sources: vec![parcel_source], + } + } + + pub fn from_iter(paths: impl Iterator)>) -> Self { + let sources = paths + .map(|(digest, path)| ParcelSource { + digest, + source_path: path.as_ref().to_owned(), + }) + .collect(); + + Self { sources } + } +} + +fn has_annotation(parcel: &Parcel, key: &str) -> bool { + match &parcel.label.annotations { + Some(map) => map.contains_key(key), + None => false, + } +} + +const DELETE_ON_WRITE: &str = "fermyon:spin:delete_on_write"; + +pub(crate) fn delete_after_copy() -> BTreeMap { + BTreeMap::from([(DELETE_ON_WRITE.to_owned(), ".".to_owned())]) +} + +trait IgnoreErrors { + fn ignore_errors(&self); +} + +impl IgnoreErrors for Result<(), E> { + fn ignore_errors(&self) {} +} + +fn copy_parcel_failed_msg(source_file: &Path, dest_file: &Path) -> String { + format!( + "Failed to copy parcel from {} to '{}'", + source_file.display(), + dest_file.display() + ) +} diff --git a/crates/cloud/src/registry/bindle/expander.rs b/crates/cloud/src/registry/bindle/expander.rs new file mode 100644 index 0000000000..853a36f729 --- /dev/null +++ b/crates/cloud/src/registry/bindle/expander.rs @@ -0,0 +1,420 @@ +#![deny(missing_docs)] + +use super::bindle_writer::{self, ParcelSources}; +use anyhow::{Context, Result}; +use bindle::{BindleSpec, Condition, Group, Invoice, Label, Parcel}; +use path_absolutize::Absolutize; +use semver::BuildMetadata; +use sha2::{Digest, Sha256}; +use spin_loader::{bindle::config as bindle_schema, local::config as local_schema}; +use std::path::{Path, PathBuf}; + +/// Expands a file-based application manifest to a Bindle invoice. +pub async fn expand_manifest( + app_file: impl AsRef, + buildinfo: Option, + scratch_dir: impl AsRef, +) -> Result<(Invoice, ParcelSources)> { + let app_file = app_file + .as_ref() + .absolutize() + .context("Failed to resolve absolute path to manifest file")?; + let manifest = spin_loader::local::raw_manifest_from_file(&app_file).await?; + let local_schema::RawAppManifestAnyVersion::V1(manifest) = manifest; + let app_dir = app_dir(&app_file)?; + + // * create a new spin.toml-like document where + // - each component changes its `files` entry to a group name + // - each component changes its `source` entry to a parcel SHA + let dest_manifest = bindle_manifest(&manifest, &app_dir)?; + + // * create an invoice where + // - the metadata is copied from the app manifest + // - there is a group for each component + // - there is a parcel for each asset + // - there is a parcel for each module source + // - if a component refers to an asset then the asset is in the component's group + // - the source and manifest parcels should NOT be group members + // - there is a parcel for the spin.toml-a-like and it has the magic media type + + // - n parcels for the Wasm modules at their locations + let wasm_parcels = wasm_parcels(&manifest, &app_dir) + .await + .context("Failed to collect Wasm modules")?; + let wasm_parcels = consolidate_wasm_parcels(wasm_parcels); + // - n parcels for the assets under the base directory + let asset_parcels = asset_parcels(&manifest, &app_dir) + .await + .context("Failed to collect asset files")?; + let asset_parcels = consolidate_asset_parcels(asset_parcels); + // - one parcel to rule them all, and in the Spin app bind them + let manifest_parcel = manifest_parcel(&dest_manifest, &scratch_dir).await?; + + let sourced_parcels = itertools::concat([vec![manifest_parcel], wasm_parcels, asset_parcels]); + let (parcels, sources) = split_sources(sourced_parcels); + + let bindle_id = bindle_id(&manifest.info, buildinfo)?; + let groups = build_groups(&manifest); + + let invoice = Invoice { + bindle_version: "1.0.0".to_owned(), + yanked: None, + bindle: BindleSpec { + id: bindle_id, + description: manifest.info.description.clone(), + authors: manifest.info.authors.clone(), + }, + annotations: None, + parcel: Some(parcels), + group: Some(groups), + signature: None, + yanked_signature: None, + }; + + Ok((invoice, sources)) +} + +fn bindle_manifest( + local: &local_schema::RawAppManifest, + base_dir: &Path, +) -> Result { + let components = local + .components + .iter() + .map(|c| bindle_component_manifest(c, base_dir)) + .collect::>>() + .context("Failed to convert components to Bindle format")?; + let trigger = local.info.trigger.clone(); + let variables = local.variables.clone(); + + Ok(bindle_schema::RawAppManifest { + trigger, + components, + variables, + }) +} + +fn bindle_component_manifest( + local: &local_schema::RawComponentManifest, + base_dir: &Path, +) -> Result { + let source_digest = match &local.source { + local_schema::RawModuleSource::FileReference(path) => { + let full_path = base_dir.join(path); + file_digest_string(&full_path) + .with_context(|| format!("Failed to get parcel id for '{}'", full_path.display()))? + } + local_schema::RawModuleSource::Bindle(_) => { + anyhow::bail!( + "This version of Spin can't publish components whose sources are already bindles" + ) + } + }; + let asset_group = local.wasm.files.as_ref().map(|_| group_name_for(&local.id)); + Ok(bindle_schema::RawComponentManifest { + id: local.id.clone(), + description: local.description.clone(), + source: source_digest, + wasm: bindle_schema::RawWasmConfig { + environment: local.wasm.environment.clone(), + files: asset_group, + allowed_http_hosts: local.wasm.allowed_http_hosts.clone(), + }, + trigger: local.trigger.clone(), + config: local.config.clone(), + }) +} + +async fn wasm_parcels( + manifest: &local_schema::RawAppManifest, + base_dir: &Path, +) -> Result> { + let parcel_futures = manifest.components.iter().map(|c| wasm_parcel(c, base_dir)); + let parcels = futures::future::join_all(parcel_futures).await; + parcels.into_iter().collect() +} + +async fn wasm_parcel( + component: &local_schema::RawComponentManifest, + base_dir: &Path, +) -> Result { + let wasm_file = match &component.source { + local_schema::RawModuleSource::FileReference(path) => path, + local_schema::RawModuleSource::Bindle(_) => { + anyhow::bail!( + "This version of Spin can't publish components whose sources are already bindles" + ) + } + }; + let absolute_wasm_file = base_dir.join(wasm_file); + + file_parcel(&absolute_wasm_file, wasm_file, None, "application/wasm").await +} + +async fn asset_parcels( + manifest: &local_schema::RawAppManifest, + base_dir: impl AsRef, +) -> Result> { + let assets_by_component: Vec> = manifest + .components + .iter() + .map(|c| collect_assets(c, &base_dir)) + .collect::>()?; + let parcel_futures = assets_by_component + .iter() + .flatten() + .map(|(fm, s)| file_parcel_from_mount(fm, s)); + let parcel_results = futures::future::join_all(parcel_futures).await; + let parcels = parcel_results.into_iter().collect::>()?; + Ok(parcels) +} + +fn collect_assets( + component: &local_schema::RawComponentManifest, + base_dir: impl AsRef, +) -> Result> { + let patterns = component.wasm.files.clone().unwrap_or_default(); + let exclude_files = component.wasm.exclude_files.clone().unwrap_or_default(); + let file_mounts = spin_loader::local::assets::collect(&patterns, &exclude_files, &base_dir) + .with_context(|| format!("Failed to get file mounts for component '{}'", component.id))?; + let annotated = file_mounts + .into_iter() + .map(|v| (v, component.id.clone())) + .collect(); + Ok(annotated) +} + +async fn file_parcel_from_mount( + file_mount: &spin_loader::local::assets::FileMount, + component_id: &str, +) -> Result { + let source_file = &file_mount.src; + + let media_type = mime_guess::from_path(&source_file) + .first_or_octet_stream() + .to_string(); + + file_parcel( + source_file, + &file_mount.relative_dst, + Some(component_id), + &media_type, + ) + .await + .with_context(|| format!("Failed to assemble parcel from '{}'", source_file.display())) +} + +async fn file_parcel( + abs_src: &Path, + dest_relative_path: impl AsRef, + component_id: Option<&str>, + media_type: impl Into, +) -> Result { + let digest = file_digest_string(&abs_src) + .with_context(|| format!("Failed to calculate digest for '{}'", abs_src.display()))?; + let size = tokio::fs::metadata(&abs_src).await?.len(); + + let member_of = component_id.map(|id| vec![group_name_for(id)]); + + let parcel = Parcel { + label: Label { + sha256: digest, + name: dest_relative_path.as_ref().display().to_string(), + size, + media_type: media_type.into(), + annotations: None, + feature: None, + origin: None, + }, + conditions: Some(Condition { + member_of, + requires: None, + }), + }; + + Ok(SourcedParcel { + parcel, + source: abs_src.to_owned(), + }) +} + +async fn manifest_parcel( + manifest: &bindle_schema::RawAppManifest, + scratch_dir: impl AsRef, +) -> Result { + let text = toml::to_string_pretty(&manifest).context("Failed to write app manifest to TOML")?; + let bytes = text.as_bytes(); + let digest = bytes_digest_string(bytes); + + let parcel_name = format!("spin.{}.toml", digest); + let temp_dir = scratch_dir.as_ref().join("manifests"); + let temp_file = temp_dir.join(&parcel_name); + + tokio::fs::create_dir_all(temp_dir) + .await + .context("Failed to save app manifest to temporary file")?; + tokio::fs::write(&temp_file, &bytes) + .await + .context("Failed to save app manifest to temporary file")?; + + let absolute_path = dunce::canonicalize(&temp_file) + .context("Failed to acquire full path for app manifest temporary file")?; + + let parcel = Parcel { + label: Label { + sha256: digest.clone(), + name: parcel_name, + size: u64::try_from(bytes.len())?, + media_type: spin_loader::bindle::SPIN_MANIFEST_MEDIA_TYPE.to_owned(), + annotations: Some(bindle_writer::delete_after_copy()), + feature: None, + origin: None, + }, + conditions: None, + }; + + Ok(SourcedParcel { + parcel, + source: absolute_path, + }) +} + +fn consolidate_wasm_parcels(parcels: Vec) -> Vec { + // We use only the content of Wasm parcels, not their names, so we only + // care if the content is the same. + let mut parcels = parcels; + parcels.dedup_by_key(|p| p.parcel.label.sha256.clone()); + parcels +} + +fn consolidate_asset_parcels(parcels: Vec) -> Vec { + let mut consolidated = vec![]; + + for mut parcel in parcels { + match consolidated + .iter_mut() + .find(|p: &&mut SourcedParcel| can_consolidate_asset_parcels(&p.parcel, &parcel.parcel)) + { + None => consolidated.push(parcel), + Some(existing) => { + // If can_consolidate returned true, both parcels must have conditions + // and both conditions must have a member_of list. So these unwraps + // are safe. + // + // TODO: modify can_consolidate to return suitable stuff so we don't + // have to unwrap. + let existing_conds = existing.parcel.conditions.as_mut().unwrap(); + let conds_to_merge = parcel.parcel.conditions.as_mut().unwrap(); + let existing_member_of = existing_conds.member_of.as_mut().unwrap(); + let member_of_to_merge = conds_to_merge.member_of.as_mut().unwrap(); + existing_member_of.append(member_of_to_merge); + } + } + } + + consolidated +} + +fn can_consolidate_asset_parcels(first: &Parcel, second: &Parcel) -> bool { + // For asset parcels, we care not only about the content, but where they + // are placed and whether they have any metadata. For example, if the same + // image is needed both at /resources/logo.png and at /images/header.png, + // we don't want to consolidate those references. + if first.label.name == second.label.name + && first.label.sha256 == second.label.sha256 + && first.label.size == second.label.size + && first.label.media_type == second.label.media_type + && first.label.annotations.is_none() + && second.label.annotations.is_none() + && first.label.feature.is_none() + && second.label.feature.is_none() + { + match (&first.conditions, &second.conditions) { + (Some(c1), Some(c2)) => { + c1.member_of.is_some() + && c2.member_of.is_some() + && c1.requires.is_none() + && c2.requires.is_none() + } + _ => false, + } + } else { + false + } +} + +fn build_groups(manifest: &local_schema::RawAppManifest) -> Vec { + manifest + .components + .iter() + .map(|c| group_for(&c.id)) + .collect() +} + +fn group_name_for(component_id: &str) -> String { + format!("files-{}", component_id) +} + +fn group_for(component_id: &str) -> Group { + Group { + name: group_name_for(component_id), + required: None, + satisfied_by: None, + } +} + +fn file_digest_string(path: impl AsRef) -> Result { + let mut file = std::fs::File::open(&path)?; + let mut sha = Sha256::new(); + std::io::copy(&mut file, &mut sha)?; + let digest_value = sha.finalize(); + let digest_string = format!("{:x}", digest_value); + Ok(digest_string) +} + +fn bytes_digest_string(bytes: &[u8]) -> String { + let digest_value = Sha256::digest(bytes); + let digest_string = format!("{:x}", digest_value); + digest_string +} + +fn bindle_id( + app_info: &local_schema::RawAppInformation, + buildinfo: Option, +) -> Result { + let text = match buildinfo { + None => format!("{}/{}", app_info.name, app_info.version), + Some(buildinfo) => format!("{}/{}+{}", app_info.name, app_info.version, buildinfo), + }; + bindle::Id::try_from(&text) + .with_context(|| format!("App name and version '{}' do not form a bindle ID", text)) +} + +fn app_dir(app_file: impl AsRef) -> Result { + let path_buf = app_file + .as_ref() + .parent() + .ok_or_else(|| { + anyhow::anyhow!( + "Failed to get containing directory for app file '{}'", + app_file.as_ref().display() + ) + })? + .to_owned(); + Ok(path_buf) +} + +struct SourcedParcel { + parcel: Parcel, + source: PathBuf, +} + +fn split_sources(sourced_parcels: Vec) -> (Vec, ParcelSources) { + let sources = sourced_parcels + .iter() + .map(|sp| (sp.parcel.label.sha256.clone(), &sp.source)); + let parcel_sources = ParcelSources::from_iter(sources); + let parcels = sourced_parcels.into_iter().map(|sp| sp.parcel); + + (parcels.collect(), parcel_sources) +} diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index bda00f3ef6..ce719efd56 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -1,6 +1,9 @@ use anyhow::{anyhow, bail, Context, Result}; use bindle::Id; use clap::Parser; +use copypasta::{ClipboardContext, ClipboardProvider}; +use cloud::client::Client as CloudClient; +use cloud::config::ConnectionConfig; use hippo::{Client, ConnectionInfo}; use hippo_openapi::models::ChannelRevisionSelectionStrategy; use semver::BuildMetadata; @@ -10,9 +13,11 @@ use spin_http::routes::RoutePattern; use spin_loader::local::config::{RawAppManifest, RawAppManifestAnyVersion}; use spin_loader::local::{assets, config}; use spin_manifest::{HttpTriggerConfiguration, TriggerConfig}; + use std::fs::File; use std::io::{copy, Write}; use std::path::PathBuf; +use std::time::Duration; use url::Url; use uuid::Uuid; @@ -20,6 +25,9 @@ use crate::{opts::*, parse_buildinfo, sloth::warn_if_slow_response}; const SPIN_DEPLOY_CHANNEL_NAME: &str = "spin-deploy"; +// this is the client ID registered in the Cloud's backend +const SPIN_CLIENT_ID: &str = "583e63e9-461f-4fbe-a246-23e0fb1cad10"; + /// Package and upload Spin artifacts, notifying Hippo #[derive(Parser, Debug)] #[clap(about = "Deploy a Spin application")] @@ -38,8 +46,10 @@ pub struct DeployCommand { name = BINDLE_SERVER_URL_OPT, long = "bindle-server", env = BINDLE_URL_ENV, + requires = HIPPO_USERNAME, + requires = HIPPO_PASSWORD, )] - pub bindle_server_url: String, + pub bindle_server_url: Option, /// Basic http auth username for the bindle server #[clap( @@ -87,19 +97,23 @@ pub struct DeployCommand { /// Hippo username #[clap( - name = "HIPPO_USERNAME", + name = HIPPO_USERNAME, long = "hippo-username", - env = "HIPPO_USERNAME" + env = HIPPO_USERNAME, + requires = BINDLE_SERVER_URL_OPT, + requires = HIPPO_PASSWORD, )] - pub hippo_username: String, + pub hippo_username: Option, /// Hippo password #[clap( - name = "HIPPO_PASSWORD", + name = HIPPO_PASSWORD, long = "hippo-password", - env = "HIPPO_PASSWORD" + env = HIPPO_PASSWORD, + requires = BINDLE_SERVER_URL_OPT, + requires = HIPPO_USERNAME, )] - pub hippo_password: String, + pub hippo_password: Option, /// Disable attaching buildinfo #[clap( @@ -130,6 +144,14 @@ pub struct DeployCommand { impl DeployCommand { pub async fn run(self) -> Result<()> { + if self.hippo_username.is_some() { + self.deploy_hippo().await + } else { + self.deploy_cloud().await + } + } + + async fn deploy_hippo(self) -> Result<()> { let cfg_any = spin_loader::local::raw_manifest_from_file(&self.app).await?; let RawAppManifestAnyVersion::V1(cfg) = cfg_any; @@ -154,8 +176,8 @@ impl DeployCommand { danger_accept_invalid_certs: self.insecure, api_key: None, }), - self.hippo_username.clone(), - self.hippo_password.clone(), + self.hippo_username.as_deref().unwrap().to_string(), + self.hippo_password.as_deref().unwrap().to_string(), ) .await { @@ -256,6 +278,105 @@ impl DeployCommand { Ok(()) } + async fn deploy_cloud(self) -> Result<()> { + let mut connection_config = ConnectionConfig { + url: self.hippo_server_url.clone(), + insecure: self.insecure, + token: Default::default(), + }; + + connection_config.token = self.github_token(connection_config.clone()).await?; + + let client = CloudClient::new(connection_config.clone()); + + client + .create_application(None, self.app, self.buildinfo, connection_config) + .await?; + + Ok(()) + } + + async fn github_token( + &self, + connection_config: ConnectionConfig, + ) -> Result { + let client = CloudClient::new(connection_config); + + // Generate a device code and a user code to activate it with + let device_code = client + .create_device_code(Uuid::parse_str(SPIN_CLIENT_ID)?) + .await?; + + // Copy the user code to the clipboard. + + // TODO(radu): should this interact with a user's clipboard? + // This was added purely for convenience, particularly because the token + // returned by our Platform is short lived, which means a user would have to + // perform the login process every 30 minutes by default, which sounds + // VERY aggressive. + + // TODO(radu): this works on macOS, but might fail on other systems. + // Also, there should be a way to disable it. + + // This works on Linux, but needs an extra library installed, which is not very easy to find. + let user_code = device_code.user_code.clone().unwrap(); + let copied_to_clipboard = try_copy_to_clipboard(&user_code); + + println!( + "Open the Cloud's device authorization URL in your browser: {} and enter the code: {}", + device_code.verification_url.clone().unwrap(), + user_code + ); + + if copied_to_clipboard { + println!("The code has been copied to your clipboard for convenience.") + } + + // Open the default web browser to the device verification page, with + // the user code copied to the clipboard. + + // TODO(radu): this works on macOS, but might fail on other systems (e.g. WSL2). + // Also, there should be a way to disable it. + + // According to https://docs.rs/webbrowser/latest/webbrowser/ this should work on windows and Linux as well, + // Tested on my linux VM and it worked + let _ = webbrowser::open(&device_code.verification_url.clone().unwrap()); + + // The OAuth library should theoretically handle waiting for the device to be authorized, but + // testing revealed that it doesn't work. So we manually poll every 10 seconds for two minutes. + let mut count = 0; + let timeout = 12; + + // Loop while waiting for the device code to be authorized by the user + loop { + if count > timeout { + bail!("Timed out waiting to authorize the device. Please execute the `fermyon login` command again and authorize the device with GitHub."); + } + + match client.login(device_code.device_code.clone().unwrap()).await { + // The cloud returns a 500 when the code is not authorized with a specific message, but when testing I only saw the response coming + // back as Ok, but when the device code was not authorized the token was null + // Expected behaviour would be that 500 lands in the Err + Ok(response) => { + if response.token != None { + println!("Device authorized!"); + return Ok(response); + } + + println!("Waiting for device authorization..."); + tokio::time::sleep(Duration::from_secs(10)).await; + count += 1; + continue; + } + Err(_) => { + println!("There was an error while waiting for device authorization"); + tokio::time::sleep(Duration::from_secs(10)).await; + count += 1; + } + }; + } + } + async fn compute_buildinfo(&self, cfg: &RawAppManifest) -> Result { let mut sha256 = Sha256::new(); let app_folder = self.app.parent().with_context(|| { @@ -348,9 +469,10 @@ impl DeployCommand { } async fn create_and_push_bindle(&self, buildinfo: Option) -> Result { + let bindle_url = self.bindle_server_url.as_deref().unwrap(); let source_dir = crate::app_dir(&self.app)?; let bindle_connection_info = spin_publish::BindleConnectionInfo::new( - &self.bindle_server_url, + bindle_url, self.insecure, self.bindle_username.clone(), self.bindle_password.clone(), @@ -371,7 +493,7 @@ impl DeployCommand { .await .with_context(|| crate::write_failed_msg(bindle_id, dest_dir))?; - let _sloth_warning = warn_if_slow_response(&self.bindle_server_url); + let _sloth_warning = warn_if_slow_response(bindle_url); let publish_result = spin_publish::push_all(&dest_dir, bindle_id, bindle_connection_info).await; @@ -394,7 +516,7 @@ impl DeployCommand { return Err(publish_err).with_context(|| { format!( "Failed to push bindle {} to server {}", - bindle_id, self.bindle_server_url + bindle_id, bindle_url ) }); } @@ -524,3 +646,13 @@ fn format_login_error(err: &anyhow::Error) -> anyhow::Result { Ok(format!("Problem logging into Hippo: {}", error.detail)) } } + +fn try_copy_to_clipboard(text: &str) -> bool { + match ClipboardContext::new() { + Ok(mut ctx) => { + let result = ctx.set_contents(text.to_owned()); + result.is_ok() + } + Err(_) => false, + } +} diff --git a/src/opts.rs b/src/opts.rs index 0b2d2d5794..ab7de16fbe 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -10,6 +10,8 @@ pub const INSECURE_OPT: &str = "INSECURE"; pub const STAGING_DIR_OPT: &str = "STAGING_DIR"; pub const HIPPO_SERVER_URL_OPT: &str = "HIPPO_SERVER_URL"; pub const HIPPO_URL_ENV: &str = "HIPPO_URL"; +pub const HIPPO_USERNAME: &str = "HIPPO_USERNAME"; +pub const HIPPO_PASSWORD: &str = "HIPPO_PASSWORD"; pub const BUILD_UP_OPT: &str = "UP"; pub const PLUGIN_NAME_OPT: &str = "PLUGIN_NAME"; pub const PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT: &str = "REMOTE_PLUGIN_MANIFEST"; From 62935af1cfc93c2bf26bfe65b4dbb68355b349a6 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Mon, 3 Oct 2022 22:17:13 -0700 Subject: [PATCH 02/36] first hack at `spin login` Signed-off-by: Matthew Fisher --- Cargo.lock | 370 +++++++++++++++++++++++++++- Cargo.toml | 1 + crates/cloud/src/client.rs | 11 +- crates/cloud/src/config.rs | 9 - crates/cloud/src/lib.rs | 1 - crates/cloud/src/registry/bindle.rs | 2 +- src/bin/spin.rs | 4 +- src/commands.rs | 2 + src/commands/deploy.rs | 3 +- src/commands/login.rs | 224 +++++++++++++++++ 10 files changed, 607 insertions(+), 20 deletions(-) delete mode 100644 crates/cloud/src/config.rs create mode 100644 src/commands/login.rs diff --git a/Cargo.lock b/Cargo.lock index 9812695fe9..83066c2606 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "opaque-debug", +] + [[package]] name = "ahash" version = "0.7.6" @@ -98,6 +110,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-io" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7" +dependencies = [ + "autocfg", + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "winapi", +] + [[package]] name = "async-trait" version = "0.1.57" @@ -239,6 +271,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-modes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" +dependencies = [ + "block-padding", + "cipher", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "blowfish" version = "0.8.0" @@ -280,6 +328,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + [[package]] name = "cap-fs-ext" version = "0.25.3" @@ -551,6 +605,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "concurrent-queue" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" +dependencies = [ + "cache-padded", +] + [[package]] name = "console" version = "0.15.2" @@ -834,6 +897,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "csv" version = "1.1.6" @@ -914,6 +987,17 @@ dependencies = [ "syn", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dialoguer" version = "0.9.0" @@ -1107,6 +1191,27 @@ dependencies = [ "syn", ] +[[package]] +name = "enumflags2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.9.1" @@ -1293,6 +1398,21 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.24" @@ -1539,6 +1659,26 @@ dependencies = [ "uuid", ] +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest 0.9.0", + "hmac 0.11.0", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1813,6 +1953,18 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "keyring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38fb8399ddcabfccb274577a8d90f0653e0b5b5977797c1c8834ad09839a10e5" +dependencies = [ + "byteorder", + "secret-service", + "security-framework", + "winapi", +] + [[package]] name = "kstring" version = "1.0.6" @@ -2114,6 +2266,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nb-connect" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1bb540dc6ef51cfe1916ec038ce7a620daf3a111e2502d745197cd53d6bca15" +dependencies = [ + "libc", + "socket2", +] + [[package]] name = "ndk" version = "0.6.0" @@ -2155,7 +2317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ "darling", - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "syn", @@ -2170,6 +2332,19 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nix" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "nix" version = "0.24.2" @@ -2204,6 +2379,20 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.3" @@ -2215,6 +2404,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2225,6 +2423,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -2259,7 +2480,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "syn", @@ -2474,6 +2695,12 @@ dependencies = [ "wit-bindgen-wasmtime", ] +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" version = "0.11.2" @@ -2695,6 +2922,20 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "polling" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "winapi", +] + [[package]] name = "postgres-protocol" version = "0.6.4" @@ -2705,7 +2946,7 @@ dependencies = [ "byteorder", "bytes", "fallible-iterator", - "hmac", + "hmac 0.12.1", "md-5", "memchr", "rand 0.8.5", @@ -2730,6 +2971,15 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + [[package]] name = "proc-macro-crate" version = "1.2.1" @@ -3210,6 +3460,26 @@ dependencies = [ "untrusted", ] +[[package]] +name = "secret-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1da5c423b8783185fd3fecd1c8796c267d2c089d894ce5a93c280a5d3f780a2" +dependencies = [ + "aes", + "block-modes", + "hkdf", + "lazy_static", + "num", + "rand 0.8.5", + "serde", + "sha2 0.9.9", + "zbus", + "zbus_macros", + "zvariant", + "zvariant_derive", +] + [[package]] name = "security-framework" version = "2.7.0" @@ -3311,6 +3581,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3595,6 +3876,7 @@ dependencies = [ "hippo", "hippo-openapi", "hyper", + "keyring", "lazy_static", "nix 0.24.2", "openssl", @@ -3920,6 +4202,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.2" @@ -4524,6 +4812,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "walkdir" version = "2.3.2" @@ -5052,6 +5346,15 @@ dependencies = [ "webpki", ] +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "which" version = "4.3.0" @@ -5357,6 +5660,41 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +[[package]] +name = "zbus" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cbeb2291cd7267a94489b71376eda33496c1b9881adf6b36f26cc2779f3fc49" +dependencies = [ + "async-io", + "byteorder", + "derivative", + "enumflags2", + "fastrand", + "futures", + "nb-connect", + "nix 0.22.3", + "once_cell", + "polling", + "scoped-tls", + "serde", + "serde_repr", + "zbus_macros", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa3959a7847cf95e3d51e312856617c5b1b77191176c65a79a5f14d778bbe0a6" +dependencies = [ + "proc-macro-crate 0.1.5", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zeroize" version = "1.3.0" @@ -5406,3 +5744,29 @@ dependencies = [ "cc", "libc", ] + +[[package]] +name = "zvariant" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68c7b55f2074489b7e8e07d2d0a6ee6b4f233867a653c664d8020ba53692525" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ca5e22593eb4212382d60d26350065bf2a02c34b85bc850474a74b589a3de9" +dependencies = [ + "proc-macro-crate 1.2.1", + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 81b93b95a4..0a466c26d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ env_logger = "0.9" futures = "0.3" hippo-openapi = "0.10" hippo = { git = "https://github.com/deislabs/hippo-cli", tag = "v0.16.1" } +keyring = "1" lazy_static = "1.4.0" nix = { version = "0.24", features = ["signal"] } outbound-http = { path = "crates/outbound-http" } diff --git a/crates/cloud/src/client.rs b/crates/cloud/src/client.rs index 9911c576ba..a40c3552d0 100644 --- a/crates/cloud/src/client.rs +++ b/crates/cloud/src/client.rs @@ -20,19 +20,24 @@ use cloud_openapi::{ }; use reqwest::header; use semver::BuildMetadata; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::Path}; use tracing::log; use uuid::Uuid; -use crate::config::ConnectionConfig; - const JSON_MIME_TYPE: &str = "application/json"; pub struct Client { configuration: Configuration, } +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct ConnectionConfig { + pub insecure: bool, + pub token: TokenInfo, + pub url: String, +} + impl Client { pub fn new(conn_info: ConnectionConfig) -> Self { let mut headers = header::HeaderMap::new(); diff --git a/crates/cloud/src/config.rs b/crates/cloud/src/config.rs deleted file mode 100644 index 6cc258104a..0000000000 --- a/crates/cloud/src/config.rs +++ /dev/null @@ -1,9 +0,0 @@ -use cloud_openapi::models::TokenInfo; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Clone, Debug, Default)] -pub struct ConnectionConfig { - pub insecure: bool, - pub token: TokenInfo, - pub url: String, -} diff --git a/crates/cloud/src/lib.rs b/crates/cloud/src/lib.rs index f576d2f855..27d44aa192 100644 --- a/crates/cloud/src/lib.rs +++ b/crates/cloud/src/lib.rs @@ -1,4 +1,3 @@ #[allow(unused)] pub mod client; -pub mod config; pub mod registry; diff --git a/crates/cloud/src/registry/bindle.rs b/crates/cloud/src/registry/bindle.rs index 9ef2715643..e6dc07275a 100644 --- a/crates/cloud/src/registry/bindle.rs +++ b/crates/cloud/src/registry/bindle.rs @@ -21,7 +21,7 @@ use bindle::client::{ use semver::BuildMetadata; use tracing::log; -use crate::config::ConnectionConfig; +use crate::client::ConnectionConfig; use self::bindle_pusher::push_all; diff --git a/src/bin/spin.rs b/src/bin/spin.rs index 277ae8c4cb..c6a6f56b13 100644 --- a/src/bin/spin.rs +++ b/src/bin/spin.rs @@ -4,7 +4,7 @@ use lazy_static::lazy_static; use spin_cli::commands::{ bindle::BindleCommands, build::BuildCommand, deploy::DeployCommand, external::execute_external_subcommand, new::NewCommand, plugins::PluginCommands, - templates::TemplateCommands, up::UpCommand, + templates::TemplateCommands, up::UpCommand, login::LoginCommand, }; use spin_http::HttpTrigger; use spin_redis_engine::RedisTrigger; @@ -44,6 +44,7 @@ enum SpinApp { Bindle(BindleCommands), Deploy(DeployCommand), Build(BuildCommand), + Login(LoginCommand), #[clap(subcommand)] Plugin(PluginCommands), #[clap(subcommand, hide = true)] @@ -70,6 +71,7 @@ impl SpinApp { Self::Build(cmd) => cmd.run().await, Self::Trigger(TriggerCommands::Http(cmd)) => cmd.run().await, Self::Trigger(TriggerCommands::Redis(cmd)) => cmd.run().await, + Self::Login(cmd) => cmd.run().await, Self::Plugin(cmd) => cmd.run().await, Self::External(cmd) => execute_external_subcommand(cmd, SpinApp::command()).await, } diff --git a/src/commands.rs b/src/commands.rs index 0da8569c71..892241ee79 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -8,6 +8,8 @@ pub mod build; pub mod deploy; /// Commands for external subcommands (i.e. plugins) pub mod external; +// Command for logging into the server +pub mod login; /// Command for creating a new application. pub mod new; /// Command for adding a plugin to Spin diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index ce719efd56..785e6b81bf 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -2,8 +2,7 @@ use anyhow::{anyhow, bail, Context, Result}; use bindle::Id; use clap::Parser; use copypasta::{ClipboardContext, ClipboardProvider}; -use cloud::client::Client as CloudClient; -use cloud::config::ConnectionConfig; +use cloud::client::{Client as CloudClient, ConnectionConfig}; use hippo::{Client, ConnectionInfo}; use hippo_openapi::models::ChannelRevisionSelectionStrategy; use semver::BuildMetadata; diff --git a/src/commands/login.rs b/src/commands/login.rs new file mode 100644 index 0000000000..edee2e80bb --- /dev/null +++ b/src/commands/login.rs @@ -0,0 +1,224 @@ +use std::time::Duration; + +use anyhow::{Result, bail, Context}; +use clap::Parser; +use cloud::client::{ConnectionConfig, Client}; +use hippo::Client as HippoClient; +use hippo::ConnectionInfo; +use serde::Deserialize; +use serde::Serialize; +use uuid::Uuid; + +use crate::opts::{BINDLE_SERVER_URL_OPT, BINDLE_URL_ENV, HIPPO_USERNAME, HIPPO_PASSWORD, BINDLE_USERNAME, BINDLE_PASSWORD, INSECURE_OPT, HIPPO_SERVER_URL_OPT, HIPPO_URL_ENV}; + +// this is the client ID registered in the Cloud's backend +const SPIN_CLIENT_ID: &str = "583e63e9-461f-4fbe-a246-23e0fb1cad10"; + +/// Log into the server +#[derive(Parser, Debug)] +#[clap(about = "Log into the server")] +pub struct LoginCommand { + /// URL of bindle server + #[clap( + name = BINDLE_SERVER_URL_OPT, + long = "bindle-server", + env = BINDLE_URL_ENV, + requires = HIPPO_SERVER_URL_OPT, + requires = HIPPO_USERNAME, + requires = HIPPO_PASSWORD, + )] + pub bindle_server_url: Option, + + /// Basic http auth username for the bindle server + #[clap( + name = BINDLE_USERNAME, + long = "bindle-username", + env = BINDLE_USERNAME, + requires = BINDLE_PASSWORD + )] + pub bindle_username: Option, + + /// Basic http auth password for the bindle server + #[clap( + name = BINDLE_PASSWORD, + long = "bindle-password", + env = BINDLE_PASSWORD, + requires = BINDLE_USERNAME + )] + pub bindle_password: Option, + + /// Ignore server certificate errors from bindle and hippo + #[clap( + name = INSECURE_OPT, + short = 'k', + long = "insecure", + takes_value = false, + )] + pub insecure: bool, + + /// URL of hippo server + #[clap( + name = HIPPO_SERVER_URL_OPT, + long = "hippo-server", + env = HIPPO_URL_ENV, + requires = BINDLE_SERVER_URL_OPT, + requires = HIPPO_USERNAME, + requires = HIPPO_PASSWORD, + )] + pub hippo_server_url: Option, + + /// Hippo username + #[clap( + name = HIPPO_USERNAME, + long = "hippo-username", + env = HIPPO_USERNAME, + requires = BINDLE_SERVER_URL_OPT, + requires = HIPPO_SERVER_URL_OPT, + requires = HIPPO_PASSWORD, + )] + pub hippo_username: Option, + + /// Hippo password + #[clap( + name = HIPPO_PASSWORD, + long = "hippo-password", + env = HIPPO_PASSWORD, + requires = BINDLE_SERVER_URL_OPT, + requires = HIPPO_SERVER_URL_OPT, + requires = HIPPO_USERNAME, + )] + pub hippo_password: Option, +} + +impl LoginCommand { + pub async fn run(self) -> Result<()> { + + if self.hippo_server_url.is_some() { + // log in with username/password + let token = match HippoClient::login( + &HippoClient::new(ConnectionInfo { + url: self.hippo_server_url.as_deref().unwrap().to_string(), + danger_accept_invalid_certs: self.insecure, + api_key: None, + }), + self.hippo_username.as_deref().unwrap().to_string(), + self.hippo_password.as_deref().unwrap().to_string(), + ) + .await + { + Ok(token_info) => token_info.token.unwrap_or_default(), + Err(err) => bail!(format_login_error(&err)?), + }; + + let connection_info = ConnectionInfoDef { + url: self.hippo_server_url.unwrap().clone(), + danger_accept_invalid_certs: self.insecure, + api_key: Some(token), + }; + + let path = dirs::config_dir().context("Cannot find configuration directory")?.join("spin").join("hippo.json"); + + std::fs::write( + path, + serde_json::to_string_pretty(&connection_info)?, + )?; + + return Ok(()); + } + + // log in to the cloud API + let mut connection_config = ConnectionConfig { + url: "http://localhost:5309".to_owned(), + insecure: self.insecure, + token: Default::default(), + }; + + connection_config.token = github_token(connection_config.clone()).await?; + + // save token to file + let path = dirs::config_dir().context("Cannot find configuration directory")?.join("spin").join("cloud.json"); + + std::fs::write( + path, + serde_json::to_string_pretty(&connection_config)?, + )?; + + Ok(()) + } +} + +async fn github_token(connection_config: ConnectionConfig) -> Result { + let client = Client::new(connection_config); + + // Generate a device code and a user code to activate it with + let device_code = client + .create_device_code(Uuid::parse_str(SPIN_CLIENT_ID)?) + .await?; + + println!( + "Open {} in your browser", + device_code.verification_url.clone().unwrap(), + ); + + println!( + "! Copy your one-time code: {}", + device_code.user_code.clone().unwrap(), + ); + + // The OAuth library should theoretically handle waiting for the device to be authorized, but + // testing revealed that it doesn't work. So we manually poll every 10 seconds for fifteen minutes. + const POLL_INTERVAL_SECS: u64 = 10; + let mut seconds_elapsed = 0; + let timeout_seconds = 15 * 60; + + // Loop while waiting for the device code to be authorized by the user + loop { + if seconds_elapsed > timeout_seconds { + bail!("Timed out waiting to authorize the device. Please execute `spin login` again and authorize the device with GitHub."); + } + + match client.login(device_code.device_code.clone().unwrap()).await { + Ok(response) => { + if response.token != None { + println!("Device authorized!"); + return Ok(response); + } + + println!("Waiting for device authorization..."); + tokio::time::sleep(Duration::from_secs(POLL_INTERVAL_SECS)).await; + seconds_elapsed += POLL_INTERVAL_SECS; + continue; + } + Err(_) => { + println!("There was an error while waiting for device authorization"); + tokio::time::sleep(Duration::from_secs(POLL_INTERVAL_SECS)).await; + seconds_elapsed += POLL_INTERVAL_SECS; + } + }; + } +} + +#[derive(Serialize, Deserialize)] +struct ConnectionInfoDef { + url: String, + danger_accept_invalid_certs: bool, + api_key: Option, +} + +#[derive(Deserialize, Serialize)] +struct LoginHippoError { + title: String, + detail: String, +} + +fn format_login_error(err: &anyhow::Error) -> anyhow::Result { + let error: LoginHippoError = serde_json::from_str(err.to_string().as_str())?; + if error.detail.ends_with(": ") { + Ok(format!( + "Problem logging into Hippo: {}", + error.detail.replace(": ", ".") + )) + } else { + Ok(format!("Problem logging into Hippo: {}", error.detail)) + } +} \ No newline at end of file From 25178709a4fdaee3fff85f746ab1873b4935aa88 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Mon, 3 Oct 2022 22:29:28 -0700 Subject: [PATCH 03/36] standardize to config.json Signed-off-by: Matthew Fisher --- src/commands/login.rs | 63 ++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index edee2e80bb..ba16721fd8 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -93,6 +93,8 @@ pub struct LoginCommand { impl LoginCommand { pub async fn run(self) -> Result<()> { + let path = dirs::config_dir().context("Cannot find configuration directory")?.join("spin").join("config.json"); + if self.hippo_server_url.is_some() { // log in with username/password let token = match HippoClient::login( @@ -106,42 +108,49 @@ impl LoginCommand { ) .await { - Ok(token_info) => token_info.token.unwrap_or_default(), + Ok(token_info) => token_info, Err(err) => bail!(format_login_error(&err)?), }; - let connection_info = ConnectionInfoDef { + let login_connection = LoginConnection { url: self.hippo_server_url.unwrap().clone(), danger_accept_invalid_certs: self.insecure, - api_key: Some(token), + token: token.token.unwrap_or_default(), + expiration: token.expiration.unwrap_or_default(), + bindle_url: self.bindle_server_url, + bindle_username: self.bindle_username, + bindle_password: self.bindle_password, }; - let path = dirs::config_dir().context("Cannot find configuration directory")?.join("spin").join("hippo.json"); - std::fs::write( path, - serde_json::to_string_pretty(&connection_info)?, + serde_json::to_string_pretty(&login_connection)?, )?; + } else { + // log in to the cloud API + let connection_config = ConnectionConfig { + url: "http://localhost:5309".to_owned(), + insecure: self.insecure, + token: Default::default(), + }; - return Ok(()); - } - - // log in to the cloud API - let mut connection_config = ConnectionConfig { - url: "http://localhost:5309".to_owned(), - insecure: self.insecure, - token: Default::default(), - }; - - connection_config.token = github_token(connection_config.clone()).await?; + let token = github_token(connection_config).await?; - // save token to file - let path = dirs::config_dir().context("Cannot find configuration directory")?.join("spin").join("cloud.json"); + let login_connection = LoginConnection { + url: "http://localhost:5309".to_owned(), + danger_accept_invalid_certs: self.insecure, + token: token.token.unwrap_or_default(), + expiration: token.expiration.unwrap_or_default(), + bindle_url: None, + bindle_username: None, + bindle_password: None, + }; - std::fs::write( - path, - serde_json::to_string_pretty(&connection_config)?, - )?; + std::fs::write( + path, + serde_json::to_string_pretty(&login_connection)?, + )?; + } Ok(()) } @@ -199,10 +208,14 @@ async fn github_token(connection_config: ConnectionConfig) -> Result, + bindle_username: Option, + bindle_password: Option, danger_accept_invalid_certs: bool, - api_key: Option, + token: String, + expiration: String, } #[derive(Deserialize, Serialize)] From fa66141e993d1c12afd37074bd8f2c6f9dd99f4d Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Tue, 4 Oct 2022 13:16:59 -0700 Subject: [PATCH 04/36] use credentials from `spin login` Signed-off-by: Matthew Fisher --- src/commands/deploy.rs | 285 ++++++++--------------------------------- src/commands/login.rs | 18 +-- 2 files changed, 61 insertions(+), 242 deletions(-) diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 785e6b81bf..58b9c0d416 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -1,31 +1,29 @@ -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{anyhow, Context, Result}; use bindle::Id; use clap::Parser; -use copypasta::{ClipboardContext, ClipboardProvider}; +use cloud_openapi::models::TokenInfo; use cloud::client::{Client as CloudClient, ConnectionConfig}; use hippo::{Client, ConnectionInfo}; -use hippo_openapi::models::ChannelRevisionSelectionStrategy; +use hippo_openapi::models::{ChannelRevisionSelectionStrategy}; use semver::BuildMetadata; -use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use spin_http::routes::RoutePattern; use spin_loader::local::config::{RawAppManifest, RawAppManifestAnyVersion}; use spin_loader::local::{assets, config}; use spin_manifest::{HttpTriggerConfiguration, TriggerConfig}; +use tokio::fs; use std::fs::File; use std::io::{copy, Write}; use std::path::PathBuf; -use std::time::Duration; use url::Url; use uuid::Uuid; use crate::{opts::*, parse_buildinfo, sloth::warn_if_slow_response}; -const SPIN_DEPLOY_CHANNEL_NAME: &str = "spin-deploy"; +use super::login::LoginConnection; -// this is the client ID registered in the Cloud's backend -const SPIN_CLIENT_ID: &str = "583e63e9-461f-4fbe-a246-23e0fb1cad10"; +const SPIN_DEPLOY_CHANNEL_NAME: &str = "spin-deploy"; /// Package and upload Spin artifacts, notifying Hippo #[derive(Parser, Debug)] @@ -40,51 +38,6 @@ pub struct DeployCommand { )] pub app: PathBuf, - /// URL of bindle server - #[clap( - name = BINDLE_SERVER_URL_OPT, - long = "bindle-server", - env = BINDLE_URL_ENV, - requires = HIPPO_USERNAME, - requires = HIPPO_PASSWORD, - )] - pub bindle_server_url: Option, - - /// Basic http auth username for the bindle server - #[clap( - name = BINDLE_USERNAME, - long = "bindle-username", - env = BINDLE_USERNAME, - requires = BINDLE_PASSWORD - )] - pub bindle_username: Option, - - /// Basic http auth password for the bindle server - #[clap( - name = BINDLE_PASSWORD, - long = "bindle-password", - env = BINDLE_PASSWORD, - requires = BINDLE_USERNAME - )] - pub bindle_password: Option, - - /// Ignore server certificate errors from bindle and hippo - #[clap( - name = INSECURE_OPT, - short = 'k', - long = "insecure", - takes_value = false, - )] - pub insecure: bool, - - /// URL of hippo server - #[clap( - name = HIPPO_SERVER_URL_OPT, - long = "hippo-server", - env = HIPPO_URL_ENV, - )] - pub hippo_server_url: String, - /// Path to assemble the bindle before pushing (defaults to /// a temporary directory) #[clap( @@ -94,26 +47,6 @@ pub struct DeployCommand { )] pub staging_dir: Option, - /// Hippo username - #[clap( - name = HIPPO_USERNAME, - long = "hippo-username", - env = HIPPO_USERNAME, - requires = BINDLE_SERVER_URL_OPT, - requires = HIPPO_PASSWORD, - )] - pub hippo_username: Option, - - /// Hippo password - #[clap( - name = HIPPO_PASSWORD, - long = "hippo-password", - env = HIPPO_PASSWORD, - requires = BINDLE_SERVER_URL_OPT, - requires = HIPPO_USERNAME, - )] - pub hippo_password: Option, - /// Disable attaching buildinfo #[clap( long = "no-buildinfo", @@ -143,14 +76,23 @@ pub struct DeployCommand { impl DeployCommand { pub async fn run(self) -> Result<()> { - if self.hippo_username.is_some() { - self.deploy_hippo().await + + let path = dirs::config_dir().context("Cannot find configuration directory")?.join("spin").join("config.json"); + + let data = fs::read_to_string(path).await?; + + // TODO: invoke LoginCommand::run() if the file cannot be found (not logged in) + + let login_connection: LoginConnection = serde_json::from_str(&data)?; + + if login_connection.bindle_url.is_some() { + self.deploy_hippo(login_connection).await } else { - self.deploy_cloud().await + self.deploy_cloud(login_connection).await } } - async fn deploy_hippo(self) -> Result<()> { + async fn deploy_hippo(self, login_connection: LoginConnection) -> Result<()> { let cfg_any = spin_loader::local::raw_manifest_from_file(&self.app).await?; let RawAppManifestAnyVersion::V1(cfg) = cfg_any; @@ -163,31 +105,16 @@ impl DeployCommand { None }; - self.check_hippo_healthz().await?; - - let bindle_id = self.create_and_push_bindle(buildinfo).await?; - - let sloth_warning = warn_if_slow_response(&self.hippo_server_url); - - let token = match Client::login( - &Client::new(ConnectionInfo { - url: self.hippo_server_url.clone(), - danger_accept_invalid_certs: self.insecure, - api_key: None, - }), - self.hippo_username.as_deref().unwrap().to_string(), - self.hippo_password.as_deref().unwrap().to_string(), - ) - .await - { - Ok(token_info) => token_info.token.unwrap_or_default(), - Err(err) => bail!(format_login_error(&err)?), - }; + check_hippo_healthz(&login_connection.url).await?; + + let bindle_id = self.create_and_push_bindle(buildinfo, login_connection.clone()).await?; + + let _sloth_warning = warn_if_slow_response(&login_connection.url); let hippo_client = Client::new(ConnectionInfo { - url: self.hippo_server_url.clone(), - danger_accept_invalid_certs: self.insecure, - api_key: Some(token), + url: login_connection.url.clone(), + danger_accept_invalid_certs: login_connection.danger_accept_invalid_certs, + api_key: Some(login_connection.token), }); let name = bindle_id.name().to_string(); @@ -267,7 +194,7 @@ impl DeployCommand { print_available_routes( &channel.domain, &http_config.base, - &self.hippo_server_url, + &login_connection.url, &cfg, ); } else { @@ -277,15 +204,16 @@ impl DeployCommand { Ok(()) } - async fn deploy_cloud(self) -> Result<()> { - let mut connection_config = ConnectionConfig { - url: self.hippo_server_url.clone(), - insecure: self.insecure, - token: Default::default(), + async fn deploy_cloud(self, login_connection: LoginConnection) -> Result<()> { + let connection_config = ConnectionConfig { + url: login_connection.url, + insecure: login_connection.danger_accept_invalid_certs, + token: TokenInfo { + token: Some(login_connection.token), + expiration: Some(login_connection.expiration) + }, }; - connection_config.token = self.github_token(connection_config.clone()).await?; - let client = CloudClient::new(connection_config.clone()); client @@ -295,87 +223,6 @@ impl DeployCommand { Ok(()) } - async fn github_token( - &self, - connection_config: ConnectionConfig, - ) -> Result { - let client = CloudClient::new(connection_config); - - // Generate a device code and a user code to activate it with - let device_code = client - .create_device_code(Uuid::parse_str(SPIN_CLIENT_ID)?) - .await?; - - // Copy the user code to the clipboard. - - // TODO(radu): should this interact with a user's clipboard? - // This was added purely for convenience, particularly because the token - // returned by our Platform is short lived, which means a user would have to - // perform the login process every 30 minutes by default, which sounds - // VERY aggressive. - - // TODO(radu): this works on macOS, but might fail on other systems. - // Also, there should be a way to disable it. - - // This works on Linux, but needs an extra library installed, which is not very easy to find. - let user_code = device_code.user_code.clone().unwrap(); - let copied_to_clipboard = try_copy_to_clipboard(&user_code); - - println!( - "Open the Cloud's device authorization URL in your browser: {} and enter the code: {}", - device_code.verification_url.clone().unwrap(), - user_code - ); - - if copied_to_clipboard { - println!("The code has been copied to your clipboard for convenience.") - } - - // Open the default web browser to the device verification page, with - // the user code copied to the clipboard. - - // TODO(radu): this works on macOS, but might fail on other systems (e.g. WSL2). - // Also, there should be a way to disable it. - - // According to https://docs.rs/webbrowser/latest/webbrowser/ this should work on windows and Linux as well, - // Tested on my linux VM and it worked - let _ = webbrowser::open(&device_code.verification_url.clone().unwrap()); - - // The OAuth library should theoretically handle waiting for the device to be authorized, but - // testing revealed that it doesn't work. So we manually poll every 10 seconds for two minutes. - let mut count = 0; - let timeout = 12; - - // Loop while waiting for the device code to be authorized by the user - loop { - if count > timeout { - bail!("Timed out waiting to authorize the device. Please execute the `fermyon login` command again and authorize the device with GitHub."); - } - - match client.login(device_code.device_code.clone().unwrap()).await { - // The cloud returns a 500 when the code is not authorized with a specific message, but when testing I only saw the response coming - // back as Ok, but when the device code was not authorized the token was null - // Expected behaviour would be that 500 lands in the Err - Ok(response) => { - if response.token != None { - println!("Device authorized!"); - return Ok(response); - } - - println!("Waiting for device authorization..."); - tokio::time::sleep(Duration::from_secs(10)).await; - count += 1; - continue; - } - Err(_) => { - println!("There was an error while waiting for device authorization"); - tokio::time::sleep(Duration::from_secs(10)).await; - count += 1; - } - }; - } - } - async fn compute_buildinfo(&self, cfg: &RawAppManifest) -> Result { let mut sha256 = Sha256::new(); let app_folder = self.app.parent().with_context(|| { @@ -467,14 +314,14 @@ impl DeployCommand { } } - async fn create_and_push_bindle(&self, buildinfo: Option) -> Result { - let bindle_url = self.bindle_server_url.as_deref().unwrap(); + async fn create_and_push_bindle(&self, buildinfo: Option, login_connection: LoginConnection) -> Result { + let bindle_url = login_connection.bindle_url.unwrap(); let source_dir = crate::app_dir(&self.app)?; let bindle_connection_info = spin_publish::BindleConnectionInfo::new( - bindle_url, - self.insecure, - self.bindle_username.clone(), - self.bindle_password.clone(), + bindle_url.clone(), + login_connection.danger_accept_invalid_certs, + login_connection.bindle_username, + login_connection.bindle_password, ); let temp_dir = tempfile::tempdir()?; @@ -492,7 +339,7 @@ impl DeployCommand { .await .with_context(|| crate::write_failed_msg(bindle_id, dest_dir))?; - let _sloth_warning = warn_if_slow_response(bindle_url); + let _sloth_warning = warn_if_slow_response(&bindle_url); let publish_result = spin_publish::push_all(&dest_dir, bindle_id, bindle_connection_info).await; @@ -523,16 +370,16 @@ impl DeployCommand { Ok(bindle_id.clone()) } +} - async fn check_hippo_healthz(&self) -> Result<()> { - let hippo_base_url = url::Url::parse(&self.hippo_server_url)?; - let hippo_healthz_url = hippo_base_url.join("/healthz")?; - reqwest::get(hippo_healthz_url.to_string()) - .await? - .error_for_status() - .with_context(|| format!("Hippo server {} is unhealthy", hippo_base_url))?; - Ok(()) - } +async fn check_hippo_healthz(url: &str) -> Result<()> { + let hippo_base_url = url::Url::parse(url)?; + let hippo_healthz_url = hippo_base_url.join("/healthz")?; + reqwest::get(hippo_healthz_url.to_string()) + .await? + .error_for_status() + .with_context(|| format!("Hippo server {} is unhealthy", hippo_base_url))?; + Ok(()) } const READINESS_POLL_INTERVAL_SECS: u64 = 2; @@ -627,31 +474,3 @@ fn print_available_routes( } } } - -#[derive(Deserialize, Serialize)] -struct LoginHippoError { - title: String, - detail: String, -} - -fn format_login_error(err: &anyhow::Error) -> anyhow::Result { - let error: LoginHippoError = serde_json::from_str(err.to_string().as_str())?; - if error.detail.ends_with(": ") { - Ok(format!( - "Problem logging into Hippo: {}", - error.detail.replace(": ", ".") - )) - } else { - Ok(format!("Problem logging into Hippo: {}", error.detail)) - } -} - -fn try_copy_to_clipboard(text: &str) -> bool { - match ClipboardContext::new() { - Ok(mut ctx) => { - let result = ctx.set_contents(text.to_owned()); - result.is_ok() - } - Err(_) => false, - } -} diff --git a/src/commands/login.rs b/src/commands/login.rs index ba16721fd8..0c1ed975e8 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -207,15 +207,15 @@ async fn github_token(connection_config: ConnectionConfig) -> Result, - bindle_username: Option, - bindle_password: Option, - danger_accept_invalid_certs: bool, - token: String, - expiration: String, +#[derive(Clone, Serialize, Deserialize)] +pub struct LoginConnection { + pub url: String, + pub bindle_url: Option, + pub bindle_username: Option, + pub bindle_password: Option, + pub danger_accept_invalid_certs: bool, + pub token: String, + pub expiration: String, } #[derive(Deserialize, Serialize)] From 952f80369134133382efc41df6444e84270bbb77 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Tue, 4 Oct 2022 15:10:11 -0700 Subject: [PATCH 05/36] bugfix: ensure directory, check token's expiration date Signed-off-by: Matthew Fisher --- Cargo.lock | 1 + Cargo.toml | 1 + src/commands/deploy.rs | 18 +++++++++++++----- src/commands/login.rs | 40 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 83066c2606..533dc88950 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3862,6 +3862,7 @@ dependencies = [ "bindle", "bytes", "cargo-target-dep", + "chrono", "clap 3.2.22", "cloud", "cloud-openapi", diff --git a/Cargo.toml b/Cargo.toml index 0a466c26d6..51f07c2f9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ async-trait = "0.1" atty = "0.2" bindle = { git = "https://github.com/fermyon/bindle", tag = "v0.8.1", default-features = false, features = ["client"] } bytes = "1.1" +chrono = "0.4" clap = { version = "3.1.15", features = ["derive", "env"] } cloud = { path = "crates/cloud" } cloud-openapi = { git = "https://github.com/fermyon/cloud-openapi" } diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 58b9c0d416..37d414ce3b 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -1,5 +1,6 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context, Result, bail}; use bindle::Id; +use chrono::{DateTime, Utc}; use clap::Parser; use cloud_openapi::models::TokenInfo; use cloud::client::{Client as CloudClient, ConnectionConfig}; @@ -78,13 +79,20 @@ impl DeployCommand { pub async fn run(self) -> Result<()> { let path = dirs::config_dir().context("Cannot find configuration directory")?.join("spin").join("config.json"); - let data = fs::read_to_string(path).await?; // TODO: invoke LoginCommand::run() if the file cannot be found (not logged in) let login_connection: LoginConnection = serde_json::from_str(&data)?; + // ... or if the token has expired. + let expiration_date = DateTime::parse_from_rfc3339(&login_connection.expiration)?; + let now: DateTime = Utc::now(); + if now > expiration_date { + bail!("Your token has expired. Please log back in.") + } + + // TODO: we should have a smarter check in place here to determine the difference between Hippo and the Cloud APIs if login_connection.bindle_url.is_some() { self.deploy_hippo(login_connection).await } else { @@ -271,7 +279,7 @@ impl DeployCommand { let app = apps_vm.items.iter().find(|&x| x.name == name.clone()); match app { Some(a) => Ok(a.id), - None => anyhow::bail!("No app with name: {}", name), + None => bail!("No app with name: {}", name), } } @@ -288,7 +296,7 @@ impl DeployCommand { .find(|&x| x.revision_number == bindle_version && x.app_id == app_id); Ok(revision .ok_or_else(|| { - anyhow::anyhow!( + anyhow!( "No revision with version {} and app id {}", bindle_version, app_id @@ -310,7 +318,7 @@ impl DeployCommand { .find(|&x| x.app_id == app_id && x.name == name.clone()); match channel { Some(c) => Ok(c.id), - None => anyhow::bail!("No channel with app_id {} and name {}", app_id, name), + None => bail!("No channel with app_id {} and name {}", app_id, name), } } diff --git a/src/commands/login.rs b/src/commands/login.rs index 0c1ed975e8..4075e12d08 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use std::time::Duration; use anyhow::{Result, bail, Context}; @@ -7,6 +8,7 @@ use hippo::Client as HippoClient; use hippo::ConnectionInfo; use serde::Deserialize; use serde::Serialize; +use tracing::log; use uuid::Uuid; use crate::opts::{BINDLE_SERVER_URL_OPT, BINDLE_URL_ENV, HIPPO_USERNAME, HIPPO_PASSWORD, BINDLE_USERNAME, BINDLE_PASSWORD, INSECURE_OPT, HIPPO_SERVER_URL_OPT, HIPPO_URL_ENV}; @@ -93,7 +95,11 @@ pub struct LoginCommand { impl LoginCommand { pub async fn run(self) -> Result<()> { - let path = dirs::config_dir().context("Cannot find configuration directory")?.join("spin").join("config.json"); + let root = dirs::config_dir().context("Cannot find configuration directory")?.join("spin"); + + ensure(&root)?; + + let path = root.join("config.json"); if self.hippo_server_url.is_some() { // log in with username/password @@ -210,8 +216,14 @@ async fn github_token(connection_config: ConnectionConfig) -> Result, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] pub bindle_username: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] pub bindle_password: Option, pub danger_accept_invalid_certs: bool, pub token: String, @@ -234,4 +246,30 @@ fn format_login_error(err: &anyhow::Error) -> anyhow::Result { } else { Ok(format!("Problem logging into Hippo: {}", error.detail)) } +} + +/// Ensure the root directory exists, or else create it. +fn ensure(root: &PathBuf) -> Result<()> { + log::trace!("Ensuring root directory {:?}", root); + if !root.exists() { + log::trace!("Creating configuration root directory `{}`", root.display()); + std::fs::create_dir_all(root).with_context(|| { + format!( + "Failed to create configuration root directory `{}`", + root.display() + ) + })?; + } else if !root.is_dir() { + bail!( + "Configuration root `{}` already exists and is not a directory", + root.display() + ); + } else { + log::trace!( + "Using existing configuration root directory `{}`", + root.display() + ); + } + + Ok(()) } \ No newline at end of file From bc8801f41074b75f1fc9d5aa3abe7e1c09a1b7f5 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Tue, 4 Oct 2022 15:21:37 -0700 Subject: [PATCH 06/36] more context Signed-off-by: Matthew Fisher --- src/commands/deploy.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 37d414ce3b..b09d6a753d 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -78,11 +78,10 @@ pub struct DeployCommand { impl DeployCommand { pub async fn run(self) -> Result<()> { - let path = dirs::config_dir().context("Cannot find configuration directory")?.join("spin").join("config.json"); - let data = fs::read_to_string(path).await?; + let path = dirs::config_dir().context("Cannot find config directory")?.join("spin").join("config.json"); // TODO: invoke LoginCommand::run() if the file cannot be found (not logged in) - + let data = fs::read_to_string(path.clone()).await.context(format!("Cannot find spin config at {}", path.to_string_lossy()))?; let login_connection: LoginConnection = serde_json::from_str(&data)?; // ... or if the token has expired. From 4366a8f3bd58697855d4d2de8dcbf0be7a6a9c05 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Wed, 5 Oct 2022 12:20:46 -0700 Subject: [PATCH 07/36] move to spin-publish; emulate hippo client 1:1 Signed-off-by: Matthew Fisher --- crates/cloud/src/client.rs | 103 +++-- crates/cloud/src/lib.rs | 1 - crates/cloud/src/registry.rs | 5 - crates/cloud/src/registry/bindle.rs | 138 ------ .../src/registry/bindle/bindle_pusher.rs | 37 -- .../src/registry/bindle/bindle_writer.rs | 158 ------- crates/cloud/src/registry/bindle/expander.rs | 420 ------------------ crates/publish/src/lib.rs | 22 +- src/commands/deploy.rs | 206 +++++++-- 9 files changed, 254 insertions(+), 836 deletions(-) delete mode 100644 crates/cloud/src/registry.rs delete mode 100644 crates/cloud/src/registry/bindle.rs delete mode 100644 crates/cloud/src/registry/bindle/bindle_pusher.rs delete mode 100644 crates/cloud/src/registry/bindle/bindle_writer.rs delete mode 100644 crates/cloud/src/registry/bindle/expander.rs diff --git a/crates/cloud/src/client.rs b/crates/cloud/src/client.rs index a40c3552d0..aabfe47c82 100644 --- a/crates/cloud/src/client.rs +++ b/crates/cloud/src/client.rs @@ -6,16 +6,16 @@ use cloud_openapi::{ auth_tokens_api::api_auth_tokens_post, channels_api::{ api_channels_get, api_channels_id_delete, api_channels_id_get, - api_channels_id_logs_get, api_channels_post, + api_channels_id_logs_get, api_channels_post, api_channels_id_patch, }, configuration::{ApiKey, Configuration}, device_codes_api::api_device_codes_post, - Error, + Error, revisions_api::{api_revisions_post, api_revisions_get}, }, models::{ AppItemPage, ChannelItem, ChannelItemPage, ChannelRevisionSelectionStrategy, CreateAccountCommand, CreateAppCommand, CreateChannelCommand, CreateDeviceCodeCommand, - CreateTokenCommand, DeviceCodeItem, GetChannelLogsVm, TokenInfo, + CreateTokenCommand, DeviceCodeItem, GetChannelLogsVm, TokenInfo, RegisterRevisionCommand, RevisionItemPage, UpdateEnvironmentVariableDto, PatchChannelCommand, StringField, ChannelRevisionSelectionStrategyField, GuidNullableField, UpdateEnvironmentVariableDtoListField, }, }; use reqwest::header; @@ -118,40 +118,7 @@ impl Client { .context("Failed to parse response") } - pub async fn create_application( - &self, - name: Option, - path: impl AsRef, - buildinfo: Option, - connection: ConnectionConfig, - ) -> Result { - let (storage_name, version) = super::registry::publish(path, buildinfo, connection).await?; - let name = match name { - Some(n) => n, - None => storage_name, - }; - - let app = self.add_app(&name, &name).await?; - let id = self - .add_channel( - app, - name.to_owned(), - None, - ChannelRevisionSelectionStrategy::UseRangeRule, - None, - None, - None, - ) - .await?; - - println!("Deployed {} version {}", name.clone(), version); - let channel = self.get_channel_by_id(&id.to_string()).await?; - println!("Application is running at {}", channel.domain); - - Ok(app) - } - - pub(crate) async fn add_app(&self, name: &str, storage_id: &str) -> Result { + pub async fn add_app(&self, name: &str, storage_id: &str) -> Result { api_apps_post( &self.configuration, Some(CreateAppCommand { @@ -163,25 +130,25 @@ impl Client { .map_err(format_response_error) } - pub(crate) async fn remove_app(&self, id: String) -> Result<()> { + pub async fn remove_app(&self, id: String) -> Result<()> { api_apps_id_delete(&self.configuration, &id) .await .map_err(format_response_error) } - pub(crate) async fn list_apps(&self) -> Result { + pub async fn list_apps(&self) -> Result { api_apps_get(&self.configuration, None, None, None, None, None) .await .map_err(format_response_error) } - pub(crate) async fn get_channel_by_id(&self, id: &str) -> Result { + pub async fn get_channel_by_id(&self, id: &str) -> Result { api_channels_id_get(&self.configuration, id) .await .map_err(format_response_error) } - pub(crate) async fn list_channels(&self) -> Result { + pub async fn list_channels(&self) -> Result { api_channels_get( &self.configuration, Some(""), @@ -219,17 +186,67 @@ impl Client { .map_err(format_response_error) } - pub(crate) async fn remove_channel(&self, id: String) -> Result<()> { + #[allow(dead_code)] + pub async fn patch_channel( + &self, + id: Uuid, + name: Option, + domain: Option, + revision_selection_strategy: Option, + range_rule: Option, + active_revision_id: Option, + certificate_id: Option, + environment_variables: Option>, + ) -> anyhow::Result<()> { + let command = PatchChannelCommand { + channel_id: Some(id), + name: name.map(|n| Box::new(StringField{ value: Some(n) })), + domain: domain.map(|d| Box::new(StringField{ value: Some(d) })), + revision_selection_strategy: revision_selection_strategy.map(|r| Box::new(ChannelRevisionSelectionStrategyField{ value: Some(r) })), + range_rule: range_rule.map(|r| Box::new(StringField{ value: Some(r) })), + active_revision_id: active_revision_id.map(|r| Box::new(GuidNullableField{ value: Some(r) })), + certificate_id: certificate_id.map((|c| Box::new(GuidNullableField{ value: Some(c) }))), + environment_variables: environment_variables.map(|e| Box::new(UpdateEnvironmentVariableDtoListField{ value: Some(e) })), + }; + + api_channels_id_patch(&self.configuration, &id.to_string(), Some(command)) + .await + .map_err(format_response_error) + } + + pub async fn remove_channel(&self, id: String) -> Result<()> { api_channels_id_delete(&self.configuration, &id) .await .map_err(format_response_error) } - pub(crate) async fn channel_logs(&self, id: String) -> Result { + pub async fn channel_logs(&self, id: String) -> Result { api_channels_id_logs_get(&self.configuration, &id) .await .map_err(format_response_error) } + + pub async fn add_revision( + &self, + app_storage_id: String, + revision_number: String, + ) -> anyhow::Result<()> { + api_revisions_post( + &self.configuration, + Some(RegisterRevisionCommand { + app_storage_id: app_storage_id, + revision_number: revision_number, + }), + ) + .await + .map_err(format_response_error) + } + + pub async fn list_revisions(&self) -> anyhow::Result { + api_revisions_get(&self.configuration, None, None) + .await + .map_err(format_response_error) + } } #[derive(Deserialize, Debug)] diff --git a/crates/cloud/src/lib.rs b/crates/cloud/src/lib.rs index 27d44aa192..d7551f3646 100644 --- a/crates/cloud/src/lib.rs +++ b/crates/cloud/src/lib.rs @@ -1,3 +1,2 @@ #[allow(unused)] pub mod client; -pub mod registry; diff --git a/crates/cloud/src/registry.rs b/crates/cloud/src/registry.rs deleted file mode 100644 index edd2158d1e..0000000000 --- a/crates/cloud/src/registry.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod bindle; - -// Currently, the default way of publishing an application to the Fermyon -// platform is using the Platform's Bindle server. -pub use self::bindle::publish; diff --git a/crates/cloud/src/registry/bindle.rs b/crates/cloud/src/registry/bindle.rs deleted file mode 100644 index e6dc07275a..0000000000 --- a/crates/cloud/src/registry/bindle.rs +++ /dev/null @@ -1,138 +0,0 @@ -#![deny(missing_docs)] - -//! Functions for publishing Spin applications& to Bindle. - -mod bindle_pusher; -mod bindle_writer; -mod expander; - -pub use bindle_writer::write; -pub use expander::expand_manifest; - -const BINDLE_REGISTRY_URL_PATH: &str = "api/registry"; - -use std::{path::Path, sync::Arc}; - -use anyhow::{Context, Result}; -use bindle::client::{ - tokens::{LongLivedToken, TokenManager}, - Client, ClientBuilder, -}; -use semver::BuildMetadata; -use tracing::log; - -use crate::client::ConnectionConfig; - -use self::bindle_pusher::push_all; - -/// Publish the application to the Cloud's Bindle server. -pub async fn publish( - path: impl AsRef, - buildinfo: Option, - connection: ConnectionConfig, -) -> Result<(String, String)> { - let source_dir = path - .as_ref() - .parent() - .context("Failed to get source directory")?; - - let info = BindleConnectionInfo::new( - format!("{}/{}", connection.url, BINDLE_REGISTRY_URL_PATH), - connection.insecure, - connection.token.token.context("Failed to get token")?, - ); - - log::trace!( - "Deploying application from {:?} to {}", - source_dir, - info.base_url - ); - - let tmp = tempfile::tempdir().context("Cannot create temporary directory")?; - let dest_dir = tmp.path(); - - let (mut invoice, sources) = spin_publish::expand_manifest(&path, buildinfo, &dest_dir) - .await - .with_context(|| format!("Failed to expand '{:?}' to a bindle", &dest_dir))?; - - // This is intended to make sure all applications are namespaced using the Fermyon user account. - // TODO: This should check whether the invoice is already namespaced. - invoice.bindle.id = format!( - "{}/{}", - invoice.bindle.id.name(), - invoice.bindle.id.version() - ) - .parse()?; - - let bindle_id = &invoice.bindle.id; - - spin_publish::write(&source_dir, &dest_dir, &invoice, &sources) - .await - .with_context(|| write_failed_msg(bindle_id, dest_dir))?; - - push_all(&dest_dir, bindle_id, info.clone()).await?; - - log::trace!("Published to {:?}", invoice.bindle.id); - - Ok((bindle_id.name().into(), bindle_id.version_string())) -} - -/// BindleConnectionInfo holds the details of a connection to a -/// Bindle server, including url, insecure configuration and an -/// auth token manager -#[derive(Clone)] -pub(crate) struct BindleConnectionInfo { - pub(crate) base_url: String, - pub(crate) allow_insecure: bool, - pub(crate) token_manager: AnyAuth, -} - -impl BindleConnectionInfo { - /// Generates a new BindleConnectionInfo instance using the provided - /// base_url, allow_insecure setting and token. - pub(crate) fn new>(base_url: I, allow_insecure: bool, token: I) -> Self { - let token_manager: Box = - Box::new(LongLivedToken::new(&token.into())); - - Self { - base_url: base_url.into(), - allow_insecure, - token_manager: AnyAuth { - token_manager: Arc::new(token_manager), - }, - } - } - - /// Returns a client based on this instance's configuration - pub(crate) fn client(&self) -> bindle::client::Result> { - let builder = ClientBuilder::default() - .http2_prior_knowledge(false) - .danger_accept_invalid_certs(self.allow_insecure); - builder.build(&self.base_url, self.token_manager.clone()) - } -} - -/// AnyAuth wraps an authentication token manager which applies -/// the appropriate auth header per its configuration -#[derive(Clone)] -pub struct AnyAuth { - token_manager: Arc>, -} - -#[async_trait::async_trait] -impl TokenManager for AnyAuth { - async fn apply_auth_header( - &self, - builder: reqwest::RequestBuilder, - ) -> bindle::client::Result { - self.token_manager.apply_auth_header(builder).await - } -} - -pub(crate) fn write_failed_msg(bindle_id: &bindle::Id, dest_dir: &Path) -> String { - format!( - "Failed to write bindle '{}' to {}", - bindle_id, - dest_dir.display() - ) -} diff --git a/crates/cloud/src/registry/bindle/bindle_pusher.rs b/crates/cloud/src/registry/bindle/bindle_pusher.rs deleted file mode 100644 index 7e24b0137f..0000000000 --- a/crates/cloud/src/registry/bindle/bindle_pusher.rs +++ /dev/null @@ -1,37 +0,0 @@ -#![deny(missing_docs)] - -use anyhow::{Context, Result}; -use bindle::{standalone::StandaloneRead, Id}; -use std::path::Path; - -/// Pushes a standalone bindle to a Bindle server. -pub(crate) async fn push_all( - path: impl AsRef, - bindle_id: &Id, - bindle_connection_info: super::BindleConnectionInfo, -) -> Result<()> { - let reader = StandaloneRead::new(&path, bindle_id).await?; - let client = &bindle_connection_info.client().with_context(|| { - format!( - "Failed to create a bindle client for server '{}'", - &bindle_connection_info.base_url - ) - })?; - - if client.get_yanked_invoice(bindle_id).await.is_ok() { - anyhow::bail!("Bindle {} already exists on the server", bindle_id); - } - - reader - .push(client) - .await - .with_context(|| push_failed_msg(path, &bindle_connection_info.base_url)) -} - -fn push_failed_msg(path: impl AsRef, server_url: &str) -> String { - format!( - "Failed to push bindle from '{}' to server at '{}'", - path.as_ref().display(), - server_url - ) -} diff --git a/crates/cloud/src/registry/bindle/bindle_writer.rs b/crates/cloud/src/registry/bindle/bindle_writer.rs deleted file mode 100644 index bd581acea0..0000000000 --- a/crates/cloud/src/registry/bindle/bindle_writer.rs +++ /dev/null @@ -1,158 +0,0 @@ -#![deny(missing_docs)] - -use anyhow::{Context, Result}; -use bindle::{Invoice, Parcel}; -use std::{ - collections::BTreeMap, - path::{Path, PathBuf}, -}; - -struct BindleWriter { - source_dir: PathBuf, - dest_dir: PathBuf, - invoice: Invoice, - parcel_sources: ParcelSources, -} - -/// Writes an invoice and supporting parcels out as a standalone bindle. -pub async fn write( - source_dir: impl AsRef, - dest_dir: impl AsRef, - invoice: &Invoice, - parcel_sources: &ParcelSources, -) -> Result<()> { - let writer = BindleWriter { - source_dir: source_dir.as_ref().to_owned(), - dest_dir: dest_dir.as_ref().to_owned(), - invoice: invoice.clone(), - parcel_sources: parcel_sources.clone(), - }; - writer.write().await -} - -impl BindleWriter { - async fn write(&self) -> Result<()> { - // This is very similar to bindle::StandaloneWrite::write but... not quite the same - let bindle_id_hash = self.invoice.bindle.id.sha(); - let bindle_dir = self.dest_dir.join(bindle_id_hash); - let parcels_dir = bindle_dir.join("parcels"); - tokio::fs::create_dir_all(&parcels_dir).await?; - - self.write_invoice_file(&bindle_dir).await?; - self.write_parcel_files(&parcels_dir).await?; - Ok(()) - } - - async fn write_invoice_file(&self, bindle_dir: &Path) -> Result<()> { - let invoice_text = toml::to_string_pretty(&self.invoice)?; - let invoice_file = bindle_dir.join("invoice.toml"); - tokio::fs::write(&invoice_file, &invoice_text) - .await - .with_context(|| format!("Failed to write invoice to '{}'", invoice_file.display()))?; - Ok(()) - } - - async fn write_parcel_files(&self, parcels_dir: &Path) -> Result<()> { - let parcels = match &self.invoice.parcel { - Some(p) => p, - None => return Ok(()), - }; - - let parcel_writes = parcels - .iter() - .map(|parcel| self.write_one_parcel(parcels_dir, parcel)); - futures::future::join_all(parcel_writes) - .await - .into_iter() - .collect::>>()?; - Ok(()) - } - - async fn write_one_parcel(&self, parcels_dir: &Path, parcel: &Parcel) -> Result<()> { - let source_file = match self.parcel_sources.source(&parcel.label.sha256) { - Some(path) => path.clone(), - None => self.source_dir.join(&parcel.label.name), - }; - let hash = &parcel.label.sha256; - let dest_file = parcels_dir.join(format!("{}.dat", hash)); - tokio::fs::copy(&source_file, &dest_file) - .await - .with_context(|| copy_parcel_failed_msg(&source_file, &dest_file))?; - - if has_annotation(parcel, DELETE_ON_WRITE) { - tokio::fs::remove_file(&source_file).await.ignore_errors(); // Leaking a temp file is sad but not a reason to fail - } - - Ok(()) - } -} - -#[derive(Debug, Clone)] -pub struct ParcelSource { - digest: String, - source_path: PathBuf, -} - -#[derive(Debug, Clone)] -pub struct ParcelSources { - sources: Vec, -} - -impl ParcelSources { - pub fn source(&self, digest: &str) -> Option<&PathBuf> { - self.sources - .iter() - .find(|s| s.digest == digest) - .map(|s| &s.source_path) - } - - pub fn single(digest: &str, source: impl AsRef) -> Self { - let parcel_source = ParcelSource { - digest: digest.to_owned(), - source_path: source.as_ref().to_owned(), - }; - Self { - sources: vec![parcel_source], - } - } - - pub fn from_iter(paths: impl Iterator)>) -> Self { - let sources = paths - .map(|(digest, path)| ParcelSource { - digest, - source_path: path.as_ref().to_owned(), - }) - .collect(); - - Self { sources } - } -} - -fn has_annotation(parcel: &Parcel, key: &str) -> bool { - match &parcel.label.annotations { - Some(map) => map.contains_key(key), - None => false, - } -} - -const DELETE_ON_WRITE: &str = "fermyon:spin:delete_on_write"; - -pub(crate) fn delete_after_copy() -> BTreeMap { - BTreeMap::from([(DELETE_ON_WRITE.to_owned(), ".".to_owned())]) -} - -trait IgnoreErrors { - fn ignore_errors(&self); -} - -impl IgnoreErrors for Result<(), E> { - fn ignore_errors(&self) {} -} - -fn copy_parcel_failed_msg(source_file: &Path, dest_file: &Path) -> String { - format!( - "Failed to copy parcel from {} to '{}'", - source_file.display(), - dest_file.display() - ) -} diff --git a/crates/cloud/src/registry/bindle/expander.rs b/crates/cloud/src/registry/bindle/expander.rs deleted file mode 100644 index 853a36f729..0000000000 --- a/crates/cloud/src/registry/bindle/expander.rs +++ /dev/null @@ -1,420 +0,0 @@ -#![deny(missing_docs)] - -use super::bindle_writer::{self, ParcelSources}; -use anyhow::{Context, Result}; -use bindle::{BindleSpec, Condition, Group, Invoice, Label, Parcel}; -use path_absolutize::Absolutize; -use semver::BuildMetadata; -use sha2::{Digest, Sha256}; -use spin_loader::{bindle::config as bindle_schema, local::config as local_schema}; -use std::path::{Path, PathBuf}; - -/// Expands a file-based application manifest to a Bindle invoice. -pub async fn expand_manifest( - app_file: impl AsRef, - buildinfo: Option, - scratch_dir: impl AsRef, -) -> Result<(Invoice, ParcelSources)> { - let app_file = app_file - .as_ref() - .absolutize() - .context("Failed to resolve absolute path to manifest file")?; - let manifest = spin_loader::local::raw_manifest_from_file(&app_file).await?; - let local_schema::RawAppManifestAnyVersion::V1(manifest) = manifest; - let app_dir = app_dir(&app_file)?; - - // * create a new spin.toml-like document where - // - each component changes its `files` entry to a group name - // - each component changes its `source` entry to a parcel SHA - let dest_manifest = bindle_manifest(&manifest, &app_dir)?; - - // * create an invoice where - // - the metadata is copied from the app manifest - // - there is a group for each component - // - there is a parcel for each asset - // - there is a parcel for each module source - // - if a component refers to an asset then the asset is in the component's group - // - the source and manifest parcels should NOT be group members - // - there is a parcel for the spin.toml-a-like and it has the magic media type - - // - n parcels for the Wasm modules at their locations - let wasm_parcels = wasm_parcels(&manifest, &app_dir) - .await - .context("Failed to collect Wasm modules")?; - let wasm_parcels = consolidate_wasm_parcels(wasm_parcels); - // - n parcels for the assets under the base directory - let asset_parcels = asset_parcels(&manifest, &app_dir) - .await - .context("Failed to collect asset files")?; - let asset_parcels = consolidate_asset_parcels(asset_parcels); - // - one parcel to rule them all, and in the Spin app bind them - let manifest_parcel = manifest_parcel(&dest_manifest, &scratch_dir).await?; - - let sourced_parcels = itertools::concat([vec![manifest_parcel], wasm_parcels, asset_parcels]); - let (parcels, sources) = split_sources(sourced_parcels); - - let bindle_id = bindle_id(&manifest.info, buildinfo)?; - let groups = build_groups(&manifest); - - let invoice = Invoice { - bindle_version: "1.0.0".to_owned(), - yanked: None, - bindle: BindleSpec { - id: bindle_id, - description: manifest.info.description.clone(), - authors: manifest.info.authors.clone(), - }, - annotations: None, - parcel: Some(parcels), - group: Some(groups), - signature: None, - yanked_signature: None, - }; - - Ok((invoice, sources)) -} - -fn bindle_manifest( - local: &local_schema::RawAppManifest, - base_dir: &Path, -) -> Result { - let components = local - .components - .iter() - .map(|c| bindle_component_manifest(c, base_dir)) - .collect::>>() - .context("Failed to convert components to Bindle format")?; - let trigger = local.info.trigger.clone(); - let variables = local.variables.clone(); - - Ok(bindle_schema::RawAppManifest { - trigger, - components, - variables, - }) -} - -fn bindle_component_manifest( - local: &local_schema::RawComponentManifest, - base_dir: &Path, -) -> Result { - let source_digest = match &local.source { - local_schema::RawModuleSource::FileReference(path) => { - let full_path = base_dir.join(path); - file_digest_string(&full_path) - .with_context(|| format!("Failed to get parcel id for '{}'", full_path.display()))? - } - local_schema::RawModuleSource::Bindle(_) => { - anyhow::bail!( - "This version of Spin can't publish components whose sources are already bindles" - ) - } - }; - let asset_group = local.wasm.files.as_ref().map(|_| group_name_for(&local.id)); - Ok(bindle_schema::RawComponentManifest { - id: local.id.clone(), - description: local.description.clone(), - source: source_digest, - wasm: bindle_schema::RawWasmConfig { - environment: local.wasm.environment.clone(), - files: asset_group, - allowed_http_hosts: local.wasm.allowed_http_hosts.clone(), - }, - trigger: local.trigger.clone(), - config: local.config.clone(), - }) -} - -async fn wasm_parcels( - manifest: &local_schema::RawAppManifest, - base_dir: &Path, -) -> Result> { - let parcel_futures = manifest.components.iter().map(|c| wasm_parcel(c, base_dir)); - let parcels = futures::future::join_all(parcel_futures).await; - parcels.into_iter().collect() -} - -async fn wasm_parcel( - component: &local_schema::RawComponentManifest, - base_dir: &Path, -) -> Result { - let wasm_file = match &component.source { - local_schema::RawModuleSource::FileReference(path) => path, - local_schema::RawModuleSource::Bindle(_) => { - anyhow::bail!( - "This version of Spin can't publish components whose sources are already bindles" - ) - } - }; - let absolute_wasm_file = base_dir.join(wasm_file); - - file_parcel(&absolute_wasm_file, wasm_file, None, "application/wasm").await -} - -async fn asset_parcels( - manifest: &local_schema::RawAppManifest, - base_dir: impl AsRef, -) -> Result> { - let assets_by_component: Vec> = manifest - .components - .iter() - .map(|c| collect_assets(c, &base_dir)) - .collect::>()?; - let parcel_futures = assets_by_component - .iter() - .flatten() - .map(|(fm, s)| file_parcel_from_mount(fm, s)); - let parcel_results = futures::future::join_all(parcel_futures).await; - let parcels = parcel_results.into_iter().collect::>()?; - Ok(parcels) -} - -fn collect_assets( - component: &local_schema::RawComponentManifest, - base_dir: impl AsRef, -) -> Result> { - let patterns = component.wasm.files.clone().unwrap_or_default(); - let exclude_files = component.wasm.exclude_files.clone().unwrap_or_default(); - let file_mounts = spin_loader::local::assets::collect(&patterns, &exclude_files, &base_dir) - .with_context(|| format!("Failed to get file mounts for component '{}'", component.id))?; - let annotated = file_mounts - .into_iter() - .map(|v| (v, component.id.clone())) - .collect(); - Ok(annotated) -} - -async fn file_parcel_from_mount( - file_mount: &spin_loader::local::assets::FileMount, - component_id: &str, -) -> Result { - let source_file = &file_mount.src; - - let media_type = mime_guess::from_path(&source_file) - .first_or_octet_stream() - .to_string(); - - file_parcel( - source_file, - &file_mount.relative_dst, - Some(component_id), - &media_type, - ) - .await - .with_context(|| format!("Failed to assemble parcel from '{}'", source_file.display())) -} - -async fn file_parcel( - abs_src: &Path, - dest_relative_path: impl AsRef, - component_id: Option<&str>, - media_type: impl Into, -) -> Result { - let digest = file_digest_string(&abs_src) - .with_context(|| format!("Failed to calculate digest for '{}'", abs_src.display()))?; - let size = tokio::fs::metadata(&abs_src).await?.len(); - - let member_of = component_id.map(|id| vec![group_name_for(id)]); - - let parcel = Parcel { - label: Label { - sha256: digest, - name: dest_relative_path.as_ref().display().to_string(), - size, - media_type: media_type.into(), - annotations: None, - feature: None, - origin: None, - }, - conditions: Some(Condition { - member_of, - requires: None, - }), - }; - - Ok(SourcedParcel { - parcel, - source: abs_src.to_owned(), - }) -} - -async fn manifest_parcel( - manifest: &bindle_schema::RawAppManifest, - scratch_dir: impl AsRef, -) -> Result { - let text = toml::to_string_pretty(&manifest).context("Failed to write app manifest to TOML")?; - let bytes = text.as_bytes(); - let digest = bytes_digest_string(bytes); - - let parcel_name = format!("spin.{}.toml", digest); - let temp_dir = scratch_dir.as_ref().join("manifests"); - let temp_file = temp_dir.join(&parcel_name); - - tokio::fs::create_dir_all(temp_dir) - .await - .context("Failed to save app manifest to temporary file")?; - tokio::fs::write(&temp_file, &bytes) - .await - .context("Failed to save app manifest to temporary file")?; - - let absolute_path = dunce::canonicalize(&temp_file) - .context("Failed to acquire full path for app manifest temporary file")?; - - let parcel = Parcel { - label: Label { - sha256: digest.clone(), - name: parcel_name, - size: u64::try_from(bytes.len())?, - media_type: spin_loader::bindle::SPIN_MANIFEST_MEDIA_TYPE.to_owned(), - annotations: Some(bindle_writer::delete_after_copy()), - feature: None, - origin: None, - }, - conditions: None, - }; - - Ok(SourcedParcel { - parcel, - source: absolute_path, - }) -} - -fn consolidate_wasm_parcels(parcels: Vec) -> Vec { - // We use only the content of Wasm parcels, not their names, so we only - // care if the content is the same. - let mut parcels = parcels; - parcels.dedup_by_key(|p| p.parcel.label.sha256.clone()); - parcels -} - -fn consolidate_asset_parcels(parcels: Vec) -> Vec { - let mut consolidated = vec![]; - - for mut parcel in parcels { - match consolidated - .iter_mut() - .find(|p: &&mut SourcedParcel| can_consolidate_asset_parcels(&p.parcel, &parcel.parcel)) - { - None => consolidated.push(parcel), - Some(existing) => { - // If can_consolidate returned true, both parcels must have conditions - // and both conditions must have a member_of list. So these unwraps - // are safe. - // - // TODO: modify can_consolidate to return suitable stuff so we don't - // have to unwrap. - let existing_conds = existing.parcel.conditions.as_mut().unwrap(); - let conds_to_merge = parcel.parcel.conditions.as_mut().unwrap(); - let existing_member_of = existing_conds.member_of.as_mut().unwrap(); - let member_of_to_merge = conds_to_merge.member_of.as_mut().unwrap(); - existing_member_of.append(member_of_to_merge); - } - } - } - - consolidated -} - -fn can_consolidate_asset_parcels(first: &Parcel, second: &Parcel) -> bool { - // For asset parcels, we care not only about the content, but where they - // are placed and whether they have any metadata. For example, if the same - // image is needed both at /resources/logo.png and at /images/header.png, - // we don't want to consolidate those references. - if first.label.name == second.label.name - && first.label.sha256 == second.label.sha256 - && first.label.size == second.label.size - && first.label.media_type == second.label.media_type - && first.label.annotations.is_none() - && second.label.annotations.is_none() - && first.label.feature.is_none() - && second.label.feature.is_none() - { - match (&first.conditions, &second.conditions) { - (Some(c1), Some(c2)) => { - c1.member_of.is_some() - && c2.member_of.is_some() - && c1.requires.is_none() - && c2.requires.is_none() - } - _ => false, - } - } else { - false - } -} - -fn build_groups(manifest: &local_schema::RawAppManifest) -> Vec { - manifest - .components - .iter() - .map(|c| group_for(&c.id)) - .collect() -} - -fn group_name_for(component_id: &str) -> String { - format!("files-{}", component_id) -} - -fn group_for(component_id: &str) -> Group { - Group { - name: group_name_for(component_id), - required: None, - satisfied_by: None, - } -} - -fn file_digest_string(path: impl AsRef) -> Result { - let mut file = std::fs::File::open(&path)?; - let mut sha = Sha256::new(); - std::io::copy(&mut file, &mut sha)?; - let digest_value = sha.finalize(); - let digest_string = format!("{:x}", digest_value); - Ok(digest_string) -} - -fn bytes_digest_string(bytes: &[u8]) -> String { - let digest_value = Sha256::digest(bytes); - let digest_string = format!("{:x}", digest_value); - digest_string -} - -fn bindle_id( - app_info: &local_schema::RawAppInformation, - buildinfo: Option, -) -> Result { - let text = match buildinfo { - None => format!("{}/{}", app_info.name, app_info.version), - Some(buildinfo) => format!("{}/{}+{}", app_info.name, app_info.version, buildinfo), - }; - bindle::Id::try_from(&text) - .with_context(|| format!("App name and version '{}' do not form a bindle ID", text)) -} - -fn app_dir(app_file: impl AsRef) -> Result { - let path_buf = app_file - .as_ref() - .parent() - .ok_or_else(|| { - anyhow::anyhow!( - "Failed to get containing directory for app file '{}'", - app_file.as_ref().display() - ) - })? - .to_owned(); - Ok(path_buf) -} - -struct SourcedParcel { - parcel: Parcel, - source: PathBuf, -} - -fn split_sources(sourced_parcels: Vec) -> (Vec, ParcelSources) { - let sources = sourced_parcels - .iter() - .map(|sp| (sp.parcel.label.sha256.clone(), &sp.source)); - let parcel_sources = ParcelSources::from_iter(sources); - let parcels = sourced_parcels.into_iter().map(|sp| sp.parcel); - - (parcels.collect(), parcel_sources) -} diff --git a/crates/publish/src/lib.rs b/crates/publish/src/lib.rs index d4ec1a732c..18d53312ee 100644 --- a/crates/publish/src/lib.rs +++ b/crates/publish/src/lib.rs @@ -11,7 +11,7 @@ pub use bindle_writer::write; pub use expander::expand_manifest; use bindle::client::{ - tokens::{HttpBasic, NoToken, TokenManager}, + tokens::{HttpBasic, NoToken, TokenManager, LongLivedToken}, Client, ClientBuilder, }; use std::sync::Arc; @@ -50,6 +50,21 @@ impl BindleConnectionInfo { } } + /// Generates a new BindleConnectionInfo instance using the provided + /// base_url, allow_insecure setting and token. + pub fn from_token>(base_url: I, allow_insecure: bool, token: I) -> Self { + let token_manager: Box = + Box::new(LongLivedToken::new(&token.into())); + + Self { + base_url: base_url.into(), + allow_insecure, + token_manager: AnyAuth { + token_manager: Arc::new(token_manager), + }, + } + } + /// Returns a client based on this instance's configuration pub fn client(&self) -> bindle::client::Result> { let builder = ClientBuilder::default() @@ -57,6 +72,11 @@ impl BindleConnectionInfo { .danger_accept_invalid_certs(self.allow_insecure); builder.build(&self.base_url, self.token_manager.clone()) } + + /// Returns the base url for this client. + pub fn base_url(&self) -> &str { + self.base_url.as_ref() + } } /// AnyAuth wraps an authentication token manager which applies diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index b09d6a753d..3e118d4491 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -5,13 +5,15 @@ use clap::Parser; use cloud_openapi::models::TokenInfo; use cloud::client::{Client as CloudClient, ConnectionConfig}; use hippo::{Client, ConnectionInfo}; -use hippo_openapi::models::{ChannelRevisionSelectionStrategy}; +use hippo_openapi::models::ChannelRevisionSelectionStrategy; +use cloud_openapi::models::ChannelRevisionSelectionStrategy as CloudChannelRevisionSelectionStrategy; use semver::BuildMetadata; use sha2::{Digest, Sha256}; use spin_http::routes::RoutePattern; use spin_loader::local::config::{RawAppManifest, RawAppManifestAnyVersion}; use spin_loader::local::{assets, config}; use spin_manifest::{HttpTriggerConfiguration, TriggerConfig}; +use spin_publish::BindleConnectionInfo; use tokio::fs; use std::fs::File; @@ -26,6 +28,8 @@ use super::login::LoginConnection; const SPIN_DEPLOY_CHANNEL_NAME: &str = "spin-deploy"; +const BINDLE_REGISTRY_URL_PATH: &str = "api/registry"; + /// Package and upload Spin artifacts, notifying Hippo #[derive(Parser, Debug)] #[clap(about = "Deploy a Spin application")] @@ -91,6 +95,9 @@ impl DeployCommand { bail!("Your token has expired. Please log back in.") } + check_healthz(&login_connection.url).await?; + let _sloth_warning = warn_if_slow_response(&login_connection.url); + // TODO: we should have a smarter check in place here to determine the difference between Hippo and the Cloud APIs if login_connection.bindle_url.is_some() { self.deploy_hippo(login_connection).await @@ -112,11 +119,14 @@ impl DeployCommand { None }; - check_hippo_healthz(&login_connection.url).await?; - - let bindle_id = self.create_and_push_bindle(buildinfo, login_connection.clone()).await?; + let bindle_connection_info = BindleConnectionInfo::new( + login_connection.bindle_url.unwrap(), + login_connection.danger_accept_invalid_certs, + login_connection.bindle_username, + login_connection.bindle_password, + ); - let _sloth_warning = warn_if_slow_response(&login_connection.url); + let bindle_id = self.create_and_push_bindle(buildinfo, bindle_connection_info).await?; let hippo_client = Client::new(ConnectionInfo { url: login_connection.url.clone(), @@ -129,7 +139,7 @@ impl DeployCommand { // Create or update app // TODO: this process involves many calls to Hippo. Should be able to update the channel // via only `add_revision` if bindle naming schema is updated so bindles can be deterministically ordered by Hippo. - let channel_id = match self.get_app_id(&hippo_client, name.clone()).await { + let channel_id = match self.get_app_id_hippo(&hippo_client, name.clone()).await { Ok(app_id) => { Client::add_revision( &hippo_client, @@ -138,10 +148,10 @@ impl DeployCommand { ) .await?; let existing_channel_id = self - .get_channel_id(&hippo_client, SPIN_DEPLOY_CHANNEL_NAME.to_string(), app_id) + .get_channel_id_hippo(&hippo_client, SPIN_DEPLOY_CHANNEL_NAME.to_string(), app_id) .await?; let active_revision_id = self - .get_revision_id(&hippo_client, bindle_id.version_string().clone(), app_id) + .get_revision_id_hippo(&hippo_client, bindle_id.version_string().clone(), app_id) .await?; Client::patch_channel( &hippo_client, @@ -213,19 +223,108 @@ impl DeployCommand { async fn deploy_cloud(self, login_connection: LoginConnection) -> Result<()> { let connection_config = ConnectionConfig { - url: login_connection.url, + url: login_connection.url.clone(), insecure: login_connection.danger_accept_invalid_certs, token: TokenInfo { - token: Some(login_connection.token), - expiration: Some(login_connection.expiration) + token: Some(login_connection.token.clone()), + expiration: Some(login_connection.expiration.clone()) }, }; let client = CloudClient::new(connection_config.clone()); - client - .create_application(None, self.app, self.buildinfo, connection_config) - .await?; + let cfg_any = spin_loader::local::raw_manifest_from_file(&self.app).await?; + let RawAppManifestAnyVersion::V1(cfg) = cfg_any; + + let buildinfo = if !self.no_buildinfo { + match &self.buildinfo { + Some(i) => Some(i.clone()), + None => self.compute_buildinfo(&cfg).await.map(Option::Some)?, + } + } else { + None + }; + + let bindle_connection_info = BindleConnectionInfo::from_token( + format!("{}/{}", login_connection.url, BINDLE_REGISTRY_URL_PATH), + login_connection.danger_accept_invalid_certs, + login_connection.token, + ); + + let bindle_id = self.create_and_push_bindle(buildinfo, bindle_connection_info).await?; + let name = bindle_id.name().to_string(); + + // Create or update app + // TODO: this process involves many calls to Hippo. Should be able to update the channel + // via only `add_revision` if bindle naming schema is updated so bindles can be deterministically ordered by Hippo. + let channel_id = match self.get_app_id_cloud(&client, name.clone()).await { + Ok(app_id) => { + CloudClient::add_revision( + &client, + name.clone(), + bindle_id.version_string().clone(), + ) + .await?; + let existing_channel_id = self + .get_channel_id_cloud(&client, SPIN_DEPLOY_CHANNEL_NAME.to_string(), app_id) + .await?; + let active_revision_id = self + .get_revision_id_cloud(&client, bindle_id.version_string().clone(), app_id) + .await?; + CloudClient::patch_channel( + &client, + existing_channel_id, + None, + None, + Some(CloudChannelRevisionSelectionStrategy::UseSpecifiedRevision), + None, + Some(active_revision_id), + None, + None, + ) + .await + .context("Problem patching a channel")?; + + existing_channel_id + } + Err(_) => { + let range_rule = Some(bindle_id.version_string()); + let app_id = CloudClient::add_app(&client, &name, &name) + .await + .context("Unable to create app")?; + CloudClient::add_channel( + &client, + app_id, + String::from(SPIN_DEPLOY_CHANNEL_NAME), + None, + CloudChannelRevisionSelectionStrategy::UseRangeRule, + range_rule, + None, + None, + ) + .await + .context("Problem creating a channel")? + } + }; + + println!( + "Deployed {} version {}", + name.clone(), + bindle_id.version_string() + ); + let channel = CloudClient::get_channel_by_id(&client, &channel_id.to_string()) + .await + .context("Problem getting channel by id")?; + if let Ok(http_config) = HttpTriggerConfiguration::try_from(cfg.info.trigger.clone()) { + print_available_routes( + &channel.domain, + &http_config.base, + &login_connection.url, + &cfg, + ); + } else { + println!("Application is running at {}", channel.domain); + } Ok(()) } @@ -273,7 +372,7 @@ impl DeployCommand { Ok(buildinfo) } - async fn get_app_id(&self, hippo_client: &Client, name: String) -> Result { + async fn get_app_id_hippo(&self, hippo_client: &Client, name: String) -> Result { let apps_vm = Client::list_apps(hippo_client).await?; let app = apps_vm.items.iter().find(|&x| x.name == name.clone()); match app { @@ -282,7 +381,16 @@ impl DeployCommand { } } - async fn get_revision_id( + async fn get_app_id_cloud(&self, cloud_client: &CloudClient, name: String) -> Result { + let apps_vm = CloudClient::list_apps(cloud_client).await?; + let app = apps_vm.items.iter().find(|&x| x.name == name.clone()); + match app { + Some(a) => Ok(a.id), + None => bail!("No app with name: {}", name), + } + } + + async fn get_revision_id_hippo( &self, hippo_client: &Client, bindle_version: String, @@ -304,7 +412,29 @@ impl DeployCommand { .id) } - async fn get_channel_id( + async fn get_revision_id_cloud( + &self, + cloud_client: &CloudClient, + bindle_version: String, + app_id: Uuid, + ) -> Result { + let revisions = CloudClient::list_revisions(cloud_client).await?; + let revision = revisions + .items + .iter() + .find(|&x| x.revision_number == bindle_version && x.app_id == app_id); + Ok(revision + .ok_or_else(|| { + anyhow!( + "No revision with version {} and app id {}", + bindle_version, + app_id + ) + })? + .id) + } + + async fn get_channel_id_hippo( &self, hippo_client: &Client, name: String, @@ -321,15 +451,25 @@ impl DeployCommand { } } - async fn create_and_push_bindle(&self, buildinfo: Option, login_connection: LoginConnection) -> Result { - let bindle_url = login_connection.bindle_url.unwrap(); + async fn get_channel_id_cloud( + &self, + cloud_client: &CloudClient, + name: String, + app_id: Uuid, + ) -> Result { + let channels_vm = CloudClient::list_channels(cloud_client).await?; + let channel = channels_vm + .items + .iter() + .find(|&x| x.app_id == app_id && x.name == name.clone()); + match channel { + Some(c) => Ok(c.id), + None => bail!("No channel with app_id {} and name {}", app_id, name), + } + } + + async fn create_and_push_bindle(&self, buildinfo: Option, bindle_connection_info: BindleConnectionInfo) -> Result { let source_dir = crate::app_dir(&self.app)?; - let bindle_connection_info = spin_publish::BindleConnectionInfo::new( - bindle_url.clone(), - login_connection.danger_accept_invalid_certs, - login_connection.bindle_username, - login_connection.bindle_password, - ); let temp_dir = tempfile::tempdir()?; let dest_dir = match &self.staging_dir { @@ -346,10 +486,10 @@ impl DeployCommand { .await .with_context(|| crate::write_failed_msg(bindle_id, dest_dir))?; - let _sloth_warning = warn_if_slow_response(&bindle_url); + let _sloth_warning = warn_if_slow_response(bindle_connection_info.base_url()); let publish_result = - spin_publish::push_all(&dest_dir, bindle_id, bindle_connection_info).await; + spin_publish::push_all(&dest_dir, bindle_id, bindle_connection_info.clone()).await; if let Err(publish_err) = publish_result { // TODO: maybe use `thiserror` to return type errors. @@ -369,7 +509,7 @@ impl DeployCommand { return Err(publish_err).with_context(|| { format!( "Failed to push bindle {} to server {}", - bindle_id, bindle_url + bindle_id, bindle_connection_info.base_url() ) }); } @@ -379,13 +519,13 @@ impl DeployCommand { } } -async fn check_hippo_healthz(url: &str) -> Result<()> { - let hippo_base_url = url::Url::parse(url)?; - let hippo_healthz_url = hippo_base_url.join("/healthz")?; - reqwest::get(hippo_healthz_url.to_string()) +async fn check_healthz(url: &str) -> Result<()> { + let base_url = url::Url::parse(url)?; + let healthz_url = base_url.join("/healthz")?; + reqwest::get(healthz_url.to_string()) .await? .error_for_status() - .with_context(|| format!("Hippo server {} is unhealthy", hippo_base_url))?; + .with_context(|| format!("Server {} is unhealthy", base_url))?; Ok(()) } From 93e36f09fecd35d66fa96778aac3ea0af5acecb3 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Wed, 5 Oct 2022 12:31:49 -0700 Subject: [PATCH 08/36] spin login --status Signed-off-by: Matthew Fisher --- src/commands/deploy.rs | 2 +- src/commands/login.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 3e118d4491..9af430fc0e 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -92,7 +92,7 @@ impl DeployCommand { let expiration_date = DateTime::parse_from_rfc3339(&login_connection.expiration)?; let now: DateTime = Utc::now(); if now > expiration_date { - bail!("Your token has expired. Please log back in.") + bail!("Your session has expired. Please log back in.") } check_healthz(&login_connection.url).await?; diff --git a/src/commands/login.rs b/src/commands/login.rs index 4075e12d08..d6a68a518e 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -2,12 +2,15 @@ use std::path::PathBuf; use std::time::Duration; use anyhow::{Result, bail, Context}; +use chrono::DateTime; +use chrono::Utc; use clap::Parser; use cloud::client::{ConnectionConfig, Client}; use hippo::Client as HippoClient; use hippo::ConnectionInfo; use serde::Deserialize; use serde::Serialize; +use tokio::fs; use tracing::log; use uuid::Uuid; @@ -90,6 +93,14 @@ pub struct LoginCommand { requires = HIPPO_USERNAME, )] pub hippo_password: Option, + + /// Display login status + #[clap( + name = "status", + long = "status", + takes_value = false, + )] + pub status: bool, } impl LoginCommand { @@ -101,6 +112,24 @@ impl LoginCommand { let path = root.join("config.json"); + if self.status { + let data = fs::read_to_string(path.clone()).await.context(format!("Cannnot display login information"))?; + let login_connection: LoginConnection = serde_json::from_str(&data)?; + + println!("You are logged into {}", login_connection.url); + if let Some(bindle_url) = login_connection.bindle_url { + println!("With a bindle URL of {}", bindle_url); + } + let expiration_date = DateTime::parse_from_rfc3339(&login_connection.expiration)?; + let now: DateTime = Utc::now(); + if now > expiration_date { + println!("Your session has expired.") + } else { + println!("Your session will expire on {}.", expiration_date); + } + return Ok(()); + } + if self.hippo_server_url.is_some() { // log in with username/password let token = match HippoClient::login( From 517bd2015bdc037ada9f26279a8a649108854f8d Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Wed, 5 Oct 2022 12:39:12 -0700 Subject: [PATCH 09/36] set DEFAULT_CLOUD_URL constant Signed-off-by: Matthew Fisher --- src/commands/login.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index d6a68a518e..6bbafb64b9 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -19,6 +19,8 @@ use crate::opts::{BINDLE_SERVER_URL_OPT, BINDLE_URL_ENV, HIPPO_USERNAME, HIPPO_P // this is the client ID registered in the Cloud's backend const SPIN_CLIENT_ID: &str = "583e63e9-461f-4fbe-a246-23e0fb1cad10"; +const DEFAULT_CLOUD_URL: &str = "http://localhost:5309"; + /// Log into the server #[derive(Parser, Debug)] #[clap(about = "Log into the server")] @@ -164,7 +166,7 @@ impl LoginCommand { } else { // log in to the cloud API let connection_config = ConnectionConfig { - url: "http://localhost:5309".to_owned(), + url: DEFAULT_CLOUD_URL.to_owned(), insecure: self.insecure, token: Default::default(), }; @@ -172,7 +174,7 @@ impl LoginCommand { let token = github_token(connection_config).await?; let login_connection = LoginConnection { - url: "http://localhost:5309".to_owned(), + url: DEFAULT_CLOUD_URL.to_owned(), danger_accept_invalid_certs: self.insecure, token: token.token.unwrap_or_default(), expiration: token.expiration.unwrap_or_default(), From 24eb47bd4e97d89d74911a1ab8eedf9630930416 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Wed, 5 Oct 2022 12:42:04 -0700 Subject: [PATCH 10/36] cargo fmt Signed-off-by: Matthew Fisher --- crates/cloud/src/client.rs | 30 +++++++++++++++--------- crates/publish/src/lib.rs | 2 +- src/bin/spin.rs | 4 ++-- src/commands/deploy.rs | 48 +++++++++++++++++++++++++++----------- src/commands/login.rs | 42 ++++++++++++++++----------------- 5 files changed, 77 insertions(+), 49 deletions(-) diff --git a/crates/cloud/src/client.rs b/crates/cloud/src/client.rs index aabfe47c82..b23e3c7687 100644 --- a/crates/cloud/src/client.rs +++ b/crates/cloud/src/client.rs @@ -6,16 +6,20 @@ use cloud_openapi::{ auth_tokens_api::api_auth_tokens_post, channels_api::{ api_channels_get, api_channels_id_delete, api_channels_id_get, - api_channels_id_logs_get, api_channels_post, api_channels_id_patch, + api_channels_id_logs_get, api_channels_id_patch, api_channels_post, }, configuration::{ApiKey, Configuration}, device_codes_api::api_device_codes_post, - Error, revisions_api::{api_revisions_post, api_revisions_get}, + revisions_api::{api_revisions_get, api_revisions_post}, + Error, }, models::{ AppItemPage, ChannelItem, ChannelItemPage, ChannelRevisionSelectionStrategy, - CreateAccountCommand, CreateAppCommand, CreateChannelCommand, CreateDeviceCodeCommand, - CreateTokenCommand, DeviceCodeItem, GetChannelLogsVm, TokenInfo, RegisterRevisionCommand, RevisionItemPage, UpdateEnvironmentVariableDto, PatchChannelCommand, StringField, ChannelRevisionSelectionStrategyField, GuidNullableField, UpdateEnvironmentVariableDtoListField, + ChannelRevisionSelectionStrategyField, CreateAccountCommand, CreateAppCommand, + CreateChannelCommand, CreateDeviceCodeCommand, CreateTokenCommand, DeviceCodeItem, + GetChannelLogsVm, GuidNullableField, PatchChannelCommand, RegisterRevisionCommand, + RevisionItemPage, StringField, TokenInfo, UpdateEnvironmentVariableDto, + UpdateEnvironmentVariableDtoListField, }, }; use reqwest::header; @@ -200,13 +204,17 @@ impl Client { ) -> anyhow::Result<()> { let command = PatchChannelCommand { channel_id: Some(id), - name: name.map(|n| Box::new(StringField{ value: Some(n) })), - domain: domain.map(|d| Box::new(StringField{ value: Some(d) })), - revision_selection_strategy: revision_selection_strategy.map(|r| Box::new(ChannelRevisionSelectionStrategyField{ value: Some(r) })), - range_rule: range_rule.map(|r| Box::new(StringField{ value: Some(r) })), - active_revision_id: active_revision_id.map(|r| Box::new(GuidNullableField{ value: Some(r) })), - certificate_id: certificate_id.map((|c| Box::new(GuidNullableField{ value: Some(c) }))), - environment_variables: environment_variables.map(|e| Box::new(UpdateEnvironmentVariableDtoListField{ value: Some(e) })), + name: name.map(|n| Box::new(StringField { value: Some(n) })), + domain: domain.map(|d| Box::new(StringField { value: Some(d) })), + revision_selection_strategy: revision_selection_strategy + .map(|r| Box::new(ChannelRevisionSelectionStrategyField { value: Some(r) })), + range_rule: range_rule.map(|r| Box::new(StringField { value: Some(r) })), + active_revision_id: active_revision_id + .map(|r| Box::new(GuidNullableField { value: Some(r) })), + certificate_id: certificate_id + .map((|c| Box::new(GuidNullableField { value: Some(c) }))), + environment_variables: environment_variables + .map(|e| Box::new(UpdateEnvironmentVariableDtoListField { value: Some(e) })), }; api_channels_id_patch(&self.configuration, &id.to_string(), Some(command)) diff --git a/crates/publish/src/lib.rs b/crates/publish/src/lib.rs index 18d53312ee..f8bbf436ad 100644 --- a/crates/publish/src/lib.rs +++ b/crates/publish/src/lib.rs @@ -11,7 +11,7 @@ pub use bindle_writer::write; pub use expander::expand_manifest; use bindle::client::{ - tokens::{HttpBasic, NoToken, TokenManager, LongLivedToken}, + tokens::{HttpBasic, LongLivedToken, NoToken, TokenManager}, Client, ClientBuilder, }; use std::sync::Arc; diff --git a/src/bin/spin.rs b/src/bin/spin.rs index c6a6f56b13..73ca777981 100644 --- a/src/bin/spin.rs +++ b/src/bin/spin.rs @@ -3,8 +3,8 @@ use clap::{CommandFactory, Parser, Subcommand}; use lazy_static::lazy_static; use spin_cli::commands::{ bindle::BindleCommands, build::BuildCommand, deploy::DeployCommand, - external::execute_external_subcommand, new::NewCommand, plugins::PluginCommands, - templates::TemplateCommands, up::UpCommand, login::LoginCommand, + external::execute_external_subcommand, login::LoginCommand, new::NewCommand, + plugins::PluginCommands, templates::TemplateCommands, up::UpCommand, }; use spin_http::HttpTrigger; use spin_redis_engine::RedisTrigger; diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 9af430fc0e..df8df5ddf4 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -1,12 +1,12 @@ -use anyhow::{anyhow, Context, Result, bail}; +use anyhow::{anyhow, bail, Context, Result}; use bindle::Id; use chrono::{DateTime, Utc}; use clap::Parser; -use cloud_openapi::models::TokenInfo; use cloud::client::{Client as CloudClient, ConnectionConfig}; +use cloud_openapi::models::ChannelRevisionSelectionStrategy as CloudChannelRevisionSelectionStrategy; +use cloud_openapi::models::TokenInfo; use hippo::{Client, ConnectionInfo}; use hippo_openapi::models::ChannelRevisionSelectionStrategy; -use cloud_openapi::models::ChannelRevisionSelectionStrategy as CloudChannelRevisionSelectionStrategy; use semver::BuildMetadata; use sha2::{Digest, Sha256}; use spin_http::routes::RoutePattern; @@ -81,11 +81,16 @@ pub struct DeployCommand { impl DeployCommand { pub async fn run(self) -> Result<()> { - - let path = dirs::config_dir().context("Cannot find config directory")?.join("spin").join("config.json"); + let path = dirs::config_dir() + .context("Cannot find config directory")? + .join("spin") + .join("config.json"); // TODO: invoke LoginCommand::run() if the file cannot be found (not logged in) - let data = fs::read_to_string(path.clone()).await.context(format!("Cannot find spin config at {}", path.to_string_lossy()))?; + let data = fs::read_to_string(path.clone()).await.context(format!( + "Cannot find spin config at {}", + path.to_string_lossy() + ))?; let login_connection: LoginConnection = serde_json::from_str(&data)?; // ... or if the token has expired. @@ -126,7 +131,9 @@ impl DeployCommand { login_connection.bindle_password, ); - let bindle_id = self.create_and_push_bindle(buildinfo, bindle_connection_info).await?; + let bindle_id = self + .create_and_push_bindle(buildinfo, bindle_connection_info) + .await?; let hippo_client = Client::new(ConnectionInfo { url: login_connection.url.clone(), @@ -148,10 +155,18 @@ impl DeployCommand { ) .await?; let existing_channel_id = self - .get_channel_id_hippo(&hippo_client, SPIN_DEPLOY_CHANNEL_NAME.to_string(), app_id) + .get_channel_id_hippo( + &hippo_client, + SPIN_DEPLOY_CHANNEL_NAME.to_string(), + app_id, + ) .await?; let active_revision_id = self - .get_revision_id_hippo(&hippo_client, bindle_id.version_string().clone(), app_id) + .get_revision_id_hippo( + &hippo_client, + bindle_id.version_string().clone(), + app_id, + ) .await?; Client::patch_channel( &hippo_client, @@ -227,7 +242,7 @@ impl DeployCommand { insecure: login_connection.danger_accept_invalid_certs, token: TokenInfo { token: Some(login_connection.token.clone()), - expiration: Some(login_connection.expiration.clone()) + expiration: Some(login_connection.expiration.clone()), }, }; @@ -251,7 +266,9 @@ impl DeployCommand { login_connection.token, ); - let bindle_id = self.create_and_push_bindle(buildinfo, bindle_connection_info).await?; + let bindle_id = self + .create_and_push_bindle(buildinfo, bindle_connection_info) + .await?; let name = bindle_id.name().to_string(); // Create or update app @@ -468,7 +485,11 @@ impl DeployCommand { } } - async fn create_and_push_bindle(&self, buildinfo: Option, bindle_connection_info: BindleConnectionInfo) -> Result { + async fn create_and_push_bindle( + &self, + buildinfo: Option, + bindle_connection_info: BindleConnectionInfo, + ) -> Result { let source_dir = crate::app_dir(&self.app)?; let temp_dir = tempfile::tempdir()?; @@ -509,7 +530,8 @@ impl DeployCommand { return Err(publish_err).with_context(|| { format!( "Failed to push bindle {} to server {}", - bindle_id, bindle_connection_info.base_url() + bindle_id, + bindle_connection_info.base_url() ) }); } diff --git a/src/commands/login.rs b/src/commands/login.rs index 6bbafb64b9..3bdc32f078 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -1,11 +1,11 @@ use std::path::PathBuf; use std::time::Duration; -use anyhow::{Result, bail, Context}; +use anyhow::{bail, Context, Result}; use chrono::DateTime; use chrono::Utc; use clap::Parser; -use cloud::client::{ConnectionConfig, Client}; +use cloud::client::{Client, ConnectionConfig}; use hippo::Client as HippoClient; use hippo::ConnectionInfo; use serde::Deserialize; @@ -14,7 +14,10 @@ use tokio::fs; use tracing::log; use uuid::Uuid; -use crate::opts::{BINDLE_SERVER_URL_OPT, BINDLE_URL_ENV, HIPPO_USERNAME, HIPPO_PASSWORD, BINDLE_USERNAME, BINDLE_PASSWORD, INSECURE_OPT, HIPPO_SERVER_URL_OPT, HIPPO_URL_ENV}; +use crate::opts::{ + BINDLE_PASSWORD, BINDLE_SERVER_URL_OPT, BINDLE_URL_ENV, BINDLE_USERNAME, HIPPO_PASSWORD, + HIPPO_SERVER_URL_OPT, HIPPO_URL_ENV, HIPPO_USERNAME, INSECURE_OPT, +}; // this is the client ID registered in the Cloud's backend const SPIN_CLIENT_ID: &str = "583e63e9-461f-4fbe-a246-23e0fb1cad10"; @@ -97,25 +100,24 @@ pub struct LoginCommand { pub hippo_password: Option, /// Display login status - #[clap( - name = "status", - long = "status", - takes_value = false, - )] + #[clap(name = "status", long = "status", takes_value = false)] pub status: bool, } impl LoginCommand { pub async fn run(self) -> Result<()> { - - let root = dirs::config_dir().context("Cannot find configuration directory")?.join("spin"); + let root = dirs::config_dir() + .context("Cannot find configuration directory")? + .join("spin"); ensure(&root)?; let path = root.join("config.json"); if self.status { - let data = fs::read_to_string(path.clone()).await.context(format!("Cannnot display login information"))?; + let data = fs::read_to_string(path.clone()) + .await + .context(format!("Cannnot display login information"))?; let login_connection: LoginConnection = serde_json::from_str(&data)?; println!("You are logged into {}", login_connection.url); @@ -159,10 +161,7 @@ impl LoginCommand { bindle_password: self.bindle_password, }; - std::fs::write( - path, - serde_json::to_string_pretty(&login_connection)?, - )?; + std::fs::write(path, serde_json::to_string_pretty(&login_connection)?)?; } else { // log in to the cloud API let connection_config = ConnectionConfig { @@ -183,17 +182,16 @@ impl LoginCommand { bindle_password: None, }; - std::fs::write( - path, - serde_json::to_string_pretty(&login_connection)?, - )?; + std::fs::write(path, serde_json::to_string_pretty(&login_connection)?)?; } - + Ok(()) } } -async fn github_token(connection_config: ConnectionConfig) -> Result { +async fn github_token( + connection_config: ConnectionConfig, +) -> Result { let client = Client::new(connection_config); // Generate a device code and a user code to activate it with @@ -303,4 +301,4 @@ fn ensure(root: &PathBuf) -> Result<()> { } Ok(()) -} \ No newline at end of file +} From e5ead3d2ee4f498021497b4c6ac5b0b17d9f2fe5 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Wed, 5 Oct 2022 14:19:26 -0700 Subject: [PATCH 11/36] login if token expired Signed-off-by: Matthew Fisher --- src/commands/deploy.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index df8df5ddf4..2b1612d48f 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -24,6 +24,7 @@ use uuid::Uuid; use crate::{opts::*, parse_buildinfo, sloth::warn_if_slow_response}; +use super::login::LoginCommand; use super::login::LoginConnection; const SPIN_DEPLOY_CHANNEL_NAME: &str = "spin-deploy"; @@ -91,13 +92,19 @@ impl DeployCommand { "Cannot find spin config at {}", path.to_string_lossy() ))?; - let login_connection: LoginConnection = serde_json::from_str(&data)?; + let mut login_connection: LoginConnection = serde_json::from_str(&data)?; - // ... or if the token has expired. let expiration_date = DateTime::parse_from_rfc3339(&login_connection.expiration)?; let now: DateTime = Utc::now(); if now > expiration_date { - bail!("Your session has expired. Please log back in.") + // session has expired - log back in + LoginCommand::parse_from(vec!["login"]).run().await?; + + let new_data = fs::read_to_string(path.clone()).await.context(format!( + "Cannot find spin config at {}", + path.to_string_lossy() + ))?; + login_connection = serde_json::from_str(&new_data)?; } check_healthz(&login_connection.url).await?; From ad047e9006dec193271940307b41fb418f5dcfc1 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Wed, 5 Oct 2022 14:30:24 -0700 Subject: [PATCH 12/36] log in if config file does not exist Signed-off-by: Matthew Fisher --- src/commands/deploy.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 2b1612d48f..1d4c77fcaa 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -17,6 +17,7 @@ use spin_publish::BindleConnectionInfo; use tokio::fs; use std::fs::File; +use std::io; use std::io::{copy, Write}; use std::path::PathBuf; use url::Url; @@ -87,11 +88,19 @@ impl DeployCommand { .join("spin") .join("config.json"); - // TODO: invoke LoginCommand::run() if the file cannot be found (not logged in) - let data = fs::read_to_string(path.clone()).await.context(format!( - "Cannot find spin config at {}", - path.to_string_lossy() - ))?; + // log in if config.json does not exist or cannot be read + let data = match fs::read_to_string(path.clone()).await { + Ok(d) => d, + Err(e) if e.kind() == io::ErrorKind::NotFound => { + // log in, then read config + LoginCommand::parse_from(vec!["login"]).run().await?; + fs::read_to_string(path.clone()).await? + } + Err(e) => { + bail!("Could not log in: {}", e); + } + }; + let mut login_connection: LoginConnection = serde_json::from_str(&data)?; let expiration_date = DateTime::parse_from_rfc3339(&login_connection.expiration)?; @@ -211,9 +220,6 @@ impl DeployCommand { } }; - // Hippo has responded - we don't want to keep the sloth timer running. - drop(sloth_warning); - println!( "Deployed {} version {}", name.clone(), @@ -225,7 +231,7 @@ impl DeployCommand { if let Ok(http_config) = HttpTriggerConfiguration::try_from(cfg.info.trigger.clone()) { wait_for_ready( &channel.domain, - &self.hippo_server_url, + &login_connection.url, &cfg, self.readiness_timeout_secs, ) From 257345d0972a1b2cd6fbf09bd332b928f4325636 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Wed, 5 Oct 2022 14:39:44 -0700 Subject: [PATCH 13/36] cargo clippy Signed-off-by: Matthew Fisher --- crates/cloud/src/client.rs | 7 ++++--- src/commands/login.rs | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/cloud/src/client.rs b/crates/cloud/src/client.rs index b23e3c7687..e3a766db23 100644 --- a/crates/cloud/src/client.rs +++ b/crates/cloud/src/client.rs @@ -191,6 +191,7 @@ impl Client { } #[allow(dead_code)] + #[allow(clippy::too_many_arguments)] pub async fn patch_channel( &self, id: Uuid, @@ -212,7 +213,7 @@ impl Client { active_revision_id: active_revision_id .map(|r| Box::new(GuidNullableField { value: Some(r) })), certificate_id: certificate_id - .map((|c| Box::new(GuidNullableField { value: Some(c) }))), + .map(|c| Box::new(GuidNullableField { value: Some(c) })), environment_variables: environment_variables .map(|e| Box::new(UpdateEnvironmentVariableDtoListField { value: Some(e) })), }; @@ -242,8 +243,8 @@ impl Client { api_revisions_post( &self.configuration, Some(RegisterRevisionCommand { - app_storage_id: app_storage_id, - revision_number: revision_number, + app_storage_id, + revision_number, }), ) .await diff --git a/src/commands/login.rs b/src/commands/login.rs index 3bdc32f078..679ea55480 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -117,7 +117,7 @@ impl LoginCommand { if self.status { let data = fs::read_to_string(path.clone()) .await - .context(format!("Cannnot display login information"))?; + .context("Cannnot display login information")?; let login_connection: LoginConnection = serde_json::from_str(&data)?; println!("You are logged into {}", login_connection.url); @@ -152,7 +152,7 @@ impl LoginCommand { }; let login_connection = LoginConnection { - url: self.hippo_server_url.unwrap().clone(), + url: self.hippo_server_url.unwrap(), danger_accept_invalid_certs: self.insecure, token: token.token.unwrap_or_default(), expiration: token.expiration.unwrap_or_default(), From 471495129db9a547bfcee153fcd96fa2f24c723b Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Wed, 5 Oct 2022 14:58:19 -0700 Subject: [PATCH 14/36] cargo fmt Signed-off-by: Matthew Fisher --- crates/cloud/src/client.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/cloud/src/client.rs b/crates/cloud/src/client.rs index e3a766db23..886aac5875 100644 --- a/crates/cloud/src/client.rs +++ b/crates/cloud/src/client.rs @@ -212,8 +212,7 @@ impl Client { range_rule: range_rule.map(|r| Box::new(StringField { value: Some(r) })), active_revision_id: active_revision_id .map(|r| Box::new(GuidNullableField { value: Some(r) })), - certificate_id: certificate_id - .map(|c| Box::new(GuidNullableField { value: Some(c) })), + certificate_id: certificate_id.map(|c| Box::new(GuidNullableField { value: Some(c) })), environment_variables: environment_variables .map(|e| Box::new(UpdateEnvironmentVariableDtoListField { value: Some(e) })), }; From 7ae65cef30c3e14efcde16203a0721c2ea1c8da1 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Thu, 6 Oct 2022 14:26:13 -0700 Subject: [PATCH 15/36] implement --get-device-code, --check-device-code Signed-off-by: Matthew Fisher --- src/commands/login.rs | 79 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 679ea55480..f7d20761e5 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -6,6 +6,8 @@ use chrono::DateTime; use chrono::Utc; use clap::Parser; use cloud::client::{Client, ConnectionConfig}; +use cloud_openapi::models::DeviceCodeItem; +use cloud_openapi::models::TokenInfo; use hippo::Client as HippoClient; use hippo::ConnectionInfo; use serde::Deserialize; @@ -102,6 +104,22 @@ pub struct LoginCommand { /// Display login status #[clap(name = "status", long = "status", takes_value = false)] pub status: bool, + + // fetch a device code + #[clap( + name = "get-device-code", + long = "get-device-code", + takes_value = false + )] + pub get_device_code: bool, + + // check a device code + #[clap( + name = "check-device-code", + long = "check-device-code", + takes_value = false + )] + pub check_device_code: Option, } impl LoginCommand { @@ -170,6 +188,30 @@ impl LoginCommand { token: Default::default(), }; + if self.get_device_code { + println!( + "{}", + serde_json::to_string_pretty( + &create_device_code(&Client::new(connection_config)).await? + )? + ); + return Ok(()); + } + + if self.check_device_code.is_some() { + println!( + "{}", + serde_json::to_string_pretty( + &check_device_code( + &Client::new(connection_config), + self.check_device_code.unwrap() + ) + .await? + )? + ); + return Ok(()); + } + let token = github_token(connection_config).await?; let login_connection = LoginConnection { @@ -195,9 +237,7 @@ async fn github_token( let client = Client::new(connection_config); // Generate a device code and a user code to activate it with - let device_code = client - .create_device_code(Uuid::parse_str(SPIN_CLIENT_ID)?) - .await?; + let device_code = create_device_code(&client).await?; println!( "Open {} in your browser", @@ -221,20 +261,13 @@ async fn github_token( bail!("Timed out waiting to authorize the device. Please execute `spin login` again and authorize the device with GitHub."); } - match client.login(device_code.device_code.clone().unwrap()).await { + match check_device_code(&client, device_code.device_code.clone().unwrap()).await { Ok(response) => { - if response.token != None { - println!("Device authorized!"); - return Ok(response); - } - - println!("Waiting for device authorization..."); - tokio::time::sleep(Duration::from_secs(POLL_INTERVAL_SECS)).await; - seconds_elapsed += POLL_INTERVAL_SECS; - continue; + println!("Device authorized!"); + return Ok(response); } Err(_) => { - println!("There was an error while waiting for device authorization"); + println!("Waiting for device authorization..."); tokio::time::sleep(Duration::from_secs(POLL_INTERVAL_SECS)).await; seconds_elapsed += POLL_INTERVAL_SECS; } @@ -242,6 +275,24 @@ async fn github_token( } } +async fn create_device_code(client: &Client) -> Result { + client + .create_device_code(Uuid::parse_str(SPIN_CLIENT_ID)?) + .await +} + +async fn check_device_code(client: &Client, device_code: String) -> Result { + match client.login(device_code).await { + Ok(response) => { + if response.token != None { + return Ok(response); + } + return Err(anyhow::anyhow!("no token info")); + } + Err(e) => Err(e), + } +} + #[derive(Clone, Serialize, Deserialize)] pub struct LoginConnection { pub url: String, From e52835cb5b7a0bdc1aa0eecd3be2e66fd339a65b Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Thu, 6 Oct 2022 14:27:10 -0700 Subject: [PATCH 16/36] check based on value of --hippo-username Signed-off-by: Matthew Fisher --- src/commands/login.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index f7d20761e5..81a98f38c0 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -152,7 +152,7 @@ impl LoginCommand { return Ok(()); } - if self.hippo_server_url.is_some() { + if self.hippo_username.is_some() { // log in with username/password let token = match HippoClient::login( &HippoClient::new(ConnectionInfo { From c782b2d63ed3950f15a15d9241147734f1062a95 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Thu, 6 Oct 2022 14:31:39 -0700 Subject: [PATCH 17/36] print config.json to the terminal Signed-off-by: Matthew Fisher --- src/commands/login.rs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 81a98f38c0..49601920f3 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -136,19 +136,7 @@ impl LoginCommand { let data = fs::read_to_string(path.clone()) .await .context("Cannnot display login information")?; - let login_connection: LoginConnection = serde_json::from_str(&data)?; - - println!("You are logged into {}", login_connection.url); - if let Some(bindle_url) = login_connection.bindle_url { - println!("With a bindle URL of {}", bindle_url); - } - let expiration_date = DateTime::parse_from_rfc3339(&login_connection.expiration)?; - let now: DateTime = Utc::now(); - if now > expiration_date { - println!("Your session has expired.") - } else { - println!("Your session will expire on {}.", expiration_date); - } + println!("{}", data); return Ok(()); } From 0b3bc9c24a16495d584461f0deaa00ead15788f4 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Thu, 6 Oct 2022 14:34:20 -0700 Subject: [PATCH 18/36] remove unused imports Signed-off-by: Matthew Fisher --- src/commands/login.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 49601920f3..3abacba9dc 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -2,8 +2,6 @@ use std::path::PathBuf; use std::time::Duration; use anyhow::{bail, Context, Result}; -use chrono::DateTime; -use chrono::Utc; use clap::Parser; use cloud::client::{Client, ConnectionConfig}; use cloud_openapi::models::DeviceCodeItem; From 06eafe87248b0dec2b36a6838124d442f5d0048d Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Thu, 6 Oct 2022 14:49:00 -0700 Subject: [PATCH 19/36] report waiting status if device code check fails Signed-off-by: Matthew Fisher --- src/commands/login.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 3abacba9dc..63766d3c02 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -10,6 +10,7 @@ use hippo::Client as HippoClient; use hippo::ConnectionInfo; use serde::Deserialize; use serde::Serialize; +use serde_json::json; use tokio::fs; use tracing::log; use uuid::Uuid; @@ -185,16 +186,16 @@ impl LoginCommand { } if self.check_device_code.is_some() { - println!( - "{}", - serde_json::to_string_pretty( - &check_device_code( - &Client::new(connection_config), - self.check_device_code.unwrap() - ) - .await? - )? - ); + let status = match &check_device_code( + &Client::new(connection_config), + self.check_device_code.unwrap(), + ) + .await + { + Ok(d) => serde_json::to_string_pretty(d)?, + Err(_) => serde_json::to_string_pretty(&json!({ "status": "waiting" }))?, + }; + println!("{}", status); return Ok(()); } From 4ac19e7f0820df4134e62a481a4dcb01bbde9492 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Thu, 6 Oct 2022 15:56:23 -0700 Subject: [PATCH 20/36] write token to disk when --check-device-code Signed-off-by: Matthew Fisher --- src/commands/login.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 63766d3c02..0f0e77411f 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -185,22 +185,30 @@ impl LoginCommand { return Ok(()); } + let token: TokenInfo; if self.check_device_code.is_some() { - let status = match &check_device_code( + match check_device_code( &Client::new(connection_config), self.check_device_code.unwrap(), ) .await { - Ok(d) => serde_json::to_string_pretty(d)?, - Err(_) => serde_json::to_string_pretty(&json!({ "status": "waiting" }))?, + Ok(token_info) => { + println!("{}", serde_json::to_string_pretty(&token_info)?); + token = token_info; + } + Err(_) => { + println!( + "{}", + serde_json::to_string_pretty(&json!({ "status": "waiting" }))? + ); + return Ok(()); + } }; - println!("{}", status); - return Ok(()); + } else { + token = github_token(connection_config).await?; } - let token = github_token(connection_config).await?; - let login_connection = LoginConnection { url: DEFAULT_CLOUD_URL.to_owned(), danger_accept_invalid_certs: self.insecure, From 38a5e5f985cc498e6d86f97f53aa095497e1e924 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Thu, 6 Oct 2022 16:48:57 -0700 Subject: [PATCH 21/36] check for regular response Signed-off-by: Matthew Fisher --- src/commands/login.rs | 44 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 0f0e77411f..25150faa1c 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -186,23 +186,23 @@ impl LoginCommand { } let token: TokenInfo; - if self.check_device_code.is_some() { - match check_device_code( - &Client::new(connection_config), - self.check_device_code.unwrap(), - ) - .await - { + if let Some(device_code) = self.check_device_code { + let client = Client::new(connection_config); + match client.login(device_code).await { Ok(token_info) => { - println!("{}", serde_json::to_string_pretty(&token_info)?); - token = token_info; + if token_info.token.is_some() { + println!("{}", serde_json::to_string_pretty(&token_info)?); + token = token_info; + } else { + println!( + "{}", + serde_json::to_string_pretty(&json!({ "status": "waiting" }))? + ); + return Ok(()); + } } - Err(_) => { - println!( - "{}", - serde_json::to_string_pretty(&json!({ "status": "waiting" }))? - ); - return Ok(()); + Err(e) => { + return Err(e); } }; } else { @@ -256,7 +256,7 @@ async fn github_token( bail!("Timed out waiting to authorize the device. Please execute `spin login` again and authorize the device with GitHub."); } - match check_device_code(&client, device_code.device_code.clone().unwrap()).await { + match client.login(device_code.device_code.clone().unwrap()).await { Ok(response) => { println!("Device authorized!"); return Ok(response); @@ -276,18 +276,6 @@ async fn create_device_code(client: &Client) -> Result { .await } -async fn check_device_code(client: &Client, device_code: String) -> Result { - match client.login(device_code).await { - Ok(response) => { - if response.token != None { - return Ok(response); - } - return Err(anyhow::anyhow!("no token info")); - } - Err(e) => Err(e), - } -} - #[derive(Clone, Serialize, Deserialize)] pub struct LoginConnection { pub url: String, From 9f80d71dc06b5beac5de17e8205264e7c4fb66c0 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Fri, 7 Oct 2022 08:45:59 -0700 Subject: [PATCH 22/36] prompt for authentication method Signed-off-by: Matthew Fisher --- src/commands/login.rs | 136 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 26 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 25150faa1c..3a6a152736 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -1,3 +1,4 @@ +use std::io::stdin; use std::path::PathBuf; use std::time::Duration; @@ -139,36 +140,91 @@ impl LoginCommand { return Ok(()); } - if self.hippo_username.is_some() { - // log in with username/password - let token = match HippoClient::login( - &HippoClient::new(ConnectionInfo { - url: self.hippo_server_url.as_deref().unwrap().to_string(), + if let Some(url) = self.hippo_server_url { + let login_connection: LoginConnection; + // prompt the user for the authentication method + let auth_method = prompt_for_auth_method(); + if auth_method == AuthMethod::UsernameAndPassword { + // log in with username/password + let token = match HippoClient::login( + &HippoClient::new(ConnectionInfo { + url: url.clone(), + danger_accept_invalid_certs: self.insecure, + api_key: None, + }), + self.hippo_username.as_deref().unwrap().to_string(), + self.hippo_password.as_deref().unwrap().to_string(), + ) + .await + { + Ok(token_info) => token_info, + Err(err) => bail!(format_login_error(&err)?), + }; + + login_connection = LoginConnection { + url, danger_accept_invalid_certs: self.insecure, - api_key: None, - }), - self.hippo_username.as_deref().unwrap().to_string(), - self.hippo_password.as_deref().unwrap().to_string(), - ) - .await - { - Ok(token_info) => token_info, - Err(err) => bail!(format_login_error(&err)?), - }; + token: token.token.unwrap_or_default(), + expiration: token.expiration.unwrap_or_default(), + bindle_url: self.bindle_server_url, + bindle_username: self.bindle_username, + bindle_password: self.bindle_password, + }; + } else { + // log in to the cloud API + let connection_config = ConnectionConfig { + url, + insecure: self.insecure, + token: Default::default(), + }; - let login_connection = LoginConnection { - url: self.hippo_server_url.unwrap(), - danger_accept_invalid_certs: self.insecure, - token: token.token.unwrap_or_default(), - expiration: token.expiration.unwrap_or_default(), - bindle_url: self.bindle_server_url, - bindle_username: self.bindle_username, - bindle_password: self.bindle_password, - }; + if self.get_device_code { + println!( + "{}", + serde_json::to_string_pretty( + &create_device_code(&Client::new(connection_config)).await? + )? + ); + return Ok(()); + } + + let token: TokenInfo; + if let Some(device_code) = self.check_device_code { + let client = Client::new(connection_config); + match client.login(device_code).await { + Ok(token_info) => { + if token_info.token.is_some() { + println!("{}", serde_json::to_string_pretty(&token_info)?); + token = token_info; + } else { + println!( + "{}", + serde_json::to_string_pretty(&json!({ "status": "waiting" }))? + ); + return Ok(()); + } + } + Err(e) => { + return Err(e); + } + }; + } else { + token = github_token(connection_config).await?; + } + login_connection = LoginConnection { + url: DEFAULT_CLOUD_URL.to_owned(), + danger_accept_invalid_certs: self.insecure, + token: token.token.unwrap_or_default(), + expiration: token.expiration.unwrap_or_default(), + bindle_url: None, + bindle_username: None, + bindle_password: None, + }; + } std::fs::write(path, serde_json::to_string_pretty(&login_connection)?)?; } else { - // log in to the cloud API + // log in to the default cloud API let connection_config = ConnectionConfig { url: DEFAULT_CLOUD_URL.to_owned(), insecure: self.insecure, @@ -218,7 +274,6 @@ impl LoginCommand { bindle_username: None, bindle_password: None, }; - std::fs::write(path, serde_json::to_string_pretty(&login_connection)?)?; } @@ -336,3 +391,32 @@ fn ensure(root: &PathBuf) -> Result<()> { Ok(()) } + +#[derive(PartialEq)] +enum AuthMethod { + Github, + UsernameAndPassword, +} + +fn prompt_for_auth_method() -> AuthMethod { + loop { + // prompt the user for the authentication method + print!("What authentication method does this server support?\n\n1. Sign in with GitHub\n2. Sign in with a username and password\n\nEnter a number: "); + let mut input = String::new(); + stdin() + .read_line(&mut input) + .expect("unable to read user input"); + + match input.trim() { + "1" => { + return AuthMethod::Github; + } + "2" => { + return AuthMethod::UsernameAndPassword; + } + _ => { + println!("invalid input. Please enter either 1 or 2."); + } + } + } +} From 5aca581fe21c0de0cc7dcea88dadf107474a183d Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Fri, 7 Oct 2022 08:53:24 -0700 Subject: [PATCH 23/36] prompt for username/password Signed-off-by: Matthew Fisher --- Cargo.lock | 11 +++++++++++ Cargo.toml | 1 + src/commands/login.rs | 25 +++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 533dc88950..757f8a2ba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3345,6 +3345,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "rpassword" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b763cb66df1c928432cc35053f8bd4cec3335d8559fc16010017d16b3c1680" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -3886,6 +3896,7 @@ dependencies = [ "path-absolutize", "regex", "reqwest", + "rpassword", "semver 1.0.14", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 51f07c2f9d..40a0640564 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ outbound-redis = { path = "crates/outbound-redis" } path-absolutize = "3.0.11" regex = "1.5.5" reqwest = { version = "0.11", features = ["stream"] } +rpassword = "7.0" semver = "1.0" serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0.82" diff --git a/src/commands/login.rs b/src/commands/login.rs index 3a6a152736..219cbcdc37 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -145,6 +145,27 @@ impl LoginCommand { // prompt the user for the authentication method let auth_method = prompt_for_auth_method(); if auth_method == AuthMethod::UsernameAndPassword { + let username = match self.hippo_username { + Some(username) => username, + None => { + print!("Hippo username: "); + let mut input = String::new(); + stdin() + .read_line(&mut input) + .expect("unable to read user input"); + input.trim().to_owned() + } + }; + let password = match self.hippo_password { + Some(password) => password, + None => { + print!("Hippo pasword: "); + rpassword::read_password() + .expect("unable to read user input") + .trim() + .to_owned() + } + }; // log in with username/password let token = match HippoClient::login( &HippoClient::new(ConnectionInfo { @@ -152,8 +173,8 @@ impl LoginCommand { danger_accept_invalid_certs: self.insecure, api_key: None, }), - self.hippo_username.as_deref().unwrap().to_string(), - self.hippo_password.as_deref().unwrap().to_string(), + username, + password, ) .await { From bb26e62c2c975bb5351c4c80c64ecdeda1f9b088 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Fri, 7 Oct 2022 09:05:57 -0700 Subject: [PATCH 24/36] reintegrate sloth warnings Signed-off-by: Matthew Fisher --- src/commands/deploy.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 1d4c77fcaa..602006ef97 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -116,8 +116,10 @@ impl DeployCommand { login_connection = serde_json::from_str(&new_data)?; } + let sloth_warning = warn_if_slow_response(&login_connection.url); check_healthz(&login_connection.url).await?; - let _sloth_warning = warn_if_slow_response(&login_connection.url); + // Hippo has responded - we don't want to keep the sloth timer running. + drop(sloth_warning); // TODO: we should have a smarter check in place here to determine the difference between Hippo and the Cloud APIs if login_connection.bindle_url.is_some() { @@ -140,6 +142,8 @@ impl DeployCommand { None }; + let sloth_warning = warn_if_slow_response(login_connection.bindle_url.as_deref().unwrap()); + let bindle_connection_info = BindleConnectionInfo::new( login_connection.bindle_url.unwrap(), login_connection.danger_accept_invalid_certs, @@ -151,6 +155,9 @@ impl DeployCommand { .create_and_push_bindle(buildinfo, bindle_connection_info) .await?; + // Bindle has responded - we don't want to keep the sloth timer running. + drop(sloth_warning); + let hippo_client = Client::new(ConnectionInfo { url: login_connection.url.clone(), danger_accept_invalid_certs: login_connection.danger_accept_invalid_certs, From 201933320fe0b0c29396e579bcadb68a68e439e0 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Fri, 7 Oct 2022 09:37:57 -0700 Subject: [PATCH 25/36] return tcp connect errors Signed-off-by: Matthew Fisher --- src/commands/login.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 219cbcdc37..bc61803381 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -376,15 +376,17 @@ struct LoginHippoError { } fn format_login_error(err: &anyhow::Error) -> anyhow::Result { - let error: LoginHippoError = serde_json::from_str(err.to_string().as_str())?; - if error.detail.ends_with(": ") { - Ok(format!( - "Problem logging into Hippo: {}", - error.detail.replace(": ", ".") - )) - } else { - Ok(format!("Problem logging into Hippo: {}", error.detail)) - } + let detail = match serde_json::from_str::(err.to_string().as_str()) { + Ok(e) => { + if e.detail.ends_with(": ") { + e.detail.replace(": ", ".") + } else { + e.detail + } + } + Err(_) => err.to_string(), + }; + Ok(format!("Problem logging into Hippo: {}", detail)) } /// Ensure the root directory exists, or else create it. From 1948d6d36c29b1e2fea8fae68c873404defc7e1a Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Fri, 7 Oct 2022 09:52:39 -0700 Subject: [PATCH 26/36] fix patch APIs Signed-off-by: Matthew Fisher --- Cargo.lock | 85 +++++++++++++++++++++----------------- crates/cloud/src/client.rs | 32 +++----------- src/commands/deploy.rs | 4 -- 3 files changed, 51 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 757f8a2ba9..d38c6226ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,15 +76,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.65" @@ -230,7 +221,7 @@ dependencies = [ "sled", "tempfile", "thiserror", - "time 0.3.14", + "time 0.3.15", "tokio", "tokio-stream", "tokio-tar", @@ -558,7 +549,7 @@ dependencies = [ [[package]] name = "cloud-openapi" version = "0.1.0" -source = "git+https://github.com/fermyon/cloud-openapi#2eba9b67688771d9d44a065a3c437ae261f662d8" +source = "git+https://github.com/fermyon/cloud-openapi#350f67d7abf3cab9da83a44ba93ff5bfc2ad8b29" dependencies = [ "reqwest", "serde", @@ -1123,9 +1114,9 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "dunce" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" +checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c" [[package]] name = "ed25519" @@ -1696,7 +1687,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa 1.0.3", + "itoa 1.0.4", ] [[package]] @@ -1743,7 +1734,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.3", + "itoa 1.0.4", "pin-project-lite", "socket2", "tokio", @@ -1888,9 +1879,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" [[package]] name = "ittapi-rs" @@ -2379,6 +2370,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num" version = "0.4.0" @@ -2695,6 +2696,12 @@ dependencies = [ "wit-bindgen-wasmtime", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking" version = "2.0.0" @@ -3197,7 +3204,7 @@ dependencies = [ "bytes", "combine", "futures-util", - "itoa 1.0.3", + "itoa 1.0.4", "percent-encoding", "pin-project-lite", "ryu", @@ -3370,7 +3377,7 @@ dependencies = [ "bitflags", "errno", "io-lifetimes", - "itoa 1.0.3", + "itoa 1.0.4", "libc", "linux-raw-sys", "once_cell", @@ -3577,7 +3584,7 @@ version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ - "itoa 1.0.3", + "itoa 1.0.4", "ryu", "serde", ] @@ -3609,7 +3616,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.3", + "itoa 1.0.4", "ryu", "serde", ] @@ -3714,9 +3721,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.6.3" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb766570a2825fa972bceff0d195727876a9cdf2460ab2e52d455dc2de47fd9" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "simple_asn1" @@ -3727,7 +3734,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time 0.3.14", + "time 0.3.15", ] [[package]] @@ -4279,9 +4286,9 @@ checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" [[package]] name = "syn" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" +checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" dependencies = [ "proc-macro2", "quote", @@ -4423,11 +4430,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" dependencies = [ - "itoa 1.0.3", + "itoa 1.0.4", "libc", "num_threads", "serde", @@ -4626,9 +4633,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.36" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "log", @@ -4639,9 +4646,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", @@ -4650,9 +4657,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", "valuable", @@ -4681,12 +4688,12 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" dependencies = [ - "ansi_term", "matchers", + "nu-ansi-term", "once_cell", "regex", "sharded-slab", @@ -4815,7 +4822,7 @@ dependencies = [ "git2", "rustversion", "thiserror", - "time 0.3.14", + "time 0.3.15", ] [[package]] diff --git a/crates/cloud/src/client.rs b/crates/cloud/src/client.rs index 886aac5875..b4d8f17419 100644 --- a/crates/cloud/src/client.rs +++ b/crates/cloud/src/client.rs @@ -1,7 +1,6 @@ use anyhow::{Context, Result}; use cloud_openapi::{ apis::{ - accounts_api::api_accounts_post, apps_api::{api_apps_get, api_apps_id_delete, api_apps_post}, auth_tokens_api::api_auth_tokens_post, channels_api::{ @@ -15,10 +14,10 @@ use cloud_openapi::{ }, models::{ AppItemPage, ChannelItem, ChannelItemPage, ChannelRevisionSelectionStrategy, - ChannelRevisionSelectionStrategyField, CreateAccountCommand, CreateAppCommand, - CreateChannelCommand, CreateDeviceCodeCommand, CreateTokenCommand, DeviceCodeItem, - GetChannelLogsVm, GuidNullableField, PatchChannelCommand, RegisterRevisionCommand, - RevisionItemPage, StringField, TokenInfo, UpdateEnvironmentVariableDto, + ChannelRevisionSelectionStrategyField, CreateAppCommand, CreateChannelCommand, + CreateDeviceCodeCommand, CreateTokenCommand, DeviceCodeItem, GetChannelLogsVm, + GuidNullableField, PatchChannelCommand, RegisterRevisionCommand, RevisionItemPage, + StringField, TokenInfo, UpdateEnvironmentVariableDto, UpdateEnvironmentVariableDtoListField, }, }; @@ -86,18 +85,6 @@ impl Client { .map_err(format_response_error) } - pub async fn register(&self, username: String, password: String) -> Result { - api_accounts_post( - &self.configuration, - Some(CreateAccountCommand { - user_name: username, - password, - }), - ) - .await - .map_err(format_response_error) - } - pub async fn login(&self, token: String) -> Result { // When the new OpenAPI specification is released, manually crafting // the request should no longer be necessary. @@ -170,20 +157,16 @@ impl Client { &self, app_id: Uuid, name: String, - domain: Option, revision_selection_strategy: ChannelRevisionSelectionStrategy, range_rule: Option, active_revision_id: Option, - certificate_id: Option, ) -> anyhow::Result { let command = CreateChannelCommand { app_id, name, - domain, revision_selection_strategy, range_rule, active_revision_id, - certificate_id, }; api_channels_post(&self.configuration, Some(command)) .await @@ -196,25 +179,20 @@ impl Client { &self, id: Uuid, name: Option, - domain: Option, revision_selection_strategy: Option, range_rule: Option, active_revision_id: Option, - certificate_id: Option, environment_variables: Option>, ) -> anyhow::Result<()> { let command = PatchChannelCommand { channel_id: Some(id), name: name.map(|n| Box::new(StringField { value: Some(n) })), - domain: domain.map(|d| Box::new(StringField { value: Some(d) })), revision_selection_strategy: revision_selection_strategy .map(|r| Box::new(ChannelRevisionSelectionStrategyField { value: Some(r) })), range_rule: range_rule.map(|r| Box::new(StringField { value: Some(r) })), active_revision_id: active_revision_id .map(|r| Box::new(GuidNullableField { value: Some(r) })), - certificate_id: certificate_id.map(|c| Box::new(GuidNullableField { value: Some(c) })), - environment_variables: environment_variables - .map(|e| Box::new(UpdateEnvironmentVariableDtoListField { value: Some(e) })), + environment_variables, }; api_channels_id_patch(&self.configuration, &id.to_string(), Some(command)) diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 602006ef97..7948a09ed1 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -312,12 +312,10 @@ impl DeployCommand { &client, existing_channel_id, None, - None, Some(CloudChannelRevisionSelectionStrategy::UseSpecifiedRevision), None, Some(active_revision_id), None, - None, ) .await .context("Problem patching a channel")?; @@ -333,11 +331,9 @@ impl DeployCommand { &client, app_id, String::from(SPIN_DEPLOY_CHANNEL_NAME), - None, CloudChannelRevisionSelectionStrategy::UseRangeRule, range_rule, None, - None, ) .await .context("Problem creating a channel")? From 742908bbab8d496f2438e6919683d58143dd64e3 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Fri, 7 Oct 2022 10:52:55 -0700 Subject: [PATCH 27/36] DRY up the login logic Signed-off-by: Matthew Fisher --- src/commands/login.rs | 163 +++++++++++++++--------------------------- 1 file changed, 59 insertions(+), 104 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index bc61803381..202c380996 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -140,114 +140,69 @@ impl LoginCommand { return Ok(()); } - if let Some(url) = self.hippo_server_url { - let login_connection: LoginConnection; - // prompt the user for the authentication method - let auth_method = prompt_for_auth_method(); - if auth_method == AuthMethod::UsernameAndPassword { - let username = match self.hippo_username { - Some(username) => username, - None => { - print!("Hippo username: "); - let mut input = String::new(); - stdin() - .read_line(&mut input) - .expect("unable to read user input"); - input.trim().to_owned() - } - }; - let password = match self.hippo_password { - Some(password) => password, - None => { - print!("Hippo pasword: "); - rpassword::read_password() - .expect("unable to read user input") - .trim() - .to_owned() - } - }; - // log in with username/password - let token = match HippoClient::login( - &HippoClient::new(ConnectionInfo { - url: url.clone(), - danger_accept_invalid_certs: self.insecure, - api_key: None, - }), - username, - password, - ) - .await - { - Ok(token_info) => token_info, - Err(err) => bail!(format_login_error(&err)?), - }; + let login_connection: LoginConnection; + let mut url = DEFAULT_CLOUD_URL.to_owned(); + let mut auth_method = AuthMethod::Github; - login_connection = LoginConnection { - url, - danger_accept_invalid_certs: self.insecure, - token: token.token.unwrap_or_default(), - expiration: token.expiration.unwrap_or_default(), - bindle_url: self.bindle_server_url, - bindle_username: self.bindle_username, - bindle_password: self.bindle_password, - }; - } else { - // log in to the cloud API - let connection_config = ConnectionConfig { - url, - insecure: self.insecure, - token: Default::default(), - }; + if let Some(u) = self.hippo_server_url { + url = u; + // prompt the user for the authentication method + // TODO: implement a server "feature" check that tells us what authentication methods it supports + auth_method = prompt_for_auth_method(); + } - if self.get_device_code { - println!( - "{}", - serde_json::to_string_pretty( - &create_device_code(&Client::new(connection_config)).await? - )? - ); - return Ok(()); + // login and populate login_connection based on the auth type + if auth_method == AuthMethod::UsernameAndPassword { + let username = match self.hippo_username { + Some(username) => username, + None => { + print!("Hippo username: "); + let mut input = String::new(); + stdin() + .read_line(&mut input) + .expect("unable to read user input"); + input.trim().to_owned() } - - let token: TokenInfo; - if let Some(device_code) = self.check_device_code { - let client = Client::new(connection_config); - match client.login(device_code).await { - Ok(token_info) => { - if token_info.token.is_some() { - println!("{}", serde_json::to_string_pretty(&token_info)?); - token = token_info; - } else { - println!( - "{}", - serde_json::to_string_pretty(&json!({ "status": "waiting" }))? - ); - return Ok(()); - } - } - Err(e) => { - return Err(e); - } - }; - } else { - token = github_token(connection_config).await?; + }; + let password = match self.hippo_password { + Some(password) => password, + None => { + print!("Hippo pasword: "); + rpassword::read_password() + .expect("unable to read user input") + .trim() + .to_owned() } - - login_connection = LoginConnection { - url: DEFAULT_CLOUD_URL.to_owned(), + }; + // log in with username/password + let token = match HippoClient::login( + &HippoClient::new(ConnectionInfo { + url: url.clone(), danger_accept_invalid_certs: self.insecure, - token: token.token.unwrap_or_default(), - expiration: token.expiration.unwrap_or_default(), - bindle_url: None, - bindle_username: None, - bindle_password: None, - }; - } - std::fs::write(path, serde_json::to_string_pretty(&login_connection)?)?; + api_key: None, + }), + username, + password, + ) + .await + { + Ok(token_info) => token_info, + Err(err) => bail!(format_login_error(&err)?), + }; + + login_connection = LoginConnection { + url: url.clone(), + danger_accept_invalid_certs: self.insecure, + token: token.token.unwrap_or_default(), + expiration: token.expiration.unwrap_or_default(), + bindle_url: self.bindle_server_url, + bindle_username: self.bindle_username, + bindle_password: self.bindle_password, + }; } else { - // log in to the default cloud API + // log in to the cloud API let connection_config = ConnectionConfig { - url: DEFAULT_CLOUD_URL.to_owned(), + url: url.clone(), insecure: self.insecure, token: Default::default(), }; @@ -286,8 +241,8 @@ impl LoginCommand { token = github_token(connection_config).await?; } - let login_connection = LoginConnection { - url: DEFAULT_CLOUD_URL.to_owned(), + login_connection = LoginConnection { + url, danger_accept_invalid_certs: self.insecure, token: token.token.unwrap_or_default(), expiration: token.expiration.unwrap_or_default(), @@ -295,9 +250,9 @@ impl LoginCommand { bindle_username: None, bindle_password: None, }; - std::fs::write(path, serde_json::to_string_pretty(&login_connection)?)?; } + std::fs::write(path, serde_json::to_string_pretty(&login_connection)?)?; Ok(()) } } From 2196cdecddf450c97cb9eea60e9aa5f8bfe52a64 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Tue, 11 Oct 2022 14:51:54 -0700 Subject: [PATCH 28/36] fix auth method issues with --check-device-code Signed-off-by: Matthew Fisher --- src/commands/login.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 202c380996..52f9e8fb1d 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -120,6 +120,15 @@ pub struct LoginCommand { takes_value = false )] pub check_device_code: Option, + + // authentication method used for logging in (username|github) + #[clap( + name = "auth-method", + long = "auth-method", + env = "AUTH_METHOD", + default_value = "github" + )] + pub method: String, } impl LoginCommand { @@ -141,14 +150,21 @@ impl LoginCommand { } let login_connection: LoginConnection; - let mut url = DEFAULT_CLOUD_URL.to_owned(); let mut auth_method = AuthMethod::Github; + let mut url = DEFAULT_CLOUD_URL.to_owned(); if let Some(u) = self.hippo_server_url { url = u; - // prompt the user for the authentication method - // TODO: implement a server "feature" check that tells us what authentication methods it supports - auth_method = prompt_for_auth_method(); + + if self.get_device_code || self.check_device_code.is_some() || self.method == "github" { + auth_method = AuthMethod::Github; + } else if self.method == "username" { + auth_method = AuthMethod::UsernameAndPassword; + } else { + // prompt the user for the authentication method + // TODO: implement a server "feature" check that tells us what authentication methods it supports + auth_method = prompt_for_auth_method(); + } } // login and populate login_connection based on the auth type From cecf0ac7af5ad51718e2684cecdb54ff0f1e91fc Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Tue, 11 Oct 2022 15:01:53 -0700 Subject: [PATCH 29/36] report error on invalid --method Signed-off-by: Matthew Fisher --- src/commands/login.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 52f9e8fb1d..19f2dbcf39 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -126,9 +126,8 @@ pub struct LoginCommand { name = "auth-method", long = "auth-method", env = "AUTH_METHOD", - default_value = "github" )] - pub method: String, + pub method: Option, } impl LoginCommand { @@ -156,11 +155,15 @@ impl LoginCommand { if let Some(u) = self.hippo_server_url { url = u; - if self.get_device_code || self.check_device_code.is_some() || self.method == "github" { - auth_method = AuthMethod::Github; - } else if self.method == "username" { - auth_method = AuthMethod::UsernameAndPassword; - } else { + if let Some(method) = self.method { + if method == "username" { + auth_method = AuthMethod::UsernameAndPassword; + } else if method == "github" { + auth_method = AuthMethod::Github; + } else { + bail!("invalid auth method: {}\nvalid options: [username, github]", method); + } + } else if !self.get_device_code && self.check_device_code.is_none() { // prompt the user for the authentication method // TODO: implement a server "feature" check that tells us what authentication methods it supports auth_method = prompt_for_auth_method(); From 483788a404137e3cb77f121791acf5ce997c32b1 Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Tue, 11 Oct 2022 15:04:23 -0700 Subject: [PATCH 30/36] check token info is none Signed-off-by: Matthew Fisher --- src/commands/login.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 19f2dbcf39..3eb1d58187 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -308,8 +308,14 @@ async fn github_token( match client.login(device_code.device_code.clone().unwrap()).await { Ok(response) => { - println!("Device authorized!"); - return Ok(response); + if response.token.is_none() { + println!("Waiting for device authorization..."); + tokio::time::sleep(Duration::from_secs(POLL_INTERVAL_SECS)).await; + seconds_elapsed += POLL_INTERVAL_SECS; + } else { + println!("Device authorized!"); + return Ok(response); + } } Err(_) => { println!("Waiting for device authorization..."); From 83e1ed3e5ec456bc398377fa15ab39bde3d885bc Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Tue, 11 Oct 2022 15:36:38 -0700 Subject: [PATCH 31/36] HACK: manually patch the PatchChannelsCommand Signed-off-by: Matthew Fisher --- crates/cloud/src/client.rs | 83 ++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 13 deletions(-) diff --git a/crates/cloud/src/client.rs b/crates/cloud/src/client.rs index b4d8f17419..0177f7f81a 100644 --- a/crates/cloud/src/client.rs +++ b/crates/cloud/src/client.rs @@ -5,18 +5,18 @@ use cloud_openapi::{ auth_tokens_api::api_auth_tokens_post, channels_api::{ api_channels_get, api_channels_id_delete, api_channels_id_get, - api_channels_id_logs_get, api_channels_id_patch, api_channels_post, + api_channels_id_logs_get, api_channels_id_patch, api_channels_post, ApiChannelsIdPatchError, }, configuration::{ApiKey, Configuration}, device_codes_api::api_device_codes_post, revisions_api::{api_revisions_get, api_revisions_post}, - Error, + Error, self, ResponseContent, }, models::{ AppItemPage, ChannelItem, ChannelItemPage, ChannelRevisionSelectionStrategy, ChannelRevisionSelectionStrategyField, CreateAppCommand, CreateChannelCommand, CreateDeviceCodeCommand, CreateTokenCommand, DeviceCodeItem, GetChannelLogsVm, - GuidNullableField, PatchChannelCommand, RegisterRevisionCommand, RevisionItemPage, + GuidNullableField, RegisterRevisionCommand, RevisionItemPage, StringField, TokenInfo, UpdateEnvironmentVariableDto, UpdateEnvironmentVariableDtoListField, }, @@ -184,20 +184,48 @@ impl Client { active_revision_id: Option, environment_variables: Option>, ) -> anyhow::Result<()> { - let command = PatchChannelCommand { + let patch_channel_command = PatchChannelCommand { channel_id: Some(id), - name: name.map(|n| Box::new(StringField { value: Some(n) })), - revision_selection_strategy: revision_selection_strategy - .map(|r| Box::new(ChannelRevisionSelectionStrategyField { value: Some(r) })), - range_rule: range_rule.map(|r| Box::new(StringField { value: Some(r) })), - active_revision_id: active_revision_id - .map(|r| Box::new(GuidNullableField { value: Some(r) })), + name, + revision_selection_strategy, + range_rule, + active_revision_id, environment_variables, }; - api_channels_id_patch(&self.configuration, &id.to_string(), Some(command)) - .await - .map_err(format_response_error) + let local_var_configuration = &self.configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!("{}/api/channels/{id}", local_var_configuration.base_path, id=apis::urlencode(id.to_string())); + let mut local_var_req_builder = local_var_client.request(reqwest::Method::PATCH, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + if let Some(ref local_var_apikey) = local_var_configuration.api_key { + let local_var_key = local_var_apikey.key.clone(); + let local_var_value = match local_var_apikey.prefix { + Some(ref local_var_prefix) => format!("{} {}", local_var_prefix, local_var_key), + None => local_var_key, + }; + local_var_req_builder = local_var_req_builder.header("Authorization", local_var_value); + }; + local_var_req_builder = local_var_req_builder.json(&patch_channel_command); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + Ok(()) + } else { + let local_var_entity: Option = serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { status: local_var_status, content: local_var_content, entity: local_var_entity }; + Err(format_response_error(Error::ResponseError(local_var_error))) + } } pub async fn remove_channel(&self, id: String) -> Result<()> { @@ -255,3 +283,32 @@ fn format_response_error(e: Error) -> anyhow::Error { _ => anyhow::anyhow!(e.to_string()), } } + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct PatchChannelCommand { + #[serde(rename = "channelId", skip_serializing_if = "Option::is_none")] + pub channel_id: Option, + #[serde(rename = "environmentVariables", skip_serializing_if = "Option::is_none")] + pub environment_variables: Option>, + #[serde(rename = "name", skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(rename = "revisionSelectionStrategy", skip_serializing_if = "Option::is_none")] + pub revision_selection_strategy: Option, + #[serde(rename = "rangeRule", skip_serializing_if = "Option::is_none")] + pub range_rule: Option, + #[serde(rename = "activeRevisionId", skip_serializing_if = "Option::is_none")] + pub active_revision_id: Option, +} + +impl PatchChannelCommand { + pub fn new() -> PatchChannelCommand { + PatchChannelCommand { + channel_id: None, + environment_variables: None, + name: None, + revision_selection_strategy: None, + range_rule: None, + active_revision_id: None, + } + } +} \ No newline at end of file From 5c531506c817d59030028392a0a75a9fa08d1feb Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Tue, 11 Oct 2022 15:41:39 -0700 Subject: [PATCH 32/36] cargo fmt Signed-off-by: Matthew Fisher --- crates/cloud/src/client.rs | 44 +++++++++++++++++++++++++++----------- src/commands/login.rs | 11 +++++----- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/crates/cloud/src/client.rs b/crates/cloud/src/client.rs index 0177f7f81a..914e1c8da6 100644 --- a/crates/cloud/src/client.rs +++ b/crates/cloud/src/client.rs @@ -1,24 +1,25 @@ use anyhow::{Context, Result}; use cloud_openapi::{ apis::{ + self, apps_api::{api_apps_get, api_apps_id_delete, api_apps_post}, auth_tokens_api::api_auth_tokens_post, channels_api::{ api_channels_get, api_channels_id_delete, api_channels_id_get, - api_channels_id_logs_get, api_channels_id_patch, api_channels_post, ApiChannelsIdPatchError, + api_channels_id_logs_get, api_channels_id_patch, api_channels_post, + ApiChannelsIdPatchError, }, configuration::{ApiKey, Configuration}, device_codes_api::api_device_codes_post, revisions_api::{api_revisions_get, api_revisions_post}, - Error, self, ResponseContent, + Error, ResponseContent, }, models::{ AppItemPage, ChannelItem, ChannelItemPage, ChannelRevisionSelectionStrategy, ChannelRevisionSelectionStrategyField, CreateAppCommand, CreateChannelCommand, CreateDeviceCodeCommand, CreateTokenCommand, DeviceCodeItem, GetChannelLogsVm, - GuidNullableField, RegisterRevisionCommand, RevisionItemPage, - StringField, TokenInfo, UpdateEnvironmentVariableDto, - UpdateEnvironmentVariableDtoListField, + GuidNullableField, RegisterRevisionCommand, RevisionItemPage, StringField, TokenInfo, + UpdateEnvironmentVariableDto, UpdateEnvironmentVariableDtoListField, }, }; use reqwest::header; @@ -197,11 +198,17 @@ impl Client { let local_var_client = &local_var_configuration.client; - let local_var_uri_str = format!("{}/api/channels/{id}", local_var_configuration.base_path, id=apis::urlencode(id.to_string())); - let mut local_var_req_builder = local_var_client.request(reqwest::Method::PATCH, local_var_uri_str.as_str()); + let local_var_uri_str = format!( + "{}/api/channels/{id}", + local_var_configuration.base_path, + id = apis::urlencode(id.to_string()) + ); + let mut local_var_req_builder = + local_var_client.request(reqwest::Method::PATCH, local_var_uri_str.as_str()); if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { - local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + local_var_req_builder = local_var_req_builder + .header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); } if let Some(ref local_var_apikey) = local_var_configuration.api_key { let local_var_key = local_var_apikey.key.clone(); @@ -222,8 +229,13 @@ impl Client { if !local_var_status.is_client_error() && !local_var_status.is_server_error() { Ok(()) } else { - let local_var_entity: Option = serde_json::from_str(&local_var_content).ok(); - let local_var_error = ResponseContent { status: local_var_status, content: local_var_content, entity: local_var_entity }; + let local_var_entity: Option = + serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { + status: local_var_status, + content: local_var_content, + entity: local_var_entity, + }; Err(format_response_error(Error::ResponseError(local_var_error))) } } @@ -288,11 +300,17 @@ fn format_response_error(e: Error) -> anyhow::Error { pub struct PatchChannelCommand { #[serde(rename = "channelId", skip_serializing_if = "Option::is_none")] pub channel_id: Option, - #[serde(rename = "environmentVariables", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "environmentVariables", + skip_serializing_if = "Option::is_none" + )] pub environment_variables: Option>, #[serde(rename = "name", skip_serializing_if = "Option::is_none")] pub name: Option, - #[serde(rename = "revisionSelectionStrategy", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "revisionSelectionStrategy", + skip_serializing_if = "Option::is_none" + )] pub revision_selection_strategy: Option, #[serde(rename = "rangeRule", skip_serializing_if = "Option::is_none")] pub range_rule: Option, @@ -311,4 +329,4 @@ impl PatchChannelCommand { active_revision_id: None, } } -} \ No newline at end of file +} diff --git a/src/commands/login.rs b/src/commands/login.rs index 3eb1d58187..e9e1fa5b11 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -122,11 +122,7 @@ pub struct LoginCommand { pub check_device_code: Option, // authentication method used for logging in (username|github) - #[clap( - name = "auth-method", - long = "auth-method", - env = "AUTH_METHOD", - )] + #[clap(name = "auth-method", long = "auth-method", env = "AUTH_METHOD")] pub method: Option, } @@ -161,7 +157,10 @@ impl LoginCommand { } else if method == "github" { auth_method = AuthMethod::Github; } else { - bail!("invalid auth method: {}\nvalid options: [username, github]", method); + bail!( + "invalid auth method: {}\nvalid options: [username, github]", + method + ); } } else if !self.get_device_code && self.check_device_code.is_none() { // prompt the user for the authentication method From 32355406f91ff2cd9442cdec2a7718220e589c2f Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Wed, 12 Oct 2022 10:22:14 -0700 Subject: [PATCH 33/36] simplify requires and auth mothod logic Signed-off-by: Matthew Fisher --- src/commands/login.rs | 48 +++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index e9e1fa5b11..2c4103d02d 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -35,9 +35,6 @@ pub struct LoginCommand { name = BINDLE_SERVER_URL_OPT, long = "bindle-server", env = BINDLE_URL_ENV, - requires = HIPPO_SERVER_URL_OPT, - requires = HIPPO_USERNAME, - requires = HIPPO_PASSWORD, )] pub bindle_server_url: Option, @@ -73,9 +70,6 @@ pub struct LoginCommand { name = HIPPO_SERVER_URL_OPT, long = "hippo-server", env = HIPPO_URL_ENV, - requires = BINDLE_SERVER_URL_OPT, - requires = HIPPO_USERNAME, - requires = HIPPO_PASSWORD, )] pub hippo_server_url: Option, @@ -84,8 +78,6 @@ pub struct LoginCommand { name = HIPPO_USERNAME, long = "hippo-username", env = HIPPO_USERNAME, - requires = BINDLE_SERVER_URL_OPT, - requires = HIPPO_SERVER_URL_OPT, requires = HIPPO_PASSWORD, )] pub hippo_username: Option, @@ -95,8 +87,6 @@ pub struct LoginCommand { name = HIPPO_PASSWORD, long = "hippo-password", env = HIPPO_PASSWORD, - requires = BINDLE_SERVER_URL_OPT, - requires = HIPPO_SERVER_URL_OPT, requires = HIPPO_USERNAME, )] pub hippo_password: Option, @@ -145,28 +135,32 @@ impl LoginCommand { } let login_connection: LoginConnection; - let mut auth_method = AuthMethod::Github; - let mut url = DEFAULT_CLOUD_URL.to_owned(); + let mut url = DEFAULT_CLOUD_URL.to_owned(); if let Some(u) = self.hippo_server_url { url = u; + } - if let Some(method) = self.method { - if method == "username" { - auth_method = AuthMethod::UsernameAndPassword; - } else if method == "github" { - auth_method = AuthMethod::Github; - } else { - bail!( - "invalid auth method: {}\nvalid options: [username, github]", - method - ); - } - } else if !self.get_device_code && self.check_device_code.is_none() { - // prompt the user for the authentication method - // TODO: implement a server "feature" check that tells us what authentication methods it supports - auth_method = prompt_for_auth_method(); + let auth_method: AuthMethod; + if let Some(method) = self.method { + if method == "username" { + auth_method = AuthMethod::UsernameAndPassword; + } else if method == "github" { + auth_method = AuthMethod::Github; + } else { + bail!( + "invalid auth method: {}\nvalid options: [username, github]", + method + ); } + } else if self.get_device_code || self.check_device_code.is_some() { + auth_method = AuthMethod::Github; + } else if self.hippo_username.is_some() || self.hippo_password.is_some() { + auth_method = AuthMethod::UsernameAndPassword; + } else { + // prompt the user for the authentication method + // TODO: implement a server "feature" check that tells us what authentication methods it supports + auth_method = prompt_for_auth_method(); } // login and populate login_connection based on the auth type From 2e3dc474b8c49cf7fbcc867b097818600b9d9cda Mon Sep 17 00:00:00 2001 From: Matthew Fisher Date: Wed, 12 Oct 2022 11:46:18 -0700 Subject: [PATCH 34/36] prompt only if --hippo-server is set Signed-off-by: Matthew Fisher --- src/commands/login.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 2c4103d02d..9455b326fd 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -136,11 +136,6 @@ impl LoginCommand { let login_connection: LoginConnection; - let mut url = DEFAULT_CLOUD_URL.to_owned(); - if let Some(u) = self.hippo_server_url { - url = u; - } - let auth_method: AuthMethod; if let Some(method) = self.method { if method == "username" { @@ -157,10 +152,17 @@ impl LoginCommand { auth_method = AuthMethod::Github; } else if self.hippo_username.is_some() || self.hippo_password.is_some() { auth_method = AuthMethod::UsernameAndPassword; - } else { + } else if self.hippo_server_url.is_some() { // prompt the user for the authentication method // TODO: implement a server "feature" check that tells us what authentication methods it supports auth_method = prompt_for_auth_method(); + } else { + auth_method = AuthMethod::Github; + } + + let mut url = DEFAULT_CLOUD_URL.to_owned(); + if let Some(u) = self.hippo_server_url { + url = u; } // login and populate login_connection based on the auth type From 8edc515bf64082d92c922b57d0162231b28b14eb Mon Sep 17 00:00:00 2001 From: itowlson Date: Thu, 13 Oct 2022 14:39:37 +1300 Subject: [PATCH 35/36] Update integration tests to use login Signed-off-by: itowlson --- tests/integration.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index 4518c2e39f..76199f6fcb 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -304,12 +304,7 @@ mod integration_tests { run( vec![ SPIN_BINARY, - "deploy", - "--file", - &format!( - "{}/{}", - RUST_HTTP_HEADERS_ENV_ROUTES_TEST, DEFAULT_MANIFEST_LOCATION - ), + "login", "--bindle-server", &bindle.url, "--hippo-server", @@ -322,6 +317,19 @@ mod integration_tests { None, None, )?; + run( + vec![ + SPIN_BINARY, + "deploy", + "--file", + &format!( + "{}/{}", + RUST_HTTP_HEADERS_ENV_ROUTES_TEST, DEFAULT_MANIFEST_LOCATION + ), + ], + None, + None, + )?; let apps_vm = hippo.client.list_apps().await?; assert_eq!(apps_vm.items.len(), 1, "hippo apps: {apps_vm:?}"); From f4d5737172cea4a21bff4990014704e4ba628ace Mon Sep 17 00:00:00 2001 From: itowlson Date: Thu, 13 Oct 2022 17:09:41 +1300 Subject: [PATCH 36/36] Minor refactoring of auth_method Signed-off-by: itowlson --- src/commands/login.rs | 56 ++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/commands/login.rs b/src/commands/login.rs index 9455b326fd..1dd405e9c6 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -112,8 +112,13 @@ pub struct LoginCommand { pub check_device_code: Option, // authentication method used for logging in (username|github) - #[clap(name = "auth-method", long = "auth-method", env = "AUTH_METHOD")] - pub method: Option, + #[clap( + name = "auth-method", + long = "auth-method", + env = "AUTH_METHOD", + arg_enum + )] + pub method: Option, } impl LoginCommand { @@ -136,29 +141,7 @@ impl LoginCommand { let login_connection: LoginConnection; - let auth_method: AuthMethod; - if let Some(method) = self.method { - if method == "username" { - auth_method = AuthMethod::UsernameAndPassword; - } else if method == "github" { - auth_method = AuthMethod::Github; - } else { - bail!( - "invalid auth method: {}\nvalid options: [username, github]", - method - ); - } - } else if self.get_device_code || self.check_device_code.is_some() { - auth_method = AuthMethod::Github; - } else if self.hippo_username.is_some() || self.hippo_password.is_some() { - auth_method = AuthMethod::UsernameAndPassword; - } else if self.hippo_server_url.is_some() { - // prompt the user for the authentication method - // TODO: implement a server "feature" check that tells us what authentication methods it supports - auth_method = prompt_for_auth_method(); - } else { - auth_method = AuthMethod::Github; - } + let auth_method = self.auth_method(); let mut url = DEFAULT_CLOUD_URL.to_owned(); if let Some(u) = self.hippo_server_url { @@ -269,6 +252,22 @@ impl LoginCommand { std::fs::write(path, serde_json::to_string_pretty(&login_connection)?)?; Ok(()) } + + fn auth_method(&self) -> AuthMethod { + if let Some(method) = &self.method { + method.clone() + } else if self.get_device_code || self.check_device_code.is_some() { + AuthMethod::Github + } else if self.hippo_username.is_some() || self.hippo_password.is_some() { + AuthMethod::UsernameAndPassword + } else if self.hippo_server_url.is_some() { + // prompt the user for the authentication method + // TODO: implement a server "feature" check that tells us what authentication methods it supports + prompt_for_auth_method() + } else { + AuthMethod::Github + } + } } async fn github_token( @@ -390,9 +389,12 @@ fn ensure(root: &PathBuf) -> Result<()> { Ok(()) } -#[derive(PartialEq)] -enum AuthMethod { +/// The method by which to authenticate the login. +#[derive(clap::ArgEnum, Clone, Debug, Eq, PartialEq)] +pub enum AuthMethod { + #[clap(name = "github")] Github, + #[clap(name = "username")] UsernameAndPassword, }