diff --git a/.circleci/config.yml b/.circleci/config.yml index 967baf82f..f21e0a9e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,10 +9,12 @@ executors: docker-rust: docker: - image: cimg/rust:1.65.0 + resource_class: small image-ubuntu: machine: image: ubuntu-2204:2022.04.1 docker_layer_caching: true + resource_class: xlarge # sscache steps are from this guide # https://medium.com/@edouard.oger/rust-caching-on-circleci-using-sccache-c996344f0115 @@ -86,18 +88,33 @@ commands: cat\<< EOF > ~/.cargo/config.toml [patch.crates-io] shuttle-service = { path = "$PWD/service" } + shuttle-runtime = { path = "$PWD/runtime" } + shuttle-aws-rds = { path = "$PWD/resources/aws-rds" } shuttle-persist = { path = "$PWD/resources/persist" } shuttle-shared-db = { path = "$PWD/resources/shared-db" } shuttle-secrets = { path = "$PWD/resources/secrets" } shuttle-static-folder = { path = "$PWD/resources/static-folder" } + + shuttle-axum = { path = "$PWD/services/shuttle-axum" } + shuttle-actix-web = { path = "$PWD/services/shuttle-actix-web" } + shuttle-next = { path = "$PWD/services/shuttle-next" } + shuttle-poem = { path = "$PWD/services/shuttle-poem" } + shuttle-poise = { path = "$PWD/services/shuttle-poise" } + shuttle-rocket = { path = "$PWD/services/shuttle-rocket" } + shuttle-salvo = { path = "$PWD/services/shuttle-salvo" } + shuttle-serenity = { path = "$PWD/services/shuttle-serenity" } + shuttle-thruster = { path = "$PWD/services/shuttle-thruster" } + shuttle-tide = { path = "$PWD/services/shuttle-tide" } + shuttle-tower = { path = "$PWD/services/shuttle-tower" } + shuttle-warp = { path = "$PWD/services/shuttle-warp" } EOF install-rust: steps: - run: name: Install Rust command: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --target add wasm32-wasi sudo apt update && sudo apt install -y libssl1.1 install-protoc: steps: @@ -146,33 +163,22 @@ commands: - << parameters.target >>.env jobs: - workspace-fmt: + workspace: executor: docker-rust + resource_class: xlarge steps: - checkout - restore-cargo-cache - install-protoc - run: cargo fmt --all --check - - run: cargo install cargo-sort # TODO: this is incompatible with workspace inheritance, uncomment when # https://github.com/DevinR528/cargo-sort/pull/29 is merged + # - run: cargo install cargo-sort # - run: cargo sort --check --workspace - - run: cargo check --workspace --all-targets - - save-cargo-cache - workspace-clippy: - parameters: - framework: - description: "Framework to activate" - type: string - executor: docker-rust - steps: - - checkout - - restore-cargo-cache - - install-protoc - run: | cargo clippy --tests \ --all-targets \ - --features="codegen,loader,<< parameters.framework >>" \ + --all-features \ --no-deps -- \ --D warnings \ -A clippy::let-unit-value \ @@ -190,9 +196,9 @@ jobs: - install-protoc - apply-patches - run: cargo fmt --all --check --manifest-path << parameters.path >>/Cargo.toml - - run: cargo install cargo-sort # TODO: this is incompatible with workspace inheritance, uncomment when # https://github.com/DevinR528/cargo-sort/pull/29 is merged + # - run: cargo install cargo-sort # - run: cargo sort --check << parameters.path >> - run: | cargo clippy --tests \ @@ -205,20 +211,6 @@ jobs: -A clippy::format-push-string - run: cargo test --all-features --manifest-path << parameters.path >>/Cargo.toml -- --nocapture - save-cargo-cache - service-test: - # Using an image since tests will start a docker container - executor: image-ubuntu - steps: - - install-rust - - checkout - - restore-cargo-cache - - run: - name: Run unit tests - command: cargo test --package shuttle-service --features="codegen,loader" --lib -- --nocapture - - run: - name: Run integration tests - command: cargo test --package shuttle-service --features="codegen,loader" --test '*' -- --nocapture - - save-cargo-cache platform-test: parameters: crate: @@ -228,6 +220,7 @@ jobs: executor: image-ubuntu steps: - install-rust + - install-protoc - checkout - run: git submodule sync - run: git submodule update --init @@ -244,7 +237,6 @@ jobs: (cargo test --package << parameters.crate >> --all-features --test '*' -- --list 2>&1 | grep -q "no test target matches pattern") && echo "nothing to test" || cargo test --package << parameters.crate >> --all-features --test '*' -- --nocapture - save-cargo-cache e2e-test: - resource_class: xlarge executor: image-ubuntu steps: - install-rust @@ -276,7 +268,6 @@ jobs: key: docker-buildx-{{ .Branch }} when: always build-and-push: - resource_class: xlarge executor: image-ubuntu steps: - checkout @@ -350,7 +341,7 @@ jobs: build-binaries-mac: macos: xcode: 12.5.1 - resource_class: medium + resource_class: xlarge steps: - checkout - run: @@ -388,28 +379,9 @@ jobs: workflows: ci: jobs: - - workspace-fmt - - workspace-clippy: - name: workspace-clippy-<< matrix.framework >> - requires: - - workspace-fmt - matrix: - parameters: - framework: - [ - "web-actix-web", - "web-axum", - "web-rocket", - "web-poem", - "web-thruster", - "web-tide", - "web-tower", - "web-warp", - "web-salvo", - "bot-serenity", - "bot-poise", - ] + - workspace - check-standalone: + name: << matrix.path >> matrix: parameters: path: @@ -418,27 +390,38 @@ workflows: - resources/secrets - resources/shared-db - resources/static-folder - - service-test: - requires: - - workspace-clippy + - services/shuttle-actix-web + - services/shuttle-axum + - services/shuttle-next + - services/shuttle-poem + - services/shuttle-poise + - services/shuttle-rocket + - services/shuttle-salvo + - services/shuttle-serenity + - services/shuttle-thruster + - services/shuttle-tide + - services/shuttle-tower + - services/shuttle-warp - platform-test: + name: << matrix.crate >> requires: - - workspace-clippy + - workspace matrix: parameters: crate: [ "shuttle-auth", - "shuttle-deployer", "cargo-shuttle", "shuttle-codegen", "shuttle-common", + "shuttle-deployer", "shuttle-proto", "shuttle-provisioner", + "shuttle-runtime", + "shuttle-service", ] - e2e-test: requires: - - service-test - platform-test - check-standalone filters: @@ -454,7 +437,7 @@ workflows: name: build-binaries-x86_64-gnu image: ubuntu-2204:2022.04.1 target: x86_64-unknown-linux-gnu - resource_class: medium + resource_class: xlarge filters: branches: only: production @@ -462,7 +445,7 @@ workflows: name: build-binaries-x86_64-musl image: ubuntu-2204:2022.04.1 target: x86_64-unknown-linux-musl - resource_class: medium + resource_class: xlarge filters: branches: only: production @@ -470,7 +453,7 @@ workflows: name: build-binaries-aarch64 image: ubuntu-2004:202101-01 target: aarch64-unknown-linux-musl - resource_class: arm.medium + resource_class: arm.xlarge filters: branches: only: production diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51c8a8511..e4569995c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,9 @@ git clone git@github.com:shuttle-hq/shuttle.git cd shuttle ``` +> Note: We need the git tags for the local development workflow, but they may not be included when you clone the repository. +To make sure you have them, run `git fetch upstream --tags`, where upstream is the name of the shuttle remote repository. + You should now be ready to setup a local environment to test code changes to core `shuttle` packages as follows: From the root of the shuttle repo, build the required images with: @@ -53,13 +56,26 @@ In order to test local changes to the library crates, you may want to add the be ```toml [patch.crates-io] shuttle-service = { path = "[base]/shuttle/service" } -shuttle-common = { path = "[base]/shuttle/common" } -shuttle-proto = { path = "[base]/shuttle/proto" } +shuttle-runtime = { path = "[base]/shuttle/runtime" } + shuttle-aws-rds = { path = "[base]/shuttle/resources/aws-rds" } shuttle-persist = { path = "[base]/shuttle/resources/persist" } shuttle-shared-db = { path = "[base]/shuttle/resources/shared-db" } shuttle-secrets = { path = "[base]/shuttle/resources/secrets" } shuttle-static-folder = { path = "[base]/shuttle/resources/static-folder" } + +shuttle-axum = { path = "[base]/shuttle/services/shuttle-axum" } +shuttle-actix-web = { path = "[base]/shuttle/services/shuttle-actix-web" } +shuttle-next = { path = "[base]/shuttle/services/shuttle-next" } +shuttle-poem = { path = "[base]/shuttle/services/shuttle-poem" } +shuttle-poise = { path = "[base]/shuttle/services/shuttle-poise" } +shuttle-rocket = { path = "[base]/shuttle/services/shuttle-rocket" } +shuttle-salvo = { path = "[base]/shuttle/services/shuttle-salvo" } +shuttle-serenity = { path = "[base]/shuttle/services/shuttle-serenity" } +shuttle-thruster = { path = "[base]/shuttle/services/shuttle-thruster" } +shuttle-tide = { path = "[base]/shuttle/services/shuttle-tide" } +shuttle-tower = { path = "[base]/shuttle/services/shuttle-tower" } +shuttle-warp = { path = "[base]/shuttle/services/shuttle-warp" } ``` Before we can login to our local instance of shuttle, we need to create a user. @@ -164,7 +180,7 @@ To run the unit tests for a specific crate, from the root of the repository run: cargo test --package --all-features --lib -- --nocapture ``` -To run the integration tests for a spesific crate (if it has any), from the root of the repository run: +To run the integration tests for a specific crate (if it has any), from the root of the repository run: ```bash # replace with the name of the crate to test, e.g. `cargo-shuttle` diff --git a/Cargo.lock b/Cargo.lock index e15c70b8a..25fc411c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453e534d4f46dcdddd7aa8619e9a664e153f34383d14710db0b0d76c2964db89" dependencies = [ - "base64 0.13.1", + "base64", "hyper", "openssl", "reqwest", @@ -21,182 +21,12 @@ dependencies = [ ] [[package]] -name = "actix-codec" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" -dependencies = [ - "bitflags", - "bytes 1.3.0", - "futures-core", - "futures-sink", - "log", - "memchr", - "pin-project-lite 0.2.9", - "tokio", - "tokio-util 0.7.3", -] - -[[package]] -name = "actix-http" -version = "3.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c83abf9903e1f0ad9973cc4f7b9767fd5a03a583f51a5b7a339e07987cd2724" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "ahash", - "base64 0.13.1", - "bitflags", - "brotli", - "bytes 1.3.0", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2", - "http 0.2.8", - "httparse", - "httpdate", - "itoa 1.0.2", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite 0.2.9", - "rand 0.8.5", - "sha1 0.10.4", - "smallvec", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" -dependencies = [ - "quote 1.0.21", - "syn 1.0.104", -] - -[[package]] -name = "actix-router" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" -dependencies = [ - "bytestring", - "http 0.2.8", - "regex", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ea16c295198e958ef31930a6ef37d0fb64e9ca3b6116e6b93a8bdae96ee1000" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da34f8e659ea1b077bb4637948b815cd3768ad5a188fdcd74ff4d84240cd824" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "num_cpus", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite 0.2.9", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite 0.2.9", -] - -[[package]] -name = "actix-web" -version = "4.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48f7b6534e06c7bfc72ee91db7917d4af6afe23e7d223b51e68fffbb21e96b9" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "ahash", - "bytes 1.3.0", - "bytestring", - "cfg-if 1.0.0", - "cookie 0.16.0", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "http 0.2.8", - "itoa 1.0.2", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite 0.2.9", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time 0.3.11", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.1.0" +name = "addr2line" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa9362663c8643d67b2d5eafba49e4cb2c8a053a29ed00a0bea121f17c76b13" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" dependencies = [ - "actix-router", - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "gimli", ] [[package]] @@ -205,102 +35,13 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "aead" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" -dependencies = [ - "generic-array", -] - -[[package]] -name = "aead" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" -dependencies = [ - "generic-array", -] - -[[package]] -name = "aes" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" -dependencies = [ - "aes-soft", - "aesni", - "cipher 0.2.5", -] - -[[package]] -name = "aes" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" -dependencies = [ - "cfg-if 1.0.0", - "cipher 0.3.0", - "cpufeatures", - "opaque-debug", -] - -[[package]] -name = "aes-gcm" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" -dependencies = [ - "aead 0.3.2", - "aes 0.6.0", - "cipher 0.2.5", - "ctr 0.6.0", - "ghash 0.3.1", - "subtle", -] - -[[package]] -name = "aes-gcm" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" -dependencies = [ - "aead 0.4.3", - "aes 0.7.5", - "cipher 0.3.0", - "ctr 0.8.0", - "ghash 0.4.4", - "subtle", -] - -[[package]] -name = "aes-soft" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" -dependencies = [ - "cipher 0.2.5", - "opaque-debug", -] - -[[package]] -name = "aesni" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" -dependencies = [ - "cipher 0.2.5", - "opaque-debug", -] - [[package]] name = "ahash" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.7", + "getrandom", "once_cell", "version_check", ] @@ -315,19 +56,10 @@ dependencies = [ ] [[package]] -name = "alloc-no-stdlib" -version = "2.0.4" +name = "ambient-authority" +version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] +checksum = "ec8ad6edb4840b78c5c3d88de606b22252d552b55f3a4699fbb10fc070ec3049" [[package]] name = "android_system_properties" @@ -362,6 +94,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "assert_cmd" version = "2.0.6" @@ -376,107 +114,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "async-channel" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" -dependencies = [ - "concurrent-queue", - "event-listener", - "futures-core", -] - -[[package]] -name = "async-compression" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345fd392ab01f746c717b1357165b76f0b67a60192007b234058c9045fdcf695" -dependencies = [ - "brotli", - "flate2", - "futures-core", - "memchr", - "pin-project-lite 0.2.9", - "tokio", -] - -[[package]] -name = "async-dup" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7427a12b8dc09291528cfb1da2447059adb4a257388c2acd6497a79d55cf6f7c" -dependencies = [ - "futures-io", - "simple-mutex", -] - -[[package]] -name = "async-executor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "once_cell", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5262ed948da60dd8956c6c5aca4d4163593dddb7b32d73267c93dab7b2e98940" -dependencies = [ - "async-channel", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite", - "num_cpus", - "once_cell", - "tokio", -] - -[[package]] -name = "async-h1" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8101020758a4fc3a7c326cb42aa99e9fa77cbfb76987c128ad956406fe1f70a7" -dependencies = [ - "async-channel", - "async-dup", - "async-std", - "futures-core", - "http-types", - "httparse", - "log", - "pin-project", -] - -[[package]] -name = "async-io" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" -dependencies = [ - "concurrent-queue", - "futures-lite", - "libc", - "log", - "once_cell", - "parking", - "polling", - "slab", - "socket2", - "waker-fn", - "winapi", -] - [[package]] name = "async-lock" version = "2.5.0" @@ -486,44 +123,6 @@ dependencies = [ "event-listener", ] -[[package]] -name = "async-process" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c" -dependencies = [ - "async-io", - "blocking", - "cfg-if 1.0.0", - "event-listener", - "futures-lite", - "libc", - "once_cell", - "signal-hook", - "winapi", -] - -[[package]] -name = "async-session" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345022a2eed092cd105cc1b26fd61c341e100bd5fcbbd792df4baf31c2cc631f" -dependencies = [ - "anyhow", - "async-std", - "async-trait", - "base64 0.12.3", - "bincode", - "blake3", - "chrono", - "hmac 0.8.1", - "kv-log-macro", - "rand 0.7.3", - "serde", - "serde_json", - "sha2 0.9.9", -] - [[package]] name = "async-session" version = "3.0.0" @@ -533,59 +132,18 @@ dependencies = [ "anyhow", "async-lock", "async-trait", - "base64 0.13.1", + "base64", "bincode", "blake3", "chrono", "hmac 0.11.0", "log", - "rand 0.8.5", + "rand", "serde", "serde_json", "sha2 0.9.9", ] -[[package]] -name = "async-sse" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53bba003996b8fd22245cd0c59b869ba764188ed435392cf2796d03b805ade10" -dependencies = [ - "async-channel", - "async-std", - "http-types", - "log", - "memchr", - "pin-project-lite 0.1.12", -] - -[[package]] -name = "async-std" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" -dependencies = [ - "async-channel", - "async-global-executor", - "async-io", - "async-lock", - "async-process", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite 0.2.9", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - [[package]] name = "async-stream" version = "0.3.3" @@ -602,42 +160,20 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] -[[package]] -name = "async-task" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" - [[package]] name = "async-trait" version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", -] - -[[package]] -name = "async-tungstenite" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b71b31561643aa8e7df3effe284fa83ab1a840e52294c5f4bd7bfd8b2becbb" -dependencies = [ - "futures-io", - "futures-util", - "log", - "pin-project-lite 0.2.9", - "tokio", - "tokio-rustls", - "tungstenite", - "webpki-roots", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -649,21 +185,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atomic" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c" -dependencies = [ - "autocfg 1.1.0", -] - -[[package]] -name = "atomic-waker" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" - [[package]] name = "atty" version = "0.2.14" @@ -675,15 +196,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "autocfg" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" -dependencies = [ - "autocfg 1.1.0", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -706,12 +218,12 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes 1.3.0", + "bytes", "hex 0.4.3", - "http 0.2.8", + "http", "hyper", "ring", - "time 0.3.11", + "time", "tokio", "tower", "tracing", @@ -727,7 +239,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-types", "aws-types", - "http 0.2.8", + "http", "regex", "tracing", ] @@ -741,12 +253,12 @@ dependencies = [ "aws-smithy-http", "aws-smithy-types", "aws-types", - "bytes 1.3.0", - "http 0.2.8", + "bytes", + "http", "http-body", "lazy_static", "percent-encoding", - "pin-project-lite 0.2.9", + "pin-project-lite", "tracing", ] @@ -767,8 +279,8 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes 1.3.0", - "http 0.2.8", + "bytes", + "http", "tokio-stream", "tower", ] @@ -789,8 +301,8 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes 1.3.0", - "http 0.2.8", + "bytes", + "http", "tokio-stream", "tower", ] @@ -812,8 +324,8 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes 1.3.0", - "http 0.2.8", + "bytes", + "http", "tower", ] @@ -826,7 +338,7 @@ dependencies = [ "aws-sigv4", "aws-smithy-http", "aws-types", - "http 0.2.8", + "http", "tracing", ] @@ -839,12 +351,12 @@ dependencies = [ "aws-smithy-http", "form_urlencoded", "hex 0.4.3", - "http 0.2.8", + "http", "once_cell", "percent-encoding", "regex", "ring", - "time 0.3.11", + "time", "tracing", ] @@ -855,7 +367,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b3442b4c5d3fc39891a2e5e625735fba6b24694887d49c6518460fde98247a9" dependencies = [ "futures-util", - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", "tokio-stream", ] @@ -870,14 +382,14 @@ dependencies = [ "aws-smithy-http", "aws-smithy-http-tower", "aws-smithy-types", - "bytes 1.3.0", + "bytes", "fastrand", - "http 0.2.8", + "http", "http-body", "hyper", "hyper-rustls", "lazy_static", - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", "tower", "tracing", @@ -890,18 +402,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf58ed4fefa61dbf038e5421a521cbc2c448ef69deff0ab1d915d8a10eda5664" dependencies = [ "aws-smithy-types", - "bytes 1.3.0", + "bytes", "bytes-utils", "futures-core", - "http 0.2.8", + "http", "http-body", "hyper", "once_cell", "percent-encoding", - "pin-project-lite 0.2.9", + "pin-project-lite", "pin-utils", "tokio", - "tokio-util 0.7.3", + "tokio-util", "tracing", ] @@ -912,10 +424,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20c96d7bd35e7cf96aca1134b2f81b1b59ffe493f7c6539c051791cbbf7a42d3" dependencies = [ "aws-smithy-http", - "bytes 1.3.0", - "http 0.2.8", + "bytes", + "http", "http-body", - "pin-project-lite 0.2.9", + "pin-project-lite", "tower", "tracing", ] @@ -945,10 +457,10 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b02e06ea63498c43bc0217ea4d16605d4e58d85c12fc23f6572ff6d0a840c61" dependencies = [ - "itoa 1.0.2", + "itoa", "num-integer", "ryu", - "time 0.3.11", + "time", ] [[package]] @@ -970,7 +482,7 @@ dependencies = [ "aws-smithy-client", "aws-smithy-http", "aws-smithy-types", - "http 0.2.8", + "http", "rustc_version 0.4.0", "tracing", "zeroize", @@ -984,20 +496,20 @@ checksum = "744864363a200a5e724a7e61bc8c11b6628cf2e3ec519c8a1a48e609a8156b40" dependencies = [ "async-trait", "axum-core", - "base64 0.13.1", + "base64", "bitflags", - "bytes 1.3.0", + "bytes", "futures-util", "headers", - "http 0.2.8", + "http", "http-body", "hyper", - "itoa 1.0.2", + "itoa", "matchit", "memchr", "mime", "percent-encoding", - "pin-project-lite 0.2.9", + "pin-project-lite", "rustversion", "serde", "serde_json", @@ -1020,9 +532,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79b8558f5a0581152dc94dcd289132a1d377494bdeafcd41869b3258e3e2ad92" dependencies = [ "async-trait", - "bytes 1.3.0", + "bytes", "futures-util", - "http 0.2.8", + "http", "http-body", "mime", "rustversion", @@ -1037,12 +549,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a320103719de37b7b4da4c8eb629d4573f6bcfd3dfe80d3208806895ccf81d" dependencies = [ "axum", - "bytes 1.3.0", + "bytes", "cookie 0.16.0", "futures-util", - "http 0.2.8", + "http", "mime", - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", "tower", "tower-http 0.3.5", @@ -1057,12 +569,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51227033e4d3acad15c879092ac8a228532707b5db5ff2628f638334f63e1b7a" dependencies = [ "axum", - "bytes 1.3.0", + "bytes", "cookie 0.17.0", "futures-util", - "http 0.2.8", + "http", "mime", - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", "tower", "tower-http 0.3.5", @@ -1077,14 +589,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8456dab8f11484979a86651da8e619b355ede5d61a160755155f6c344bd18c47" dependencies = [ "arc-swap", - "bytes 1.3.0", + "bytes", "futures-util", - "http 0.2.8", + "http", "http-body", "hyper", - "pin-project-lite 0.2.9", + "pin-project-lite", "rustls", - "rustls-pemfile 1.0.1", + "rustls-pemfile", "tokio", "tokio-rustls", "tower-service", @@ -1096,7 +608,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b114309d293dd8a6fedebf09d5b8bbb0f7647b3d204ca0dd333b5f797aed5c8" dependencies = [ - "async-session 3.0.0", + "async-session", "axum", "axum-extra 0.4.2", "futures", @@ -1106,30 +618,12 @@ dependencies = [ "tracing", ] -[[package]] -name = "base-x" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" - -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" -[[package]] -name = "binascii" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" - [[package]] name = "bincode" version = "1.3.3" @@ -1161,7 +655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.5.2", "cc", "cfg-if 0.1.10", "constant_time_eq", @@ -1187,44 +681,30 @@ dependencies = [ "generic-array", ] -[[package]] -name = "blocking" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" -dependencies = [ - "async-channel", - "async-task", - "atomic-waker", - "fastrand", - "futures-lite", - "once_cell", -] - [[package]] name = "bollard" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d82e7850583ead5f8bbef247e2a3c37a19bd576e8420cd262a6711921827e1e5" dependencies = [ - "base64 0.13.1", + "base64", "bollard-stubs", - "bytes 1.3.0", + "bytes", "futures-core", "futures-util", "hex 0.4.3", - "http 0.2.8", + "http", "hyper", "hyperlocal", "log", - "pin-project-lite 0.2.9", + "pin-project-lite", "serde", "serde_derive", "serde_json", "serde_urlencoded", "thiserror", "tokio", - "tokio-util 0.7.3", + "tokio-util", "url", "winapi", ] @@ -1239,27 +719,6 @@ dependencies = [ "serde_with", ] -[[package]] -name = "brotli" -version = "3.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bson" version = "2.5.0" @@ -1267,16 +726,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8746d07211bb12a7c34d995539b4a2acd4e0b0e757de98ce2ab99bcf17443fad" dependencies = [ "ahash", - "base64 0.13.1", + "base64", "hex 0.4.3", "indexmap", "lazy_static", - "rand 0.8.5", + "rand", "serde", "serde_bytes", "serde_json", - "time 0.3.11", - "uuid 1.2.2", + "time", + "uuid", ] [[package]] @@ -1302,16 +761,6 @@ dependencies = [ "serde", ] -[[package]] -name = "buf_redux" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" -dependencies = [ - "memchr", - "safemem", -] - [[package]] name = "bumpalo" version = "3.10.0" @@ -1324,22 +773,6 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "iovec", -] - -[[package]] -name = "bytes" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" - [[package]] name = "bytes" version = "1.3.0" @@ -1352,7 +785,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1934a3ef9cac8efde4966a92781e77713e1ba329f1d42e446c7d7eba340d8ef1" dependencies = [ - "bytes 1.3.0", + "bytes", "either", ] @@ -1363,27 +796,76 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" [[package]] -name = "bytestring" -version = "1.2.0" +name = "camino" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7f83e57d9154148e355404702e2694463241880b939570d7c97c014da7a69a1" +checksum = "869119e97797867fd90f5e22af7d0bd274bd4635ebb9eb68c04f3f513ae6c412" dependencies = [ - "bytes 1.3.0", + "serde", ] [[package]] -name = "cache-padded" -version = "1.2.0" +name = "cap-fs-ext" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" +checksum = "0f8079425cfd20227020f2bff1320710ca68d6eddb4f64aba8e2741b2b4d8133" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.42.0", +] [[package]] -name = "camino" -version = "1.0.9" +name = "cap-primitives" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869119e97797867fd90f5e22af7d0bd274bd4635ebb9eb68c04f3f513ae6c412" +checksum = "84bf8faa0b6397a4e26082818be03641a40e3aba1afc4ec44cbd6228c73c3a61" dependencies = [ - "serde", + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix", + "windows-sys 0.42.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53df044ddcb88611e19b712211b342ab106105cf658406f5ed4ee09ab10ed727" +dependencies = [ + "ambient-authority", + "rand", +] + +[[package]] +name = "cap-std" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ad2b9e262a5c3b67ee92e4b9607ace704384c50c32aa6017a9282ddf15df20" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "ipnet", + "rustix", +] + +[[package]] +name = "cap-time-ext" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dcbdbcced5c88b20f27c637faaed5dd283898cbefea48d2d8f3dcfaf048e5cc" +dependencies = [ + "cap-primitives", + "once_cell", + "rustix", + "winx", ] [[package]] @@ -1430,7 +912,7 @@ dependencies = [ "percent-encoding", "rustc-workspace-hack", "rustfix", - "semver 1.0.14", + "semver 1.0.16", "serde", "serde_ignored", "serde_json", @@ -1441,7 +923,7 @@ dependencies = [ "termcolor", "toml_edit 0.14.4", "unicode-width", - "unicode-xid 0.2.3", + "unicode-xid", "url", "walkdir", "winapi", @@ -1467,7 +949,7 @@ dependencies = [ "native-tls", "pathdiff", "regex", - "semver 1.0.14", + "semver 1.0.16", "serde", "serde_derive", "serde_json", @@ -1489,7 +971,7 @@ dependencies = [ [[package]] name = "cargo-shuttle" -version = "0.11.2" +version = "0.12.0" dependencies = [ "anyhow", "assert_cmd", @@ -1505,10 +987,12 @@ dependencies = [ "crossterm", "dialoguer", "dirs", + "dunce", "flate2", "futures", "git2", "headers", + "home", "ignore", "indicatif", "indoc", @@ -1522,6 +1006,7 @@ dependencies = [ "serde", "serde_json", "shuttle-common", + "shuttle-proto", "shuttle-secrets", "shuttle-service", "sqlx", @@ -1534,10 +1019,11 @@ dependencies = [ "tokiotest-httpserver", "toml", "toml_edit 0.15.0", + "tonic", "tracing", "tracing-subscriber", "url", - "uuid 1.2.2", + "uuid", "webbrowser", ] @@ -1571,7 +1057,7 @@ checksum = "982a0cf6a99c350d7246035613882e376d58cebe571785abc5da4f648d53ac0a" dependencies = [ "camino", "cargo-platform", - "semver 1.0.14", + "semver 1.0.16", "serde", "serde_json", "thiserror", @@ -1611,12 +1097,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", - "js-sys", "num-integer", "num-traits", "serde", - "time 0.1.44", - "wasm-bindgen", "winapi", ] @@ -1626,24 +1109,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" -[[package]] -name = "cipher" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" -dependencies = [ - "generic-array", -] - -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", -] - [[package]] name = "clap" version = "3.2.23" @@ -1694,9 +1159,9 @@ checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck", "proc-macro-error", - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1707,9 +1172,9 @@ checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" dependencies = [ "heck", "proc-macro-error", - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1730,15 +1195,6 @@ dependencies = [ "os_str_bytes", ] -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -dependencies = [ - "bitflags", -] - [[package]] name = "colored" version = "2.0.0" @@ -1756,7 +1212,7 @@ version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" dependencies = [ - "bytes 1.3.0", + "bytes", "memchr", ] @@ -1813,15 +1269,6 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad159cc964ac8f9d407cbc0aa44b02436c054b541f2b4b5f06972e1efdc54bc7" -[[package]] -name = "concurrent-queue" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" -dependencies = [ - "cache-padded", -] - [[package]] name = "console" version = "0.15.2" @@ -1836,12 +1283,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "const_fn" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" - [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1852,84 +1293,181 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" +dependencies = [ + "base64", + "hmac 0.12.1", + "percent-encoding", + "rand", + "sha2 0.10.2", + "subtle", + "time", + "version_check", +] + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpp_demangle" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaa953eaad386a53111e47172c2fedba671e5684c8dd601a5f474f4f118710f" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-bforest" +version = "0.91.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc952b310b24444fc14ab8b9cbe3fafd7e7329e3eec84c3a9b11d2b5cf6f3be1" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-codegen" +version = "0.91.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e73470419b33011e50dbf0f6439cbccbaabe9381de172da4e1b6efcda4bb8fa7" +dependencies = [ + "arrayvec 0.7.2", + "bumpalo", + "cranelift-bforest", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-egraph", + "cranelift-entity", + "cranelift-isle", + "gimli", + "log", + "regalloc2", + "smallvec", + "target-lexicon", +] [[package]] -name = "cookie" -version = "0.14.4" +name = "cranelift-codegen-meta" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" +checksum = "911a1872464108a11ac9965c2b079e61bbdf1bc2e0b9001264264add2e12a38f" dependencies = [ - "aes-gcm 0.8.0", - "base64 0.13.1", - "hkdf 0.10.0", - "hmac 0.10.1", - "percent-encoding", - "rand 0.8.5", - "sha2 0.9.9", - "time 0.2.27", - "version_check", + "cranelift-codegen-shared", ] [[package]] -name = "cookie" -version = "0.16.0" +name = "cranelift-codegen-shared" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" +checksum = "e036f3f07adb24a86fb46e977e8fe03b18bb16b1eada949cf2c48283e5f8a862" + +[[package]] +name = "cranelift-egraph" +version = "0.91.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6c623f4b5d2a6bad32c403f03765d4484a827eb93ee78f8cb6219ef118fd59" dependencies = [ - "aes-gcm 0.9.4", - "base64 0.13.1", - "hkdf 0.12.3", - "hmac 0.12.1", - "percent-encoding", - "rand 0.8.5", - "sha2 0.10.2", - "subtle", - "time 0.3.11", - "version_check", + "cranelift-entity", + "fxhash", + "hashbrown", + "indexmap", + "log", + "smallvec", ] [[package]] -name = "cookie" -version = "0.17.0" +name = "cranelift-entity" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +checksum = "74385eb5e405b3562f0caa7bcc4ab9a93c7958dd5bcd0e910bffb7765eacd6fc" dependencies = [ - "percent-encoding", - "time 0.3.11", - "version_check", + "serde", ] [[package]] -name = "core-foundation" -version = "0.9.3" +name = "cranelift-frontend" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "8a4ac920422ee36bff2c66257fec861765e3d95a125cdf58d8c0f3bba7e40e61" dependencies = [ - "core-foundation-sys", - "libc", + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", ] [[package]] -name = "core-foundation-sys" -version = "0.8.3" +name = "cranelift-isle" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "c541263fb37ad2baa53ec8c37218ee5d02fa0984670d9419dedd8002ea68ff08" [[package]] -name = "cpufeatures" -version = "0.2.2" +name = "cranelift-native" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "1de5d7a063e8563d670aaca38de16591a9b70dc66cbad4d49a7b4ae8395fd1ce" dependencies = [ + "cranelift-codegen", "libc", + "target-lexicon", ] [[package]] -name = "cpuid-bool" -version = "0.2.0" +name = "cranelift-wasm" +version = "0.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +checksum = "dfbc4dd03b713b5d71b582915b8c272f4813cdd8c99a3e03d9ba70c44468a6e0" +dependencies = [ + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "itertools", + "log", + "smallvec", + "wasmparser", + "wasmtime-types", +] [[package]] name = "crates-index" @@ -1944,7 +1482,7 @@ dependencies = [ "num_cpus", "rayon", "rustc-hash", - "semver 1.0.14", + "semver 1.0.16", "serde", "serde_derive", "serde_json", @@ -2017,7 +1555,7 @@ version = "0.9.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" dependencies = [ - "autocfg 1.1.0", + "autocfg", "cfg-if 1.0.0", "crossbeam-utils", "memoffset 0.7.1", @@ -2069,16 +1607,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "cruet" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d174765c7d11eb16f70a4213583aac2ca5ae1ebd1e233c6d5104bfb70fce3" -dependencies = [ - "once_cell", - "regex", -] - [[package]] name = "crypto-common" version = "0.1.3" @@ -2111,16 +1639,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "crypto-mac" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "crypto-mac" version = "0.11.1" @@ -2137,26 +1655,8 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" dependencies = [ - "quote 1.0.21", - "syn 1.0.104", -] - -[[package]] -name = "ctr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" -dependencies = [ - "cipher 0.2.5", -] - -[[package]] -name = "ctr" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" -dependencies = [ - "cipher 0.3.0", + "quote", + "syn", ] [[package]] @@ -2202,18 +1702,8 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core 0.13.4", - "darling_macro 0.13.4", -] - -[[package]] -name = "darling" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" -dependencies = [ - "darling_core 0.14.1", - "darling_macro 0.14.1", + "darling_core", + "darling_macro", ] [[package]] @@ -2224,24 +1714,10 @@ checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.47", - "quote 1.0.21", - "strsim", - "syn 1.0.104", -] - -[[package]] -name = "darling_core" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2 1.0.47", - "quote 1.0.21", + "proc-macro2", + "quote", "strsim", - "syn 1.0.104", + "syn", ] [[package]] @@ -2250,20 +1726,9 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core 0.13.4", - "quote 1.0.21", - "syn 1.0.104", -] - -[[package]] -name = "darling_macro" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" -dependencies = [ - "darling_core 0.14.1", - "quote 1.0.21", - "syn 1.0.104", + "darling_core", + "quote", + "syn", ] [[package]] @@ -2277,7 +1742,6 @@ dependencies = [ "lock_api", "once_cell", "parking_lot_core 0.9.3", - "serde", ] [[package]] @@ -2292,9 +1756,9 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2304,43 +1768,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case", - "proc-macro2 1.0.47", - "quote 1.0.21", + "proc-macro2", + "quote", "rustc_version 0.4.0", - "syn 1.0.104", -] - -[[package]] -name = "devise" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c7580b072f1c8476148f16e0a0d5dedddab787da98d86c5082c5e9ed8ab595" -dependencies = [ - "devise_codegen", - "devise_core", -] - -[[package]] -name = "devise_codegen" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "123c73e7a6e51b05c75fe1a1b2f4e241399ea5740ed810b0e3e6cacd9db5e7b2" -dependencies = [ - "devise_core", - "quote 1.0.21", -] - -[[package]] -name = "devise_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841ef46f4787d9097405cac4e70fb8644fc037b526e8c14054247c0263c400d0" -dependencies = [ - "bitflags", - "proc-macro2 1.0.47", - "proc-macro2-diagnostics", - "quote 1.0.21", - "syn 1.0.104", + "syn", ] [[package]] @@ -2387,6 +1818,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + [[package]] name = "dirs" version = "4.0.0" @@ -2428,12 +1869,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - [[package]] name = "doc-comment" version = "0.3.3" @@ -2483,29 +1918,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" dependencies = [ "heck", - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", -] - -[[package]] -name = "enumflags2" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" -dependencies = [ - "enumflags2_derive", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" -dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2531,15 +1946,6 @@ dependencies = [ "url", ] -[[package]] -name = "erased-serde" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81d013529d5574a60caeda29e179e695125448e5de52e3874f7b4c1d7360e18e" -dependencies = [ - "serde", -] - [[package]] name = "errno" version = "0.2.8" @@ -2567,6 +1973,12 @@ version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fastrand" version = "1.7.0" @@ -2577,33 +1989,13 @@ dependencies = [ ] [[package]] -name = "femme" -version = "2.2.1" +name = "file-per-thread-logger" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc04871e5ae3aa2952d552dae6b291b3099723bf779a8054281c1366a54613ef" +checksum = "21e16290574b39ee41c71aeb90ae960c504ebaf1e2a1c87bd52aa56ed6e1a02f" dependencies = [ - "cfg-if 1.0.0", - "js-sys", + "env_logger", "log", - "serde", - "serde_derive", - "serde_json", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "figment" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790b4292c72618abbab50f787a477014fe15634f96291de45672ce46afe122df" -dependencies = [ - "atomic", - "pear", - "serde", - "toml", - "uncased", - "version_check", ] [[package]] @@ -2684,10 +2076,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b5dd19b048b2dfde153588594b4f3da47b18afd18d171bb8d1d27741256bbaa" [[package]] -name = "fuchsia-cprng" -version = "0.1.1" +name = "fs-set-times" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +checksum = "e25ca26b0001154679ce0901527330e6153b670d17ccd1f86bab4e45dfba1a74" +dependencies = [ + "io-lifetimes", + "rustix", + "windows-sys 0.42.0", +] [[package]] name = "futures" @@ -2748,30 +2145,15 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" -[[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 0.2.9", - "waker-fn", -] - [[package]] name = "futures-macro" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2799,7 +2181,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.9", + "pin-project-lite", "pin-utils", "slab", ] @@ -2824,16 +2206,12 @@ dependencies = [ ] [[package]] -name = "generator" -version = "0.7.0" +name = "fxhash" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1d9279ca822891c1a4dae06d185612cf8fc6acfe5dff37781b41297811b12ee" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "winapi", + "byteorder", ] [[package]] @@ -2846,17 +2224,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.7" @@ -2865,27 +2232,18 @@ checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "ghash" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" -dependencies = [ - "opaque-debug", - "polyval 0.4.5", + "wasi", ] [[package]] -name = "ghash" -version = "0.4.4" +name = "gimli" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" dependencies = [ - "opaque-debug", - "polyval 0.5.3", + "fallible-iterator", + "indexmap", + "stable_deref_trait", ] [[package]] @@ -2934,34 +2292,22 @@ dependencies = [ "regex", ] -[[package]] -name = "gloo-timers" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "h2" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" dependencies = [ - "bytes 1.3.0", + "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http 0.2.8", + "http", "indexmap", "slab", "tokio", - "tokio-util 0.7.3", + "tokio-util", "tracing", ] @@ -2989,14 +2335,14 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ - "base64 0.13.1", + "base64", "bitflags", - "bytes 1.3.0", + "bytes", "headers-core", - "http 0.2.8", + "http", "httpdate", "mime", - "sha1 0.10.4", + "sha1", ] [[package]] @@ -3005,7 +2351,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" dependencies = [ - "http 0.2.8", + "http", ] [[package]] @@ -3050,16 +2396,6 @@ dependencies = [ "serde", ] -[[package]] -name = "hkdf" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" -dependencies = [ - "digest 0.9.0", - "hmac 0.10.1", -] - [[package]] name = "hkdf" version = "0.12.3" @@ -3069,26 +2405,6 @@ dependencies = [ "hmac 0.12.1", ] -[[package]] -name = "hmac" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" -dependencies = [ - "crypto-mac 0.8.0", - "digest 0.9.0", -] - -[[package]] -name = "hmac" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" -dependencies = [ - "crypto-mac 0.10.1", - "digest 0.9.0", -] - [[package]] name = "hmac" version = "0.11.0" @@ -3123,20 +2439,9 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" dependencies = [ - "libc", - "match_cfg", - "winapi", -] - -[[package]] -name = "http" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" -dependencies = [ - "bytes 0.4.12", - "fnv", - "itoa 0.4.8", + "libc", + "match_cfg", + "winapi", ] [[package]] @@ -3145,9 +2450,9 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ - "bytes 1.3.0", + "bytes", "fnv", - "itoa 1.0.2", + "itoa", ] [[package]] @@ -3156,21 +2461,9 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes 1.3.0", - "http 0.2.8", - "pin-project-lite 0.2.9", -] - -[[package]] -name = "http-client" -version = "6.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1947510dc91e2bf586ea5ffb412caad7673264e14bb39fb9078da114a94ce1a5" -dependencies = [ - "async-trait", - "cfg-if 1.0.0", - "http-types", - "log", + "bytes", + "http", + "pin-project-lite", ] [[package]] @@ -3180,25 +2473,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" [[package]] -name = "http-types" -version = "2.12.0" +name = "http-serde" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +checksum = "0e272971f774ba29341db2f686255ff8a979365a26fb9e4277f6b6d9ec0cdd5e" dependencies = [ - "anyhow", - "async-channel", - "async-std", - "base64 0.13.1", - "cookie 0.14.4", - "futures-lite", - "infer", - "pin-project-lite 0.2.9", - "rand 0.7.3", + "http", "serde", - "serde_json", - "serde_qs", - "serde_urlencoded", - "url", ] [[package]] @@ -3225,17 +2506,17 @@ version = "0.14.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" dependencies = [ - "bytes 1.3.0", + "bytes", "futures-channel", "futures-core", "futures-util", "h2", - "http 0.2.8", + "http", "http-body", "httparse", "httpdate", - "itoa 1.0.2", - "pin-project-lite 0.2.9", + "itoa", + "pin-project-lite", "socket2", "tokio", "tower-service", @@ -3271,7 +2552,7 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" dependencies = [ - "http 0.2.8", + "http", "hyper", "log", "rustls", @@ -3287,7 +2568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", "tokio-io-timeout", ] @@ -3298,7 +2579,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.3.0", + "bytes", "hyper", "native-tls", "tokio", @@ -3383,7 +2664,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" dependencies = [ "bitmaps", - "rand_core 0.6.3", + "rand_core", "rand_xoshiro", "sized-chunks", "typenum", @@ -3396,7 +2677,7 @@ version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ - "autocfg 1.1.0", + "autocfg", "hashbrown", "serde", ] @@ -3419,18 +2700,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3" -[[package]] -name = "infer" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" - -[[package]] -name = "inlinable_string" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" - [[package]] name = "instant" version = "0.1.12" @@ -3446,7 +2715,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c6a5dc426fcc25b99d91e4a283a8f5518339a0f63bf28588a6c5f31e089f8a" dependencies = [ - "base64 0.13.1", + "base64", "hyper", "hyper-rustls", "ring", @@ -3456,22 +2725,23 @@ dependencies = [ ] [[package]] -name = "io-lifetimes" -version = "1.0.3" +name = "io-extras" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +checksum = "b87bc110777311d7832025f38c4ab0f089f764644009edef3b5cbadfedee8c40" dependencies = [ - "libc", + "io-lifetimes", "windows-sys 0.42.0", ] [[package]] -name = "iovec" -version = "0.1.4" +name = "io-lifetimes" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" dependencies = [ "libc", + "windows-sys 0.42.0", ] [[package]] @@ -3515,15 +2785,29 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] -name = "itoa" -version = "1.0.2" +name = "ittapi" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "e8c4f6ff06169ce7048dac5150b1501c7e3716a929721aeb06b87e51a43e42f4" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e078cce01485f418bae3beb34dd604aaedf2065502853c7da17fbce8e64eda" +dependencies = [ + "cc", +] [[package]] name = "jni" @@ -3569,7 +2853,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09f4f04699947111ec1733e71778d763555737579e44b85844cae8e1940a1828" dependencies = [ - "base64 0.13.1", + "base64", "pem", "ring", "serde", @@ -3586,21 +2870,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "lazy_static" version = "1.4.0" @@ -3613,6 +2882,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + [[package]] name = "libc" version = "0.2.137" @@ -3633,16 +2908,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if 1.0.0", - "winapi", -] - [[package]] name = "libnghttp2-sys" version = "0.1.7+1.45.0" @@ -3702,31 +2967,13 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" -[[package]] -name = "local-channel" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" -dependencies = [ - "futures-core", - "futures-sink", - "futures-util", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" - [[package]] name = "lock_api" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ - "autocfg 1.1.0", + "autocfg", "scopeguard", ] @@ -3737,32 +2984,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if 1.0.0", - "serde", - "value-bag", ] [[package]] -name = "loom" -version = "0.5.6" +name = "lru-cache" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" dependencies = [ - "cfg-if 1.0.0", - "generator", - "scoped-tls", - "serde", - "serde_json", - "tracing", - "tracing-subscriber", + "linked-hash-map", ] [[package]] -name = "lru-cache" -version = "0.1.2" +name = "mach" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" dependencies = [ - "linked-hash-map", + "libc", ] [[package]] @@ -3801,6 +3040,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfc802da7b1cf80aefffa0c7b2f77247c8b32206cc83c270b61264f5b360a80" +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "md-5" version = "0.10.1" @@ -3816,13 +3061,22 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memfd" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b20a59d985586e4a5aef64564ac77299f8586d8be6cf9106a5a40207e8908efb" +dependencies = [ + "rustix", +] + [[package]] name = "memoffset" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ - "autocfg 1.1.0", + "autocfg", ] [[package]] @@ -3831,7 +3085,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ - "autocfg 1.1.0", + "autocfg", ] [[package]] @@ -3873,7 +3127,7 @@ checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.36.1", ] @@ -3893,7 +3147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a37fe10c1485a0cd603468e284a1a8535b4ecf46808f5f7de3639a1e1252dbf8" dependencies = [ "async-trait", - "base64 0.13.1", + "base64", "bitflags", "bson", "chrono", @@ -3909,10 +3163,10 @@ dependencies = [ "md-5", "pbkdf2", "percent-encoding", - "rand 0.8.5", + "rand", "rustc_version_runtime", "rustls", - "rustls-pemfile 1.0.1", + "rustls-pemfile", "serde", "serde_bytes", "serde_with", @@ -3925,60 +3179,19 @@ dependencies = [ "thiserror", "tokio", "tokio-rustls", - "tokio-util 0.7.3", + "tokio-util", "trust-dns-proto", "trust-dns-resolver", "typed-builder", - "uuid 1.2.2", + "uuid", "webpki-roots", ] -[[package]] -name = "multer" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f8f35e687561d5c1667590911e6698a8cb714a134a7505718a182e7bc9d3836" -dependencies = [ - "bytes 1.3.0", - "encoding_rs", - "futures-util", - "http 0.2.8", - "httparse", - "log", - "memchr", - "mime", - "spin 0.9.3", - "tokio", - "tokio-util 0.6.10", - "version_check", -] - [[package]] name = "multimap" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" -dependencies = [ - "serde", -] - -[[package]] -name = "multipart" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" -dependencies = [ - "buf_redux", - "httparse", - "log", - "mime", - "mime_guess", - "quick-error", - "rand 0.8.5", - "safemem", - "tempfile", - "twoway", -] [[package]] name = "native-tls" @@ -4004,24 +3217,13 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "net2" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" -dependencies = [ - "cfg-if 0.1.10", - "libc", - "winapi", -] - [[package]] name = "nix" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" dependencies = [ - "autocfg 1.1.0", + "autocfg", "bitflags", "cfg-if 1.0.0", "libc", @@ -4055,7 +3257,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-integer", "num-traits", ] @@ -4066,7 +3268,7 @@ version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ - "autocfg 1.1.0", + "autocfg", "num-traits", ] @@ -4076,7 +3278,7 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ - "autocfg 1.1.0", + "autocfg", ] [[package]] @@ -4113,6 +3315,18 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "crc32fast", + "hashbrown", + "indexmap", + "memchr", +] + [[package]] name = "once_cell" version = "1.16.0" @@ -4156,9 +3370,9 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -4182,7 +3396,7 @@ version = "0.9.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" dependencies = [ - "autocfg 1.1.0", + "autocfg", "cc", "libc", "openssl-src", @@ -4207,8 +3421,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc79add46364183ece1a4542592ca593e6421c60807232f5b8f7a31703825d" dependencies = [ "async-trait", - "bytes 1.3.0", - "http 0.2.8", + "bytes", + "http", "opentelemetry_api", ] @@ -4221,7 +3435,7 @@ dependencies = [ "async-trait", "futures", "futures-util", - "http 0.2.8", + "http", "opentelemetry", "opentelemetry-proto", "prost", @@ -4256,7 +3470,7 @@ dependencies = [ "indexmap", "js-sys", "once_cell", - "pin-project-lite 0.2.9", + "pin-project-lite", "thiserror", ] @@ -4276,21 +3490,12 @@ dependencies = [ "once_cell", "opentelemetry_api", "percent-encoding", - "rand 0.8.5", + "rand", "thiserror", "tokio", "tokio-stream", ] -[[package]] -name = "ordered-float" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" -dependencies = [ - "num-traits", -] - [[package]] name = "os_info" version = "3.5.1" @@ -4323,12 +3528,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[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" @@ -4398,36 +3597,13 @@ dependencies = [ "digest 0.10.3", ] -[[package]] -name = "pear" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e44241c5e4c868e3eaa78b7c1848cadd6344ed4f54d029832d32b415a58702" -dependencies = [ - "inlinable_string", - "pear_codegen", - "yansi", -] - -[[package]] -name = "pear_codegen" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a5ca643c2303ecb740d506539deba189e16f2754040a42901cd8105d0282d0" -dependencies = [ - "proc-macro2 1.0.47", - "proc-macro2-diagnostics", - "quote 1.0.21", - "syn 1.0.104", -] - [[package]] name = "pem" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" dependencies = [ - "base64 0.13.1", + "base64", ] [[package]] @@ -4461,152 +3637,37 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", -] - -[[package]] -name = "pin-project-lite" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pipe" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7b8f27da217eb966df4c58d4159ea939431950ca03cf782c22bd7c5c1d8d75" -dependencies = [ - "crossbeam-channel", -] - -[[package]] -name = "pkg-config" -version = "0.3.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" - -[[package]] -name = "poem" -version = "1.3.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc88a96f338947991534ac756e28bd05665a7dd40ad9c0c143cc5503ef5635e8" -dependencies = [ - "async-trait", - "bytes 1.3.0", - "futures-util", - "headers", - "http 0.2.8", - "hyper", - "mime", - "parking_lot 0.12.1", - "percent-encoding", - "pin-project-lite 0.2.9", - "poem-derive", - "regex", - "rfc7239", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "thiserror", - "tokio", - "tokio-stream", - "tokio-util 0.7.3", - "tracing", -] - -[[package]] -name = "poem-derive" -version = "1.3.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bfb3ddf3eb162c2a2dc4dbdc610eaf56417cd4000fcda2686ccb354e2a1b2b" -dependencies = [ - "proc-macro-crate", - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "poise" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ca787e4e516076de1995a83ee05fbdfed71d072ea0da3df018318db42a87803" -dependencies = [ - "async-trait", - "derivative", - "futures-core", - "futures-util", - "log", - "once_cell", - "parking_lot 0.12.1", - "poise_macros", - "regex", - "serenity", - "tokio", -] - -[[package]] -name = "poise_macros" -version = "0.5.2" +name = "pin-project-lite" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b80c1f4e04114527f9d41ed6bb31707a095276f51bb0aef3ca11f062b25a67c4" -dependencies = [ - "darling 0.14.1", - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", -] +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] -name = "polling" -version = "2.2.0" +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "log", - "wepoll-ffi", - "winapi", -] +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "polyval" -version = "0.4.5" +name = "pipe" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +checksum = "1c7b8f27da217eb966df4c58d4159ea939431950ca03cf782c22bd7c5c1d8d75" dependencies = [ - "cpuid-bool", - "opaque-debug", - "universal-hash", + "crossbeam-channel", ] [[package]] -name = "polyval" -version = "0.5.3" +name = "pkg-config" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" -dependencies = [ - "cfg-if 1.0.0", - "cpufeatures", - "opaque-debug", - "universal-hash", -] +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "portable-atomic" @@ -4620,7 +3681,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" dependencies = [ - "rand 0.8.5", + "rand", ] [[package]] @@ -4674,18 +3735,8 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1516508b396cefe095485fdce673007422f5e48e82934b7b423dc26aa5e6a4" dependencies = [ - "proc-macro2 1.0.47", - "syn 1.0.104", -] - -[[package]] -name = "proc-macro-crate" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" -dependencies = [ - "thiserror", - "toml", + "proc-macro2", + "syn", ] [[package]] @@ -4695,9 +3746,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", "version_check", ] @@ -4707,26 +3758,11 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", + "proc-macro2", + "quote", "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro2" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -dependencies = [ - "unicode-xid 0.1.0", -] - [[package]] name = "proc-macro2" version = "1.0.47" @@ -4736,26 +3772,13 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada" -dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", - "version_check", - "yansi", -] - [[package]] name = "prost" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0841812012b2d4a6145fae9a6af1534873c32aa67fff26bd09f8fa42c83f95a" dependencies = [ - "bytes 1.3.0", + "bytes", "prost-derive", ] @@ -4765,7 +3788,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d8b442418ea0822409d9e7d047cbf1e7e9e1760b172bf9982cf29d517c93511" dependencies = [ - "bytes 1.3.0", + "bytes", "heck", "itertools", "lazy_static", @@ -4776,7 +3799,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 1.0.104", + "syn", "tempfile", "which", ] @@ -4789,9 +3812,9 @@ checksum = "7345d5f0e08c0536d7ac7229952590239e77abf0a0100a1b1d890add6ea96364" dependencies = [ "anyhow", "itertools", - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -4800,10 +3823,19 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dfaa718ad76a44b3415e6c4d53b17c8f99160dcb3a99b10470fce8ad43f6e3e" dependencies = [ - "bytes 1.3.0", + "bytes", "prost", ] +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + [[package]] name = "queues" version = "1.1.0" @@ -4816,54 +3848,13 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quote" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" -dependencies = [ - "proc-macro2 0.4.30", -] - [[package]] name = "quote" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ - "proc-macro2 1.0.47", -] - -[[package]] -name = "rand" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" -dependencies = [ - "autocfg 0.1.8", - "libc", - "rand_chacha 0.1.1", - "rand_core 0.4.2", - "rand_hc 0.1.0", - "rand_isaac", - "rand_jitter", - "rand_os", - "rand_pcg", - "rand_xorshift", - "winapi", -] - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc 0.2.0", + "proc-macro2", ] [[package]] @@ -4873,28 +3864,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.3", -] - -[[package]] -name = "rand_chacha" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -dependencies = [ - "autocfg 0.1.8", - "rand_core 0.3.1", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "rand_chacha", + "rand_core", ] [[package]] @@ -4904,31 +3875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", -] - -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", + "rand_core", ] [[package]] @@ -4937,78 +3884,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.7", -] - -[[package]] -name = "rand_hc" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_isaac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "rand_jitter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" -dependencies = [ - "libc", - "rand_core 0.4.2", - "winapi", -] - -[[package]] -name = "rand_os" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -dependencies = [ - "cloudabi", - "fuchsia-cprng", - "libc", - "rand_core 0.4.2", - "rdrand", - "winapi", -] - -[[package]] -name = "rand_pcg" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" -dependencies = [ - "autocfg 0.1.8", - "rand_core 0.4.2", -] - -[[package]] -name = "rand_xorshift" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" -dependencies = [ - "rand_core 0.3.1", + "getrandom", ] [[package]] @@ -5017,7 +3893,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" dependencies = [ - "rand_core 0.6.3", + "rand_core", ] [[package]] @@ -5060,19 +3936,10 @@ checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" dependencies = [ "pem", "ring", - "time 0.3.11", + "time", "yasna", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.2.13" @@ -5088,29 +3955,21 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.7", + "getrandom", "redox_syscall", "thiserror", ] [[package]] -name = "ref-cast" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685d58625b6c2b83e4cc88a27c4bf65adb7b6b16dbdc413e515c9405b47432ab" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.7" +name = "regalloc2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a043824e29c94169374ac5183ac0ed43f5724dc4556b19568007486bd840fa1f" +checksum = "300d4fbfb40c1c66a78ba3ddd41c1110247cf52f97b87d0f2fc9209bd49b030c" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "fxhash", + "log", + "slice-group-by", + "smallvec", ] [[package]] @@ -5154,13 +4013,13 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" dependencies = [ - "base64 0.13.1", - "bytes 1.3.0", + "base64", + "bytes", "encoding_rs", "futures-core", "futures-util", "h2", - "http 0.2.8", + "http", "http-body", "hyper", "hyper-rustls", @@ -5173,16 +4032,15 @@ dependencies = [ "native-tls", "once_cell", "percent-encoding", - "pin-project-lite 0.2.9", + "pin-project-lite", "rustls", - "rustls-pemfile 1.0.1", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", "tokio-rustls", - "tokio-util 0.7.3", "tower-service", "url", "wasm-bindgen", @@ -5200,7 +4058,7 @@ checksum = "4a1c03e9011a8c59716ad13115550469e081e2e9892656b0ba6a47c907921894" dependencies = [ "anyhow", "async-trait", - "http 0.2.8", + "http", "reqwest", "serde", "task-local-extensions", @@ -5217,7 +4075,7 @@ dependencies = [ "async-trait", "chrono", "futures", - "http 0.2.8", + "http", "hyper", "reqwest", "reqwest-middleware", @@ -5245,7 +4103,7 @@ checksum = "47f9e19b18c6cdd796cc70aea8a9ea5ee7b813be611c6589e3624fcdbfd05f9d" dependencies = [ "anyhow", "chrono", - "rand 0.8.5", + "rand", ] [[package]] @@ -5261,15 +4119,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "rfc7239" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "087317b3cf7eb481f13bd9025d729324b7cd068d6f470e2d76d049e191f5ba47" -dependencies = [ - "uncased", -] - [[package]] name = "ring" version = "0.16.20" @@ -5286,91 +4135,32 @@ dependencies = [ ] [[package]] -name = "rocket" -version = "0.5.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ead083fce4a405feb349cf09abdf64471c6077f14e0ce59364aa90d4b99317" -dependencies = [ - "async-stream", - "async-trait", - "atomic", - "atty", - "binascii", - "bytes 1.3.0", - "either", - "figment", - "futures", - "indexmap", - "log", - "memchr", - "multer", - "num_cpus", - "parking_lot 0.12.1", - "pin-project-lite 0.2.9", - "rand 0.8.5", - "ref-cast", - "rocket_codegen", - "rocket_http", - "serde", - "state", - "tempfile", - "time 0.3.11", - "tokio", - "tokio-stream", - "tokio-util 0.7.3", - "ubyte", - "version_check", - "yansi", -] - -[[package]] -name = "rocket_codegen" -version = "0.5.0-rc.2" +name = "rmp" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6aeb6bb9c61e9cd2c00d70ea267bf36f76a4cc615e5908b349c2f9d93999b47" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" dependencies = [ - "devise", - "glob", - "indexmap", - "proc-macro2 1.0.47", - "quote 1.0.21", - "rocket_http", - "syn 1.0.104", - "unicode-xid 0.2.3", + "byteorder", + "num-traits", + "paste", ] [[package]] -name = "rocket_http" -version = "0.5.0-rc.2" +name = "rmp-serde" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ded65d127954de3c12471630bf4b81a2792f065984461e65b91d0fdaafc17a2" +checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" dependencies = [ - "cookie 0.16.0", - "either", - "futures", - "http 0.2.8", - "hyper", - "indexmap", - "log", - "memchr", - "pear", - "percent-encoding", - "pin-project-lite 0.2.9", - "ref-cast", + "byteorder", + "rmp", "serde", - "smallvec", - "stable-pattern", - "state", - "time 0.3.11", - "tokio", - "uncased", ] [[package]] -name = "route-recognizer" -version = "0.2.0" +name = "rustc-demangle" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] name = "rustc-hash" @@ -5399,7 +4189,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.14", + "semver 1.0.16", ] [[package]] @@ -5433,8 +4223,10 @@ dependencies = [ "bitflags", "errno", "io-lifetimes", + "itoa", "libc", "linux-raw-sys", + "once_cell", "windows-sys 0.42.0", ] @@ -5457,27 +4249,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" dependencies = [ "openssl-probe", - "rustls-pemfile 1.0.1", + "rustls-pemfile", "schannel", "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" -dependencies = [ - "base64 0.13.1", -] - [[package]] name = "rustls-pemfile" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ - "base64 0.13.1", + "base64", ] [[package]] @@ -5493,91 +4276,20 @@ dependencies = [ "rustc-hash", "strsim", "unicode-normalization", - "unicode_categories", -] - -[[package]] -name = "rustversion" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" - -[[package]] -name = "ryu" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" - -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - -[[package]] -name = "salvo" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b290f01b3b881afd34408b5823cb44f6717ed6b93a6e16a0113e9a49645ea8a7" -dependencies = [ - "salvo_core", + "unicode_categories", ] [[package]] -name = "salvo_core" -version = "0.37.5" +name = "rustversion" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fea63014bacaaaef1eaa1f28d90921cfbbee73a379974fca30fc698f64a8853" -dependencies = [ - "async-compression", - "async-trait", - "base64 0.13.1", - "bytes 1.3.0", - "cookie 0.16.0", - "cruet", - "encoding_rs", - "enumflags2", - "fastrand", - "form_urlencoded", - "futures-util", - "headers", - "http 0.2.8", - "hyper", - "mime", - "mime_guess", - "multer", - "multimap", - "once_cell", - "parking_lot 0.12.1", - "percent-encoding", - "regex", - "salvo_macros", - "serde", - "serde_json", - "serde_urlencoded", - "tempfile", - "textnonce", - "thiserror", - "tokio", - "tokio-stream", - "tracing", - "url", -] +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" [[package]] -name = "salvo_macros" -version = "0.37.5" +name = "ryu" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b305a54f28b92483eabbfc91dd39bba62c840095b5513e83d31582c7e6bd8d44" -dependencies = [ - "cruet", - "darling 0.14.1", - "proc-macro-crate", - "proc-macro2 1.0.47", - "quote 1.0.21", - "regex", - "syn 1.0.104", -] +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] name = "same-file" @@ -5598,12 +4310,6 @@ dependencies = [ "windows-sys 0.36.1", ] -[[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" @@ -5654,9 +4360,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" dependencies = [ "serde", ] @@ -5676,16 +4382,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float", - "serde", -] - [[package]] name = "serde_bytes" version = "0.11.7" @@ -5701,18 +4397,9 @@ version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", -] - -[[package]] -name = "serde_fmt" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2963a69a2b3918c1dc75a45a18bd3fcd1120e31d3f59deb1b2f9b5d5ffb8baa4" -dependencies = [ - "serde", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -5731,7 +4418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "indexmap", - "itoa 1.0.2", + "itoa", "ryu", "serde", ] @@ -5745,17 +4432,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_qs" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" -dependencies = [ - "percent-encoding", - "serde", - "thiserror", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5763,7 +4439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.2", + "itoa", "ryu", "serde", ] @@ -5784,42 +4460,10 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ - "darling 0.13.4", - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", -] - -[[package]] -name = "serenity" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fd5e7b5858ad96e99d440138f34f5b98e1b959ebcd3a1036203b30e78eb788" -dependencies = [ - "async-trait", - "async-tungstenite", - "base64 0.13.1", - "bitflags", - "bytes 1.3.0", - "cfg-if 1.0.0", - "chrono", - "dashmap", - "flate2", - "futures", - "mime", - "mime_guess", - "parking_lot 0.12.1", - "percent-encoding", - "reqwest", - "rustversion", - "serde", - "serde-value", - "serde_json", - "time 0.3.11", - "tokio", - "tracing", - "typemap_rev", - "url", + "darling", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -5833,15 +4477,6 @@ dependencies = [ "digest 0.10.3", ] -[[package]] -name = "sha1" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" -dependencies = [ - "sha1_smol", -] - [[package]] name = "sha1" version = "0.10.4" @@ -5853,12 +4488,6 @@ dependencies = [ "digest 0.10.3", ] -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - [[package]] name = "sha2" version = "0.9.9" @@ -5898,9 +4527,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs", +] + [[package]] name = "shuttle-admin" -version = "0.11.0" +version = "0.12.0" dependencies = [ "anyhow", "clap 4.0.27", @@ -5917,7 +4555,7 @@ dependencies = [ [[package]] name = "shuttle-auth" -version = "0.11.0" +version = "0.12.0" dependencies = [ "anyhow", "async-trait", @@ -5925,11 +4563,11 @@ dependencies = [ "axum-extra 0.5.0", "axum-sessions", "clap 4.0.27", - "http 0.2.8", + "http", "hyper", "jsonwebtoken", "opentelemetry", - "rand 0.8.5", + "rand", "ring", "serde", "serde_json", @@ -5946,31 +4584,33 @@ dependencies = [ [[package]] name = "shuttle-codegen" -version = "0.11.0" +version = "0.12.0" dependencies = [ "pretty_assertions", "proc-macro-error", - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", "trybuild", ] [[package]] name = "shuttle-common" -version = "0.11.2" +version = "0.12.0" dependencies = [ "anyhow", "async-trait", "axum", - "base64 0.13.1", - "bytes 1.3.0", + "base64", + "bytes", + "cap-std", "chrono", "comfy-table", "crossterm", "headers", - "http 0.2.8", + "http", "http-body", + "http-serde", "hyper", "jsonwebtoken", "once_cell", @@ -5978,8 +4618,10 @@ dependencies = [ "opentelemetry-http", "opentelemetry-otlp", "pin-project", + "prost-types", "reqwest", "ring", + "rmp-serde", "rustrict", "serde", "serde_json", @@ -5994,17 +4636,17 @@ dependencies = [ "tracing-opentelemetry", "tracing-subscriber", "ttl_cache", - "uuid 1.2.2", + "uuid", ] [[package]] name = "shuttle-deployer" -version = "0.11.1" +version = "0.12.0" dependencies = [ "anyhow", "async-trait", "axum", - "bytes 1.3.0", + "bytes", "cargo", "cargo_metadata", "chrono", @@ -6015,6 +4657,7 @@ dependencies = [ "fqdn", "futures", "hex 0.4.3", + "home", "hyper", "hyper-reverse-proxy 0.5.2-dev (git+https://github.com/chesedo/hyper-reverse-proxy?branch=master)", "once_cell", @@ -6022,7 +4665,7 @@ dependencies = [ "opentelemetry-http", "pipe", "portpicker", - "rand 0.8.5", + "rand", "serde", "serde_json", "shuttle-common", @@ -6037,29 +4680,30 @@ dependencies = [ "toml", "tonic", "tower", + "tower-http 0.3.5", "tracing", "tracing-opentelemetry", "tracing-subscriber", - "uuid 1.2.2", + "uuid", ] [[package]] name = "shuttle-gateway" -version = "0.11.2" +version = "0.12.0" dependencies = [ "acme2", "anyhow", "async-trait", "axum", "axum-server", - "base64 0.13.1", + "base64", "bollard", "chrono", "clap 4.0.27", "colored", "fqdn", "futures", - "http 0.2.8", + "http", "hyper", "hyper-reverse-proxy 0.5.2-dev (git+https://github.com/chesedo/hyper-reverse-proxy?branch=bug/host_header)", "instant-acme", @@ -6072,11 +4716,11 @@ dependencies = [ "pem", "pin-project", "portpicker", - "rand 0.8.5", + "rand", "rcgen", "ring", "rustls", - "rustls-pemfile 1.0.1", + "rustls-pemfile", "serde", "serde_json", "shuttle-common", @@ -6086,26 +4730,34 @@ dependencies = [ "tempfile", "tokio", "tower", + "tower-http 0.3.5", "tracing", "tracing-opentelemetry", "tracing-subscriber", "ttl_cache", - "uuid 1.2.2", + "uuid", ] [[package]] name = "shuttle-proto" -version = "0.11.0" +version = "0.12.0" dependencies = [ + "anyhow", + "chrono", + "home", "prost", + "prost-types", "shuttle-common", + "tokio", "tonic", "tonic-build", + "tower", + "tracing", ] [[package]] name = "shuttle-provisioner" -version = "0.11.0" +version = "0.12.0" dependencies = [ "aws-config", "aws-sdk-rds", @@ -6116,7 +4768,7 @@ dependencies = [ "once_cell", "portpicker", "prost", - "rand 0.8.5", + "rand", "serde_json", "shuttle-common", "shuttle-proto", @@ -6130,54 +4782,62 @@ dependencies = [ ] [[package]] -name = "shuttle-secrets" -version = "0.11.0" +name = "shuttle-runtime" +version = "0.12.0" dependencies = [ + "anyhow", "async-trait", + "cap-std", + "chrono", + "clap 4.0.27", + "crossbeam-channel", + "futures", + "hyper", + "portpicker", + "prost-types", + "rmp-serde", + "serde_json", + "shuttle-common", + "shuttle-proto", "shuttle-service", + "strfmt", + "thiserror", "tokio", + "tokio-stream", + "tonic", + "tower", + "tracing", + "tracing-subscriber", + "wasi-common", + "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "shuttle-secrets" +version = "0.12.0" +dependencies = [ + "async-trait", + "shuttle-service", ] [[package]] name = "shuttle-service" -version = "0.11.0" +version = "0.12.0" dependencies = [ - "actix-web", "anyhow", - "async-std", "async-trait", - "axum", - "bincode", "cargo", "cargo_metadata", - "chrono", "crossbeam-channel", - "futures", - "hyper", - "libloading", - "num_cpus", "pipe", - "poem", - "poise", - "portpicker", - "rocket", - "salvo", "serde_json", - "serenity", "shuttle-codegen", "shuttle-common", - "sqlx", "strfmt", - "sync_wrapper", "thiserror", - "thruster", - "tide", "tokio", - "tower", "tracing", - "tracing-subscriber", - "uuid 1.2.2", - "warp", ] [[package]] @@ -6210,15 +4870,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simple-mutex" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38aabbeafa6f6dead8cebf246fe9fae1f9215c8d29b3a69f93bd62a9e4a3dcd6" -dependencies = [ - "event-listener", -] - [[package]] name = "simple_asn1" version = "0.6.2" @@ -6228,7 +4879,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time 0.3.11", + "time", ] [[package]] @@ -6247,6 +4898,12 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +[[package]] +name = "slice-group-by" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b634d87b960ab1a38c4fe143b508576f075e7c978bfad18217645ebfdfa2ec" + [[package]] name = "smallvec" version = "1.10.0" @@ -6259,7 +4916,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" dependencies = [ - "autocfg 1.1.0", + "autocfg", "serde", "static_assertions", "version_check", @@ -6340,10 +4997,10 @@ checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" dependencies = [ "ahash", "atoi", - "base64 0.13.1", + "base64", "bitflags", "byteorder", - "bytes 1.3.0", + "bytes", "chrono", "crc", "crossbeam-queue", @@ -6359,10 +5016,10 @@ dependencies = [ "futures-util", "hashlink", "hex 0.4.3", - "hkdf 0.12.3", + "hkdf", "hmac 0.12.1", "indexmap", - "itoa 1.0.2", + "itoa", "libc", "libsqlite3-sys", "log", @@ -6371,10 +5028,10 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rand 0.8.5", + "rand", "serde", "serde_json", - "sha1 0.10.4", + "sha1", "sha2 0.10.2", "smallvec", "sqlformat", @@ -6383,7 +5040,7 @@ dependencies = [ "thiserror", "tokio-stream", "url", - "uuid 1.2.2", + "uuid", "whoami", ] @@ -6397,13 +5054,13 @@ dependencies = [ "either", "heck", "once_cell", - "proc-macro2 1.0.47", - "quote 1.0.21", + "proc-macro2", + "quote", "serde_json", "sha2 0.10.2", "sqlx-core", "sqlx-rt", - "syn 1.0.104", + "syn", "url", ] @@ -6420,31 +5077,10 @@ dependencies = [ ] [[package]] -name = "stable-pattern" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045" -dependencies = [ - "memchr", -] - -[[package]] -name = "standback" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" -dependencies = [ - "version_check", -] - -[[package]] -name = "state" -version = "0.5.3" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" -dependencies = [ - "loom", -] +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "static_assertions" @@ -6452,55 +5088,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stdweb" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -dependencies = [ - "discard", - "rustc_version 0.2.3", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", -] - -[[package]] -name = "stdweb-derive" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "serde", - "serde_derive", - "syn 1.0.104", -] - -[[package]] -name = "stdweb-internal-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -dependencies = [ - "base-x", - "proc-macro2 1.0.47", - "quote 1.0.21", - "serde", - "serde_derive", - "serde_json", - "sha1 0.6.1", - "syn 1.0.104", -] - -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" - [[package]] name = "strfmt" version = "0.2.2" @@ -6548,10 +5135,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4faebde00e8ff94316c01800f9054fd2ba77d30d9e922541913051d1d978918b" dependencies = [ "heck", - "proc-macro2 1.0.47", - "quote 1.0.21", + "proc-macro2", + "quote", "rustversion", - "syn 1.0.104", + "syn", ] [[package]] @@ -6570,34 +5157,14 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" -[[package]] -name = "sval" -version = "1.0.0-alpha.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45f6ee7c7b87caf59549e9fe45d6a69c75c8019e79e212a835c5da0e92f0ba08" -dependencies = [ - "serde", -] - -[[package]] -name = "syn" -version = "0.15.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "unicode-xid 0.1.0", -] - [[package]] name = "syn" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", + "proc-macro2", + "quote", "unicode-ident", ] @@ -6607,6 +5174,21 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" +[[package]] +name = "system-interface" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77b5f685b54fe35201ca824534425d4af3562470fb67682cf20130c568b49042" +dependencies = [ + "bitflags", + "cap-fs-ext", + "cap-std", + "io-lifetimes", + "rustix", + "windows-sys 0.42.0", + "winx", +] + [[package]] name = "take_mut" version = "0.2.2" @@ -6624,6 +5206,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1" + [[package]] name = "task-local-extensions" version = "0.1.1" @@ -6647,15 +5235,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "templatify" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a528032d6917c9a80cf894d9feeffe34056e8d62d3492bbfc15abfdcfa8a8fe1" -dependencies = [ - "bytes 0.4.12", -] - [[package]] name = "termcolor" version = "1.1.3" @@ -6704,22 +5283,12 @@ dependencies = [ [[package]] name = "test-context-macros" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901a55b0a7a06ebc4a674dcca925170da8e613fa3b163a1df804ed10afb154d" -dependencies = [ - "quote 1.0.21", - "syn 1.0.104", -] - -[[package]] -name = "textnonce" -version = "1.0.0" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f8d70cd784ed1dc33106a18998d77758d281dc40dc3e6d050cf0f5286683" +checksum = "8901a55b0a7a06ebc4a674dcca925170da8e613fa3b163a1df804ed10afb154d" dependencies = [ - "base64 0.12.3", - "rand 0.7.3", + "quote", + "syn", ] [[package]] @@ -6743,9 +5312,9 @@ version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -6757,121 +5326,16 @@ dependencies = [ "once_cell", ] -[[package]] -name = "thruster" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "910effe6fa8063f44f9f2f4d15d758270a679562414235c6781bf3b606b72682" -dependencies = [ - "async-trait", - "bytes 0.5.6", - "bytes 1.3.0", - "fnv", - "futures", - "http 0.1.21", - "http 0.2.8", - "httparse", - "lazy_static", - "log", - "net2", - "num_cpus", - "paste", - "serde", - "serde_derive", - "serde_json", - "smallvec", - "socket2", - "templatify", - "thruster-proc", - "time 0.1.44", - "tokio", - "tokio-stream", - "tokio-util 0.6.10", -] - -[[package]] -name = "thruster-proc" -version = "1.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cfac33b0a1b0be1aae8e3ca87005671eb2e33617661c20052c98709410d364f" -dependencies = [ - "lazy_static", - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", - "uuid 0.7.4", -] - -[[package]] -name = "tide" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c459573f0dd2cc734b539047f57489ea875af8ee950860ded20cf93a79a1dee0" -dependencies = [ - "async-h1", - "async-session 2.0.1", - "async-sse", - "async-std", - "async-trait", - "femme", - "futures-util", - "http-client", - "http-types", - "kv-log-macro", - "log", - "pin-project-lite 0.2.9", - "route-recognizer", - "serde", - "serde_json", -] - -[[package]] -name = "time" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" -dependencies = [ - "const_fn", - "libc", - "standback", - "stdweb", - "time-macros 0.1.1", - "version_check", - "winapi", -] - [[package]] name = "time" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" dependencies = [ - "itoa 1.0.2", + "itoa", "libc", "num_threads", - "serde", - "time-macros 0.2.4", -] - -[[package]] -name = "time-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" -dependencies = [ - "proc-macro-hack", - "time-macros-impl", + "time-macros", ] [[package]] @@ -6880,19 +5344,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" -[[package]] -name = "time-macros-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" -dependencies = [ - "proc-macro-hack", - "proc-macro2 1.0.47", - "quote 1.0.21", - "standback", - "syn 1.0.104", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -6910,22 +5361,22 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.22.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" +checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" dependencies = [ - "autocfg 1.1.0", - "bytes 1.3.0", + "autocfg", + "bytes", "libc", "memchr", "mio", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite 0.2.9", + "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.45.0", ] [[package]] @@ -6934,7 +5385,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", ] @@ -6944,9 +5395,9 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -6972,12 +5423,12 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" dependencies = [ "futures-core", - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", ] @@ -6988,7 +5439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3" dependencies = [ "async-stream", - "bytes 1.3.0", + "bytes", "futures-core", "tokio", "tokio-stream", @@ -7008,33 +5459,17 @@ dependencies = [ "tungstenite", ] -[[package]] -name = "tokio-util" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" -dependencies = [ - "bytes 1.3.0", - "futures-core", - "futures-io", - "futures-sink", - "log", - "pin-project-lite 0.2.9", - "slab", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" dependencies = [ - "bytes 1.3.0", + "bytes", "futures-core", "futures-io", "futures-sink", - "pin-project-lite 0.2.9", + "pin-project-lite", "tokio", "tracing", ] @@ -7105,12 +5540,12 @@ dependencies = [ "async-stream", "async-trait", "axum", - "base64 0.13.1", - "bytes 1.3.0", + "base64", + "bytes", "futures-core", "futures-util", "h2", - "http 0.2.8", + "http", "http-body", "hyper", "hyper-timeout", @@ -7120,7 +5555,7 @@ dependencies = [ "prost-derive", "tokio", "tokio-stream", - "tokio-util 0.7.3", + "tokio-util", "tower", "tower-layer", "tower-service", @@ -7135,10 +5570,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31fa2c5e870bdce133847d15e075333e6e1ca3fff913001fede6754f3060e367" dependencies = [ "prettyplease", - "proc-macro2 1.0.47", + "proc-macro2", "prost-build", - "quote 1.0.21", - "syn 1.0.104", + "quote", + "syn", ] [[package]] @@ -7151,11 +5586,11 @@ dependencies = [ "futures-util", "indexmap", "pin-project", - "pin-project-lite 0.2.9", - "rand 0.8.5", + "pin-project-lite", + "rand", "slab", "tokio", - "tokio-util 0.7.3", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -7168,13 +5603,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aba3f3efabf7fb41fae8534fc20a817013dd1c12cb45441efb6c82e6556b4cd8" dependencies = [ "bitflags", - "bytes 1.3.0", + "bytes", "futures-core", "futures-util", - "http 0.2.8", + "http", "http-body", "http-range-header", - "pin-project-lite 0.2.9", + "pin-project-lite", "tower-layer", "tower-service", ] @@ -7185,14 +5620,15 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ + "base64", "bitflags", - "bytes 1.3.0", + "bytes", "futures-core", "futures-util", - "http 0.2.8", + "http", "http-body", "http-range-header", - "pin-project-lite 0.2.9", + "pin-project-lite", "tower", "tower-layer", "tower-service", @@ -7219,7 +5655,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.9", + "pin-project-lite", "tracing-attributes", "tracing-core", ] @@ -7230,9 +5666,9 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -7326,7 +5762,7 @@ dependencies = [ "ipnet", "lazy_static", "log", - "rand 0.8.5", + "rand", "smallvec", "thiserror", "tinyvec", @@ -7390,29 +5826,18 @@ version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" dependencies = [ - "base64 0.13.1", + "base64", "byteorder", - "bytes 1.3.0", - "http 0.2.8", + "bytes", + "http", "httparse", "log", "native-tls", - "rand 0.8.5", - "rustls", + "rand", "sha-1", "thiserror", "url", "utf-8", - "webpki", -] - -[[package]] -name = "twoway" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" -dependencies = [ - "memchr", ] [[package]] @@ -7421,42 +5846,17 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", + "proc-macro2", + "quote", + "syn", ] -[[package]] -name = "typemap_rev" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5b74f0a24b5454580a79abb6994393b09adf0ab8070f15827cb666255de155" - [[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" -[[package]] -name = "ubyte" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a58e29f263341a29bb79e14ad7fda5f63b1c7e48929bad4c685d7876b1d04e94" -dependencies = [ - "serde", -] - -[[package]] -name = "uncased" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622" -dependencies = [ - "serde", - "version_check", -] - [[package]] name = "unicase" version = "2.6.0" @@ -7499,12 +5899,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" - [[package]] name = "unicode-xid" version = "0.2.3" @@ -7517,16 +5911,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" -[[package]] -name = "universal-hash" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "untrusted" version = "0.7.1" @@ -7539,7 +5923,7 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f" dependencies = [ - "base64 0.13.1", + "base64", "chunked_transfer", "log", "native-tls", @@ -7562,14 +5946,13 @@ dependencies = [ "form_urlencoded", "idna 0.3.0", "percent-encoding", - "serde", ] [[package]] name = "urlencoding" -version = "2.1.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" [[package]] name = "utf-8" @@ -7583,22 +5966,13 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" -[[package]] -name = "uuid" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" -dependencies = [ - "rand 0.6.5", -] - [[package]] name = "uuid" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ - "getrandom 0.2.7", + "getrandom", "serde", ] @@ -7609,204 +5983,441 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] -name = "value-bag" -version = "1.0.0-alpha.9" +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +dependencies = [ + "arrayvec 0.5.2", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi-cap-std-sync" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79eba5cf83a4adb2ccba4c029858229a4992dd88cc35dbfa5a555ec7fc2a8416" +dependencies = [ + "anyhow", + "async-trait", + "cap-fs-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "io-extras", + "io-lifetimes", + "is-terminal", + "once_cell", + "rustix", + "system-interface", + "tracing", + "wasi-common", + "windows-sys 0.42.0", +] + +[[package]] +name = "wasi-common" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678ff55fb89ae721dae166003b843f53ee3e7bdb96aa96715fec8d44d012b105" +dependencies = [ + "anyhow", + "bitflags", + "cap-rand", + "cap-std", + "io-extras", + "rustix", + "thiserror", + "tracing", + "wasmtime", + "wiggle", + "windows-sys 0.42.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" dependencies = [ - "ctor", - "erased-serde", - "serde", - "serde_fmt", - "sval", - "version_check", + "quote", + "wasm-bindgen-macro-support", ] [[package]] -name = "vcpkg" -version = "0.2.15" +name = "wasm-bindgen-macro-support" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] [[package]] -name = "version_check" -version = "0.9.4" +name = "wasm-bindgen-shared" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" [[package]] -name = "vte" -version = "0.10.1" +name = "wasm-encoder" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +checksum = "05632e0a66a6ed8cca593c24223aabd6262f256c3693ad9822c315285f010614" dependencies = [ - "arrayvec", - "utf8parse", - "vte_generate_state_changes", + "leb128", ] [[package]] -name = "vte_generate_state_changes" -version = "0.1.1" +name = "wasmparser" +version = "0.95.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +checksum = "f2ea896273ea99b15132414be1da01ab0d8836415083298ecaffbe308eaac87a" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", + "indexmap", + "url", ] [[package]] -name = "wait-timeout" -version = "0.2.0" +name = "wasmtime" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "4abddf11816dd8f5e7310f6ebe5a2503b43f20ab2bf050b7d63f5b1bb96a81d9" dependencies = [ + "anyhow", + "async-trait", + "bincode", + "cfg-if 1.0.0", + "indexmap", "libc", + "log", + "object", + "once_cell", + "paste", + "psm", + "rayon", + "serde", + "target-lexicon", + "wasmparser", + "wasmtime-cache", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit", + "wasmtime-runtime", + "wat", + "windows-sys 0.42.0", ] [[package]] -name = "waker-fn" -version = "1.1.0" +name = "wasmtime-asm-macros" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "c1f5206486f0467ba86e84d35996c4048b077cec2c9e5b322e7b853bdbe79334" +dependencies = [ + "cfg-if 1.0.0", +] [[package]] -name = "walkdir" -version = "2.3.2" +name = "wasmtime-cache" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "d1e77abcf538af42517e188c109e4b50ecf6c0ee4d77ede76a438e0306b934dc" dependencies = [ - "same-file", - "winapi", - "winapi-util", + "anyhow", + "base64", + "bincode", + "directories-next", + "file-per-thread-logger", + "log", + "rustix", + "serde", + "sha2 0.10.2", + "toml", + "windows-sys 0.42.0", + "zstd", ] [[package]] -name = "want" -version = "0.3.0" +name = "wasmtime-cranelift" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "9e5bcb1d5ef211726b11e1286fe96cb40c69044c3632e1d6c67805d88a2e1a34" dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "cranelift-wasm", + "gimli", "log", - "try-lock", + "object", + "target-lexicon", + "thiserror", + "wasmparser", + "wasmtime-environ", ] [[package]] -name = "warp" -version = "0.3.3" +name = "wasmtime-environ" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7b8be92646fc3d18b06147664ebc5f48d222686cb11a8755e561a735aacc6d" +checksum = "dcab3fac5a2ff68ce9857166a7d7c0e5251b554839b9dda7ed3b5528e191936e" dependencies = [ - "bytes 1.3.0", - "futures-channel", - "futures-util", - "headers", - "http 0.2.8", - "hyper", + "anyhow", + "cranelift-entity", + "gimli", + "indexmap", "log", - "mime", - "mime_guess", - "multipart", - "percent-encoding", - "pin-project", - "rustls-pemfile 0.2.1", - "scoped-tls", + "object", "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-stream", - "tokio-tungstenite", - "tokio-util 0.7.3", - "tower-service", - "tracing", + "target-lexicon", + "thiserror", + "wasmparser", + "wasmtime-types", ] [[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +name = "wasmtime-fiber" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +checksum = "2fb38af221b780f2c03764d763fe7f7bc414ea9db744d66dac98f9b694892561" +dependencies = [ + "cc", + "cfg-if 1.0.0", + "rustix", + "wasmtime-asm-macros", + "windows-sys 0.42.0", +] [[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +name = "wasmtime-jit" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "a7d866e2a84ee164739b7ed7bd7cc9e1f918639d2ec5e2817a31e24c148cab20" +dependencies = [ + "addr2line", + "anyhow", + "bincode", + "cfg-if 1.0.0", + "cpp_demangle", + "gimli", + "ittapi", + "log", + "object", + "rustc-demangle", + "serde", + "target-lexicon", + "wasmtime-environ", + "wasmtime-jit-debug", + "wasmtime-jit-icache-coherence", + "wasmtime-runtime", + "windows-sys 0.42.0", +] [[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +name = "wasmtime-jit-debug" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "0104c2b1ce443f2a2806216fcdf6dce09303203ec5797a698d313063b31e5bc8" +dependencies = [ + "object", + "once_cell", + "rustix", +] [[package]] -name = "wasm-bindgen" -version = "0.2.81" +name = "wasmtime-jit-icache-coherence" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +checksum = "22d9c2e92b0fc124d2cad6cb497a4c840580a7dd2414a37109e8c7cfe699c0ea" dependencies = [ "cfg-if 1.0.0", - "serde", - "serde_json", - "wasm-bindgen-macro", + "libc", + "windows-sys 0.42.0", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.81" +name = "wasmtime-runtime" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +checksum = "0a1f0f99297a94cb20c511d1d4e864d9b54794644016d2530dc797cacfa7224a" dependencies = [ - "bumpalo", - "lazy_static", + "anyhow", + "cc", + "cfg-if 1.0.0", + "indexmap", + "libc", "log", - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", - "wasm-bindgen-shared", + "mach", + "memfd", + "memoffset 0.6.5", + "paste", + "rand", + "rustix", + "wasmtime-asm-macros", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit-debug", + "windows-sys 0.42.0", ] [[package]] -name = "wasm-bindgen-futures" -version = "0.4.31" +name = "wasmtime-types" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +checksum = "62f3d8ee409447cae51651fd812437a0047ed8d7f44e94171ee05ce7cb955c96" dependencies = [ - "cfg-if 1.0.0", - "js-sys", - "wasm-bindgen", - "web-sys", + "cranelift-entity", + "serde", + "thiserror", + "wasmparser", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.81" +name = "wasmtime-wasi" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +checksum = "9f32b06e3282ccbeab6fb96c64fa12a359f1253022dfd5cf99385b2344e70830" dependencies = [ - "quote 1.0.21", - "wasm-bindgen-macro-support", + "anyhow", + "wasi-cap-std-sync", + "wasi-common", + "wasmtime", + "wiggle", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.81" +name = "wast" +version = "35.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" dependencies = [ - "proc-macro2 1.0.47", - "quote 1.0.21", - "syn 1.0.104", - "wasm-bindgen-backend", - "wasm-bindgen-shared", + "leb128", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.81" +name = "wast" +version = "50.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" +checksum = "a2cbb59d4ac799842791fe7e806fa5dbbf6b5554d538e51cc8e176db6ff0ae34" +dependencies = [ + "leb128", + "memchr", + "unicode-width", + "wasm-encoder", +] + +[[package]] +name = "wat" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "584aaf7a1ecf4d383bbe1a25eeab0cbb8ff96acc6796707ff65cde48f4632f15" +dependencies = [ + "wast 50.0.0", +] [[package]] name = "web-sys" @@ -7853,24 +6464,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.2.5" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" dependencies = [ "either", - "lazy_static", "libc", + "once_cell", ] [[package]] @@ -7889,6 +6491,48 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" +[[package]] +name = "wiggle" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2433252352677648dc4ac0c99e7e254e1c58be8019cda3323ab3a3ce29da5b" +dependencies = [ + "anyhow", + "async-trait", + "bitflags", + "thiserror", + "tracing", + "wasmtime", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c15bf89e66bd1a9463ee529d37b999947befafd792f345d4a82e0d2b28c0845f" +dependencies = [ + "anyhow", + "heck", + "proc-macro2", + "quote", + "shellexpand", + "syn", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919fb8f106375c7f6daf7b388a1fea3e2092dedb273b17b2d917522917c07a3c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wiggle-generate", +] + [[package]] name = "winapi" version = "0.3.9" @@ -7940,19 +6584,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.0", + "windows_x86_64_msvc 0.42.1", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" @@ -7962,9 +6630,9 @@ checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" @@ -7974,9 +6642,9 @@ checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" @@ -7986,9 +6654,9 @@ checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" @@ -7998,15 +6666,15 @@ checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" @@ -8016,9 +6684,9 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "winreg" @@ -8038,6 +6706,29 @@ dependencies = [ "winapi", ] +[[package]] +name = "winx" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9baf690e238840de84bbfad6ad72d6628c41d34c1a5e276dab7fb2c9167ca1ac" +dependencies = [ + "bitflags", + "io-lifetimes", + "windows-sys 0.42.0", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror", + "wast 35.0.2", +] + [[package]] name = "xattr" version = "0.2.3" @@ -8065,7 +6756,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346d34a236c9d3e5f3b9b74563f238f955bbd05fa0b8b4efa53c130c43982f4c" dependencies = [ - "time 0.3.11", + "time", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5bd66069a..b6d8b90bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "gateway", "proto", "provisioner", + "runtime", "service" ] @@ -19,26 +20,27 @@ exclude = [ "resources/persist", "resources/secrets", "resources/shared-db", - "resources/static-folder" + "resources/static-folder", + "services" ] [workspace.package] -version = "0.11.0" +version = "0.12.0" edition = "2021" license = "Apache-2.0" repository = "https://github.com/shuttle-hq/shuttle" # https://doc.rust-lang.org/cargo/reference/workspaces.html#the-workspacedependencies-table [workspace.dependencies] -shuttle-codegen = { path = "codegen", version = "0.11.0" } -shuttle-common = { path = "common", version = "0.11.2" } -shuttle-proto = { path = "proto", version = "0.11.0" } -shuttle-service = { path = "service", version = "0.11.0" } +shuttle-codegen = { path = "codegen", version = "0.12.0" } +shuttle-common = { path = "common", version = "0.12.0" } +shuttle-proto = { path = "proto", version = "0.12.0" } +shuttle-service = { path = "service", version = "0.12.0" } anyhow = "1.0.66" async-trait = "0.1.58" -axum = "0.6.0" -chrono = { version = "0.4.23", default-features = false, features = ["clock"] } +axum = { version = "0.6.0", default-features = false } +chrono = { version = "0.4.23", default-features = false } clap = { version = "4.0.27", features = [ "derive" ] } headers = "0.3.8" http = "0.2.8" @@ -49,16 +51,18 @@ opentelemetry = { version = "0.18.0", features = ["rt-tokio"] } opentelemetry-http = "0.7.0" pin-project = "1.0.12" portpicker = "0.1.1" +prost-types = "0.11.0" rand = "0.8.5" ring = "0.16.20" -serde = "1.0.148" +serde = { version = "1.0.148", default-features = false } serde_json = "1.0.89" strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.37" +tonic = "0.8.3" tower = "0.4.13" tower-http = { version = "0.3.4", features = ["trace"] } -tracing = "0.1.37" +tracing = { version = "0.1.37", default-features = false } tracing-opentelemetry = "0.18.0" -tracing-subscriber = "0.3.16" +tracing-subscriber = { version = "0.3.16", default-features = false, features = ["registry", "std"] } ttl_cache = "0.5.1" uuid = "1.2.2" diff --git a/Containerfile b/Containerfile index 0765cba36..6dce75316 100644 --- a/Containerfile +++ b/Containerfile @@ -14,7 +14,7 @@ WORKDIR /build FROM shuttle-build as cache WORKDIR /src COPY . . -RUN find ${SRC_CRATES} \( -name "*.proto" -or -name "*.rs" -or -name "*.toml" -or -name "README.md" -or -name "*.sql" \) -type f -exec install -D \{\} /build/\{\} \; +RUN find ${SRC_CRATES} \( -name "*.proto" -or -name "*.rs" -or -name "*.toml" -or -name "Cargo.lock" -or -name "README.md" -or -name "*.sql" \) -type f -exec install -D \{\} /build/\{\} \; FROM shuttle-build AS planner COPY --from=cache /build . @@ -26,12 +26,18 @@ ARG CARGO_PROFILE RUN cargo chef cook $(if [ "$CARGO_PROFILE" = "release" ]; then echo --${CARGO_PROFILE}; fi) --recipe-path recipe.json COPY --from=cache /build . ARG folder +# if CARGO_PROFILE is release, pass --release, else use default debug profile RUN cargo build --bin shuttle-${folder} $(if [ "$CARGO_PROFILE" = "release" ]; then echo --${CARGO_PROFILE}; fi) ARG RUSTUP_TOOLCHAIN FROM rust:${RUSTUP_TOOLCHAIN}-buster as shuttle-common RUN apt-get update &&\ apt-get install -y curl +# download protoc binary and unzip it in usr/bin +RUN curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v21.9/protoc-21.9-linux-x86_64.zip &&\ + unzip -o protoc-21.9-linux-x86_64.zip -d /usr bin/protoc &&\ + unzip -o protoc-21.9-linux-x86_64.zip -d /usr/ 'include/*' &&\ + rm -f protoc-21.9-linux-x86_64.zip RUN rustup component add rust-src COPY --from=cache /build/ /usr/src/shuttle/ diff --git a/Makefile b/Makefile index 2bed9be28..c4752a48c 100644 --- a/Makefile +++ b/Makefile @@ -124,8 +124,8 @@ test: cd e2e; POSTGRES_PASSWORD=$(POSTGRES_PASSWORD) APPS_FQDN=$(APPS_FQDN) cargo test $(CARGO_TEST_FLAGS) -- --nocapture # Start the containers locally. This does not start panamax by default, -# to start panamax locally run this command with the COMPOSE_PROFILES=panamax -# environment variable. +# to start panamax locally run this command with an override for the profiles: +# `make COMPOSE_PROFILES=panamax up` up: docker-compose.rendered.yml CONTAINER_REGISTRY=$(CONTAINER_REGISTRY) $(DOCKER_COMPOSE) -f $< -p $(STACK) up -d $(DOCKER_COMPOSE_FLAGS) diff --git a/README.md b/README.md index 03b30c12b..537e6bd22 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@


- + docs @@ -72,7 +72,7 @@ And that's... it. ```bash $ cargo shuttle deploy - Finished dev [unoptimized + debuginfo] target(s) in 1m 01s + Finished release [optimized + debuginfo] target(s) in 1m 01s Project: hello-world Deployment Id: 3d08ac34-ad63-41c1-836b-99afdc90af9f diff --git a/admin/Cargo.toml b/admin/Cargo.toml index 993558126..76e1996d0 100644 --- a/admin/Cargo.toml +++ b/admin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-admin" -version = "0.11.0" +version = "0.12.0" edition = "2021" [dependencies] @@ -12,8 +12,8 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { version = "1.22.0", features = ["macros", "rt-multi-thread"] } toml = "0.5.9" -tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing = { workspace = true, features = ["default"] } +tracing-subscriber = { workspace = true, features = ["default", "env-filter"] } [dependencies.shuttle-common] workspace = true diff --git a/auth/src/api/handlers.rs b/auth/src/api/handlers.rs index 2d08221ce..b45061bf6 100644 --- a/auth/src/api/handlers.rs +++ b/auth/src/api/handlers.rs @@ -9,7 +9,7 @@ use axum::{ use axum_sessions::extractors::{ReadableSession, WritableSession}; use http::StatusCode; use serde::{Deserialize, Serialize}; -use shuttle_common::{backends::auth::Claim, models::user}; +use shuttle_common::{claims::Claim, models::user}; use tracing::instrument; use super::{ diff --git a/auth/src/user.rs b/auth/src/user.rs index 264adfb60..8d9b5a0c9 100644 --- a/auth/src/user.rs +++ b/auth/src/user.rs @@ -9,7 +9,7 @@ use axum::{ }; use rand::distributions::{Alphanumeric, DistString}; use serde::{Deserialize, Deserializer, Serialize}; -use shuttle_common::backends::auth::Scope; +use shuttle_common::claims::Scope; use sqlx::{query, Row, SqlitePool}; use tracing::{trace, Span}; diff --git a/auth/tests/api/session.rs b/auth/tests/api/session.rs index aa07e8bdd..b768a0096 100644 --- a/auth/tests/api/session.rs +++ b/auth/tests/api/session.rs @@ -2,7 +2,7 @@ use axum_extra::extract::cookie::{self, Cookie}; use http::{Request, StatusCode}; use hyper::Body; use serde_json::{json, Value}; -use shuttle_common::backends::auth::Claim; +use shuttle_common::claims::Claim; use crate::helpers::app; diff --git a/cargo-shuttle/Cargo.toml b/cargo-shuttle/Cargo.toml index 64d9d8f26..75224a0cc 100644 --- a/cargo-shuttle/Cargo.toml +++ b/cargo-shuttle/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-shuttle" -version = "0.11.2" +version = "0.12.0" edition.workspace = true license.workspace = true repository.workspace = true @@ -22,9 +22,11 @@ crossbeam-channel = "0.5.6" crossterm = "0.25.0" dialoguer = { version = "0.10.2", features = ["fuzzy-select"] } dirs = "4.0.0" +dunce = "1.0.3" flate2 = "1.0.25" futures = "0.3.25" git2 = "0.14.2" +home = "0.5.4" headers = { workspace = true } indicatif = "0.17.2" ignore = "0.4.18" @@ -47,8 +49,9 @@ tokio = { version = "1.22.0", features = ["macros"] } tokio-tungstenite = { version = "0.17.2", features = ["native-tls"] } toml = "0.5.9" toml_edit = "0.15.0" -tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter"] } +tonic = { workspace = true } +tracing = { workspace = true, features = ["default"] } +tracing-subscriber = { workspace = true, features = ["default", "env-filter", "fmt"] } url = "2.3.1" uuid = { workspace = true, features = ["v4"] } webbrowser = "0.8.2" @@ -57,13 +60,16 @@ webbrowser = "0.8.2" workspace = true features = ["models"] +[dependencies.shuttle-proto] +workspace = true + [dependencies.shuttle-secrets] -version = "0.11.0" +version = "0.12.0" path = "../resources/secrets" [dependencies.shuttle-service] workspace = true -features = ["loader"] +features = ["builder"] [features] vendored-openssl = ["openssl/vendored"] diff --git a/cargo-shuttle/README.md b/cargo-shuttle/README.md index ae1da5f10..aee7b13b8 100644 --- a/cargo-shuttle/README.md +++ b/cargo-shuttle/README.md @@ -105,7 +105,7 @@ cargo shuttle init --rocket my-rocket-app This should generate the following dependency in `Cargo.toml`: ```toml -shuttle-service = { version = "0.11.0", features = ["web-rocket"] } +shuttle-service = { version = "0.12.0", features = ["web-rocket"] } ``` The following boilerplate code should be generated into `src/lib.rs`: diff --git a/cargo-shuttle/src/args.rs b/cargo-shuttle/src/args.rs index fb1347198..9810a7a21 100644 --- a/cargo-shuttle/src/args.rs +++ b/cargo-shuttle/src/args.rs @@ -1,6 +1,6 @@ use std::{ ffi::OsString, - fs::{canonicalize, create_dir_all}, + fs::create_dir_all, io::{self, ErrorKind}, path::PathBuf, }; @@ -203,7 +203,7 @@ pub struct InitArgs { #[command(flatten)] pub login_args: LoginArgs, /// Path to initialize a new shuttle project - #[arg(default_value = ".", value_parser = OsStringValueParser::new().try_map(parse_path) )] + #[arg(default_value = ".", value_parser = OsStringValueParser::new().try_map(parse_init_path) )] pub path: PathBuf, } @@ -241,7 +241,7 @@ impl InitArgs { // Helper function to parse and return the absolute path fn parse_path(path: OsString) -> Result { - canonicalize(&path).map_err(|e| format!("could not turn {path:?} into a real path: {e}")) + dunce::canonicalize(&path).map_err(|e| format!("could not turn {path:?} into a real path: {e}")) } // Helper function to parse, create if not exists, and return the absolute path diff --git a/cargo-shuttle/src/client.rs b/cargo-shuttle/src/client.rs index 28ded40ee..048c441b4 100644 --- a/cargo-shuttle/src/client.rs +++ b/cargo-shuttle/src/client.rs @@ -200,7 +200,7 @@ impl Client { async fn ws_get(&self, path: String) -> Result>> { let ws_scheme = self.api_url.clone().replace("http", "ws"); - let url = format!("{}{}", ws_scheme, path); + let url = format!("{ws_scheme}{path}"); let mut request = url.into_client_request()?; if let Some(ref api_key) = self.api_key { diff --git a/cargo-shuttle/src/init.rs b/cargo-shuttle/src/init.rs index b36c1c52c..2926a08ac 100644 --- a/cargo-shuttle/src/init.rs +++ b/cargo-shuttle/src/init.rs @@ -6,7 +6,7 @@ use anyhow::Result; use cargo::ops::NewOptions; use cargo_edit::{find, get_latest_dependency, registry_url}; use indoc::indoc; -use toml_edit::{value, Array, Document, Item, Table}; +use toml_edit::{value, Array, Document, Table}; use url::Url; #[derive(Clone, Copy, Debug, PartialEq, Eq, strum::Display, strum::EnumIter)] @@ -29,7 +29,7 @@ pub enum Framework { impl Framework { /// Returns a framework-specific struct that implements the trait `ShuttleInit` /// for writing framework-specific dependencies to `Cargo.toml` and generating - /// boilerplate code in `src/lib.rs`. + /// boilerplate code in `src/main.rs`. pub fn init_config(&self) -> Box { match self { Framework::ActixWeb => Box::new(ShuttleInitActixWeb), @@ -78,29 +78,43 @@ impl ShuttleInit for ShuttleInitActixWeb { get_dependency_version_fn, ); - set_inline_table_dependency_features( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-actix-web", + dependencies, + manifest_path, + url, + true, + get_dependency_version_fn, + ); + + set_key_value_dependency_version( + "tokio", dependencies, - vec!["web-actix-web".to_string()], + manifest_path, + url, + true, + get_dependency_version_fn, ); } fn get_boilerplate_code_for_framework(&self) -> &'static str { indoc! {r#" use actix_web::{get, web::ServiceConfig}; - use shuttle_service::ShuttleActixWeb; + use shuttle_actix_web::ShuttleActixWeb; #[get("/hello")] async fn hello_world() -> &'static str { "Hello World!" } - #[shuttle_service::main] + #[shuttle_runtime::main] async fn actix_web( - ) -> ShuttleActixWeb { - Ok(move |cfg: &mut ServiceConfig| { + ) -> ShuttleActixWeb { + let config = move |cfg: &mut ServiceConfig| { cfg.service(hello_world); - }) + }; + + Ok(config.into()) }"#} } } @@ -124,17 +138,21 @@ impl ShuttleInit for ShuttleInitAxum { get_dependency_version_fn, ); - set_inline_table_dependency_features( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-axum", dependencies, - vec!["web-axum".to_string()], + manifest_path, + url, + true, + get_dependency_version_fn, ); + set_key_value_dependency_version( - "sync_wrapper", + "tokio", dependencies, manifest_path, url, - false, + true, get_dependency_version_fn, ); } @@ -142,18 +160,16 @@ impl ShuttleInit for ShuttleInitAxum { fn get_boilerplate_code_for_framework(&self) -> &'static str { indoc! {r#" use axum::{routing::get, Router}; - use sync_wrapper::SyncWrapper; async fn hello_world() -> &'static str { "Hello, world!" } - #[shuttle_service::main] - async fn axum() -> shuttle_service::ShuttleAxum { + #[shuttle_runtime::main] + async fn axum() -> shuttle_axum::ShuttleAxum { let router = Router::new().route("/hello", get(hello_world)); - let sync_wrapper = SyncWrapper::new(router); - Ok(sync_wrapper) + Ok(router.into()) }"#} } } @@ -177,10 +193,22 @@ impl ShuttleInit for ShuttleInitRocket { get_dependency_version_fn, ); - set_inline_table_dependency_features( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-rocket", + dependencies, + manifest_path, + url, + true, + get_dependency_version_fn, + ); + + set_key_value_dependency_version( + "tokio", dependencies, - vec!["web-rocket".to_string()], + manifest_path, + url, + true, + get_dependency_version_fn, ); } @@ -194,11 +222,11 @@ impl ShuttleInit for ShuttleInitRocket { "Hello, world!" } - #[shuttle_service::main] - async fn rocket() -> shuttle_service::ShuttleRocket { + #[shuttle_runtime::main] + async fn rocket() -> shuttle_rocket::ShuttleRocket { let rocket = rocket::build().mount("/hello", routes![index]); - Ok(rocket) + Ok(rocket.into()) }"#} } } @@ -213,10 +241,22 @@ impl ShuttleInit for ShuttleInitTide { url: &Url, get_dependency_version_fn: GetDependencyVersionFn, ) { - set_inline_table_dependency_features( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-tide", dependencies, - vec!["web-tide".to_string()], + manifest_path, + url, + true, + get_dependency_version_fn, + ); + + set_key_value_dependency_version( + "tokio", + dependencies, + manifest_path, + url, + true, + get_dependency_version_fn, ); set_key_value_dependency_version( @@ -231,14 +271,14 @@ impl ShuttleInit for ShuttleInitTide { fn get_boilerplate_code_for_framework(&self) -> &'static str { indoc! {r#" - #[shuttle_service::main] - async fn tide() -> shuttle_service::ShuttleTide<()> { + #[shuttle_runtime::main] + async fn tide() -> shuttle_tide::ShuttleTide<()> { let mut app = tide::new(); app.with(tide::log::LogMiddleware::new()); app.at("/hello").get(|_| async { Ok("Hello, world!") }); - Ok(app) + Ok(app.into()) }"#} } } @@ -253,18 +293,30 @@ impl ShuttleInit for ShuttleInitPoem { url: &Url, get_dependency_version_fn: GetDependencyVersionFn, ) { - set_inline_table_dependency_features( - "shuttle-service", + set_key_value_dependency_version( + "poem", dependencies, - vec!["web-poem".to_string()], + manifest_path, + url, + false, + get_dependency_version_fn, ); set_key_value_dependency_version( - "poem", + "shuttle-poem", dependencies, manifest_path, url, - false, + true, + get_dependency_version_fn, + ); + + set_key_value_dependency_version( + "tokio", + dependencies, + manifest_path, + url, + true, get_dependency_version_fn, ); } @@ -272,17 +324,18 @@ impl ShuttleInit for ShuttleInitPoem { fn get_boilerplate_code_for_framework(&self) -> &'static str { indoc! {r#" use poem::{get, handler, Route}; + use shuttle_poem::ShuttlePoem; #[handler] fn hello_world() -> &'static str { "Hello, world!" } - #[shuttle_service::main] - async fn poem() -> shuttle_service::ShuttlePoem { + #[shuttle_runtime::main] + async fn poem() -> ShuttlePoem { let app = Route::new().at("/hello", get(hello_world)); - Ok(app) + Ok(app.into()) }"#} } } @@ -297,18 +350,30 @@ impl ShuttleInit for ShuttleInitSalvo { url: &Url, get_dependency_version_fn: GetDependencyVersionFn, ) { - set_inline_table_dependency_features( - "shuttle-service", + set_key_value_dependency_version( + "salvo", dependencies, - vec!["web-salvo".to_string()], + manifest_path, + url, + false, + get_dependency_version_fn, ); set_key_value_dependency_version( - "salvo", + "shuttle-salvo", dependencies, manifest_path, url, - false, + true, + get_dependency_version_fn, + ); + + set_key_value_dependency_version( + "tokio", + dependencies, + manifest_path, + url, + true, get_dependency_version_fn, ); } @@ -319,14 +384,14 @@ impl ShuttleInit for ShuttleInitSalvo { #[handler] async fn hello_world(res: &mut Response) { - res.render(Text::Plain("Hello, World!")); + res.render(Text::Plain("Hello, world!")); } - #[shuttle_service::main] - async fn salvo() -> shuttle_service::ShuttleSalvo { - let router = Router::new().get(hello_world); + #[shuttle_runtime::main] + async fn salvo() -> shuttle_salvo::ShuttleSalvo { + let router = Router::with_path("hello").get(hello_world); - Ok(router) + Ok(router.into()) }"#} } } @@ -341,12 +406,6 @@ impl ShuttleInit for ShuttleInitSerenity { url: &Url, get_dependency_version_fn: GetDependencyVersionFn, ) { - set_inline_table_dependency_features( - "shuttle-service", - dependencies, - vec!["bot-serenity".to_string()], - ); - set_key_value_dependency_version( "anyhow", dependencies, @@ -387,6 +446,24 @@ impl ShuttleInit for ShuttleInitSerenity { get_dependency_version_fn, ); + set_key_value_dependency_version( + "shuttle-serenity", + dependencies, + manifest_path, + url, + true, + get_dependency_version_fn, + ); + + set_key_value_dependency_version( + "tokio", + dependencies, + manifest_path, + url, + true, + get_dependency_version_fn, + ); + set_key_value_dependency_version( "tracing", dependencies, @@ -424,10 +501,10 @@ impl ShuttleInit for ShuttleInitSerenity { } } - #[shuttle_service::main] + #[shuttle_runtime::main] async fn serenity( #[shuttle_secrets::Secrets] secret_store: SecretStore, - ) -> shuttle_service::ShuttleSerenity { + ) -> shuttle_serenity::ShuttleSerenity { // Get the discord token set in `Secrets.toml` let token = if let Some(token) = secret_store.get("DISCORD_TOKEN") { token @@ -443,7 +520,7 @@ impl ShuttleInit for ShuttleInitSerenity { .await .expect("Err creating client"); - Ok(client) + Ok(client.into()) }"#} } } @@ -458,12 +535,6 @@ impl ShuttleInit for ShuttleInitPoise { url: &Url, get_dependency_version_fn: GetDependencyVersionFn, ) { - set_inline_table_dependency_features( - "shuttle-service", - dependencies, - vec!["bot-poise".to_string()], - ); - set_key_value_dependency_version( "anyhow", dependencies, @@ -482,6 +553,15 @@ impl ShuttleInit for ShuttleInitPoise { get_dependency_version_fn, ); + set_key_value_dependency_version( + "shuttle-poise", + dependencies, + manifest_path, + url, + true, + get_dependency_version_fn, + ); + set_key_value_dependency_version( "shuttle-secrets", dependencies, @@ -491,6 +571,15 @@ impl ShuttleInit for ShuttleInitPoise { get_dependency_version_fn, ); + set_key_value_dependency_version( + "tokio", + dependencies, + manifest_path, + url, + true, + get_dependency_version_fn, + ); + set_key_value_dependency_version( "tracing", dependencies, @@ -504,47 +593,47 @@ impl ShuttleInit for ShuttleInitPoise { fn get_boilerplate_code_for_framework(&self) -> &'static str { indoc! {r#" use anyhow::Context as _; - use poise::serenity_prelude as serenity; - use shuttle_secrets::SecretStore; - use shuttle_service::ShuttlePoise; - - struct Data {} // User data, which is stored and accessible in all command invocations - type Error = Box; - type Context<'a> = poise::Context<'a, Data, Error>; - - /// Responds with "world!" - #[poise::command(slash_command)] - async fn hello(ctx: Context<'_>) -> Result<(), Error> { - ctx.say("world!").await?; - Ok(()) - } - - #[shuttle_service::main] - async fn poise(#[shuttle_secrets::Secrets] secret_store: SecretStore) -> ShuttlePoise { - // Get the discord token set in `Secrets.toml` - let discord_token = secret_store - .get("DISCORD_TOKEN") - .context("'DISCORD_TOKEN' was not found")?; - - let framework = poise::Framework::builder() - .options(poise::FrameworkOptions { - commands: vec![hello()], - ..Default::default() - }) - .token(discord_token) - .intents(serenity::GatewayIntents::non_privileged()) - .setup(|ctx, _ready, framework| { - Box::pin(async move { - poise::builtins::register_globally(ctx, &framework.options().commands).await?; - Ok(Data {}) - }) - }) - .build() - .await - .map_err(shuttle_service::error::CustomError::new)?; - - Ok(framework) - }"#} + use poise::serenity_prelude as serenity; + use shuttle_secrets::SecretStore; + use shuttle_poise::ShuttlePoise; + + struct Data {} // User data, which is stored and accessible in all command invocations + type Error = Box; + type Context<'a> = poise::Context<'a, Data, Error>; + + /// Responds with "world!" + #[poise::command(slash_command)] + async fn hello(ctx: Context<'_>) -> Result<(), Error> { + ctx.say("world!").await?; + Ok(()) + } + + #[shuttle_runtime::main] + async fn poise(#[shuttle_secrets::Secrets] secret_store: SecretStore) -> ShuttlePoise { + // Get the discord token set in `Secrets.toml` + let discord_token = secret_store + .get("DISCORD_TOKEN") + .context("'DISCORD_TOKEN' was not found")?; + + let framework = poise::Framework::builder() + .options(poise::FrameworkOptions { + commands: vec![hello()], + ..Default::default() + }) + .token(discord_token) + .intents(serenity::GatewayIntents::non_privileged()) + .setup(|ctx, _ready, framework| { + Box::pin(async move { + poise::builtins::register_globally(ctx, &framework.options().commands).await?; + Ok(Data {}) + }) + }) + .build() + .await + .map_err(shuttle_runtime::CustomError::new)?; + + Ok(framework.into()) + }"#} } } @@ -558,25 +647,37 @@ impl ShuttleInit for ShuttleInitTower { url: &Url, get_dependency_version_fn: GetDependencyVersionFn, ) { - set_inline_table_dependency_features( - "shuttle-service", + set_inline_table_dependency_version( + "hyper", dependencies, - vec!["web-tower".to_string()], + manifest_path, + url, + false, + get_dependency_version_fn, ); - set_inline_table_dependency_version( - "tower", + set_inline_table_dependency_features("hyper", dependencies, vec!["full".to_string()]); + + set_key_value_dependency_version( + "shuttle-tower", dependencies, manifest_path, url, - false, + true, get_dependency_version_fn, ); - set_inline_table_dependency_features("tower", dependencies, vec!["full".to_string()]); + set_key_value_dependency_version( + "tokio", + dependencies, + manifest_path, + url, + true, + get_dependency_version_fn, + ); set_inline_table_dependency_version( - "hyper", + "tower", dependencies, manifest_path, url, @@ -584,7 +685,7 @@ impl ShuttleInit for ShuttleInitTower { get_dependency_version_fn, ); - set_inline_table_dependency_features("hyper", dependencies, vec!["full".to_string()]); + set_inline_table_dependency_features("tower", dependencies, vec!["full".to_string()]); } fn get_boilerplate_code_for_framework(&self) -> &'static str { @@ -619,9 +720,11 @@ impl ShuttleInit for ShuttleInitTower { } } - #[shuttle_service::main] - async fn tower() -> Result { - Ok(HelloWorld) + #[shuttle_runtime::main] + async fn tower() -> shuttle_tower::ShuttleTower { + let service = HelloWorld; + + Ok(service.into()) }"#} } } @@ -636,10 +739,22 @@ impl ShuttleInit for ShuttleInitWarp { url: &Url, get_dependency_version_fn: GetDependencyVersionFn, ) { - set_inline_table_dependency_features( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-warp", dependencies, - vec!["web-warp".to_string()], + manifest_path, + url, + true, + get_dependency_version_fn, + ); + + set_key_value_dependency_version( + "tokio", + dependencies, + manifest_path, + url, + true, + get_dependency_version_fn, ); set_key_value_dependency_version( @@ -654,13 +769,13 @@ impl ShuttleInit for ShuttleInitWarp { fn get_boilerplate_code_for_framework(&self) -> &'static str { indoc! {r#" - use warp::Filter; - use warp::Reply; - - #[shuttle_service::main] - async fn warp() -> shuttle_service::ShuttleWarp<(impl Reply,)> { - let route = warp::any().map(|| "Hello, World"); - Ok(route.boxed()) + use warp::Filter; + use warp::Reply; + + #[shuttle_runtime::main] + async fn warp() -> shuttle_warp::ShuttleWarp<(impl Reply,)> { + let route = warp::any().map(|| "Hello, World!"); + Ok(route.boxed().into()) }"#} } } @@ -675,10 +790,13 @@ impl ShuttleInit for ShuttleInitThruster { url: &Url, get_dependency_version_fn: GetDependencyVersionFn, ) { - set_inline_table_dependency_features( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-thruster", dependencies, - vec!["web-thruster".to_string()], + manifest_path, + url, + true, + get_dependency_version_fn, ); set_inline_table_dependency_version( @@ -695,6 +813,15 @@ impl ShuttleInit for ShuttleInitThruster { dependencies, vec!["hyper_server".to_string()], ); + + set_key_value_dependency_version( + "tokio", + dependencies, + manifest_path, + url, + true, + get_dependency_version_fn, + ); } fn get_boilerplate_code_for_framework(&self) -> &'static str { @@ -703,20 +830,21 @@ impl ShuttleInit for ShuttleInitThruster { context::basic_hyper_context::{generate_context, BasicHyperContext as Ctx, HyperRequest}, m, middleware_fn, App, HyperServer, MiddlewareNext, MiddlewareResult, ThrusterServer, }; - + #[middleware_fn] async fn hello(mut context: Ctx, _next: MiddlewareNext) -> MiddlewareResult { context.body("Hello, World!"); Ok(context) } - - #[shuttle_service::main] - async fn thruster() -> shuttle_service::ShuttleThruster> { - Ok(HyperServer::new( + + #[shuttle_runtime::main] + async fn thruster() -> shuttle_thruster::ShuttleThruster> { + let server = HyperServer::new( App::::create(generate_context, ()).get("/hello", m![hello]), - )) - } - "#} + ); + + Ok(server.into()) + }"#} } } @@ -736,21 +864,21 @@ impl ShuttleInit for ShuttleInitNoOp { } } -/// Interoprates with `cargo` crate and calls `cargo init --libs [path]`. +/// Interoprates with `cargo` crate and calls `cargo init [path]`. pub fn cargo_init(path: PathBuf) -> Result<()> { - let opts = NewOptions::new(None, false, true, path, None, None, None)?; + let opts = NewOptions::new(None, true, false, path, None, None, None)?; let cargo_config = cargo::util::config::Config::default()?; let init_result = cargo::ops::init(&opts, &cargo_config)?; // Mimic `cargo init` behavior and log status or error to shell cargo_config .shell() - .status("Created", format!("{} (shuttle) package", init_result))?; + .status("Created", format!("{init_result} (shuttle) package"))?; Ok(()) } -/// Performs shuttle init on the existing files generated by `cargo init --libs [path]`. +/// Performs shuttle init on the existing files generated by `cargo init [path]`. pub fn cargo_shuttle_init(path: PathBuf, framework: Framework) -> Result<()> { let cargo_toml_path = path.join("Cargo.toml"); let mut cargo_doc = read_to_string(cargo_toml_path.clone()) @@ -758,36 +886,31 @@ pub fn cargo_shuttle_init(path: PathBuf, framework: Framework) -> Result<()> { .parse::() .unwrap(); - // Remove empty dependencies table to re-insert after the lib table is inserted - cargo_doc.remove("dependencies"); - - // Create an empty `[lib]` table - cargo_doc["lib"] = Item::Table(Table::new()); - // Add publish: false to avoid accidental `cargo publish` cargo_doc["package"]["publish"] = value(false); - // Create `[dependencies]` table - let mut dependencies = Table::new(); + // Get `[dependencies]` table + let dependencies = cargo_doc["dependencies"] + .as_table_mut() + .expect("manifest to have a dependencies table"); - // Set "shuttle-service" version to `[dependencies]` table let manifest_path = find(Some(path.as_path())).unwrap(); let url = registry_url(manifest_path.as_path(), None).expect("Could not find registry URL"); - set_inline_table_dependency_version( - "shuttle-service", - &mut dependencies, + let init_config = framework.init_config(); + + set_key_value_dependency_version( + "shuttle-runtime", + dependencies, &manifest_path, &url, - false, + true, // TODO: disallow pre-release when releasing 0.12? get_latest_dependency_version, ); - let init_config = framework.init_config(); - // Set framework-specific dependencies to the `dependencies` table init_config.set_cargo_dependencies( - &mut dependencies, + dependencies, &manifest_path, &url, get_latest_dependency_version, @@ -796,14 +919,13 @@ pub fn cargo_shuttle_init(path: PathBuf, framework: Framework) -> Result<()> { // Truncate Cargo.toml and write the updated `Document` to it let mut cargo_toml = File::create(cargo_toml_path)?; - cargo_doc["dependencies"] = Item::Table(dependencies); cargo_toml.write_all(cargo_doc.to_string().as_bytes())?; - // Write boilerplate to `src/lib.rs` file - let lib_path = path.join("src").join("lib.rs"); + // Write boilerplate to `src/main.rs` file + let main_path = path.join("src").join("main.rs"); let boilerplate = init_config.get_boilerplate_code_for_framework(); if !boilerplate.is_empty() { - write_lib_file(boilerplate, &lib_path)?; + write_main_file(boilerplate, &main_path)?; } Ok(()) @@ -863,7 +985,7 @@ fn get_latest_dependency_version( ) -> String { let latest_version = get_latest_dependency(crate_name, flag_allow_prerelease, manifest_path, Some(url)) - .unwrap_or_else(|_| panic!("Could not query the latest version of {}", crate_name)); + .unwrap_or_else(|_| panic!("Could not query the latest version of {crate_name}")); let latest_version = latest_version .version() .expect("No latest shuttle-service version available"); @@ -871,10 +993,10 @@ fn get_latest_dependency_version( latest_version.to_string() } -/// Writes `boilerplate` code to the specified `lib.rs` file path. -pub fn write_lib_file(boilerplate: &'static str, lib_path: &Path) -> Result<()> { - let mut lib_file = File::create(lib_path)?; - lib_file.write_all(boilerplate.as_bytes())?; +/// Writes `boilerplate` code to the specified `main.rs` file path. +pub fn write_main_file(boilerplate: &'static str, main_path: &Path) -> Result<()> { + let mut main_file = File::create(main_path)?; + main_file.write_all(boilerplate.as_bytes())?; Ok(()) } @@ -908,12 +1030,12 @@ mod shuttle_init_tests { set_inline_table_dependency_features( "shuttle-service", dependencies, - vec!["test-feature".to_string()], + vec!["builder".to_string()], ); let expected = indoc! {r#" [dependencies] - shuttle-service = { features = ["test-feature"] } + shuttle-service = { features = ["builder"] } "#}; assert_eq!(cargo_toml.to_string(), expected); @@ -973,12 +1095,12 @@ mod shuttle_init_tests { let manifest_path = PathBuf::new(); let url = Url::parse("https://shuttle.rs").unwrap(); - set_inline_table_dependency_version( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-runtime", dependencies, &manifest_path, &url, - false, + true, mock_get_latest_dependency_version, ); @@ -991,8 +1113,10 @@ mod shuttle_init_tests { let expected = indoc! {r#" [dependencies] - shuttle-service = { version = "1.0", features = ["web-actix-web"] } + shuttle-runtime = "1.0" actix-web = "1.0" + shuttle-actix-web = "1.0" + tokio = "1.0" "#}; assert_eq!(cargo_toml.to_string(), expected); @@ -1005,12 +1129,12 @@ mod shuttle_init_tests { let manifest_path = PathBuf::new(); let url = Url::parse("https://shuttle.rs").unwrap(); - set_inline_table_dependency_version( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-runtime", dependencies, &manifest_path, &url, - false, + true, mock_get_latest_dependency_version, ); @@ -1023,9 +1147,10 @@ mod shuttle_init_tests { let expected = indoc! {r#" [dependencies] - shuttle-service = { version = "1.0", features = ["web-axum"] } + shuttle-runtime = "1.0" axum = "1.0" - sync_wrapper = "1.0" + shuttle-axum = "1.0" + tokio = "1.0" "#}; assert_eq!(cargo_toml.to_string(), expected); @@ -1038,12 +1163,12 @@ mod shuttle_init_tests { let manifest_path = PathBuf::new(); let url = Url::parse("https://shuttle.rs").unwrap(); - set_inline_table_dependency_version( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-runtime", dependencies, &manifest_path, &url, - false, + true, mock_get_latest_dependency_version, ); @@ -1056,8 +1181,10 @@ mod shuttle_init_tests { let expected = indoc! {r#" [dependencies] - shuttle-service = { version = "1.0", features = ["web-rocket"] } + shuttle-runtime = "1.0" rocket = "1.0" + shuttle-rocket = "1.0" + tokio = "1.0" "#}; assert_eq!(cargo_toml.to_string(), expected); @@ -1070,12 +1197,12 @@ mod shuttle_init_tests { let manifest_path = PathBuf::new(); let url = Url::parse("https://shuttle.rs").unwrap(); - set_inline_table_dependency_version( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-runtime", dependencies, &manifest_path, &url, - false, + true, mock_get_latest_dependency_version, ); @@ -1088,7 +1215,9 @@ mod shuttle_init_tests { let expected = indoc! {r#" [dependencies] - shuttle-service = { version = "1.0", features = ["web-tide"] } + shuttle-runtime = "1.0" + shuttle-tide = "1.0" + tokio = "1.0" tide = "1.0" "#}; @@ -1102,12 +1231,12 @@ mod shuttle_init_tests { let manifest_path = PathBuf::new(); let url = Url::parse("https://shuttle.rs").unwrap(); - set_inline_table_dependency_version( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-runtime", dependencies, &manifest_path, &url, - false, + true, mock_get_latest_dependency_version, ); @@ -1120,9 +1249,11 @@ mod shuttle_init_tests { let expected = indoc! {r#" [dependencies] - shuttle-service = { version = "1.0", features = ["web-tower"] } - tower = { version = "1.0", features = ["full"] } + shuttle-runtime = "1.0" hyper = { version = "1.0", features = ["full"] } + shuttle-tower = "1.0" + tokio = "1.0" + tower = { version = "1.0", features = ["full"] } "#}; assert_eq!(cargo_toml.to_string(), expected); @@ -1135,12 +1266,12 @@ mod shuttle_init_tests { let manifest_path = PathBuf::new(); let url = Url::parse("https://shuttle.rs").unwrap(); - set_inline_table_dependency_version( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-runtime", dependencies, &manifest_path, &url, - false, + true, mock_get_latest_dependency_version, ); @@ -1153,8 +1284,10 @@ mod shuttle_init_tests { let expected = indoc! {r#" [dependencies] - shuttle-service = { version = "1.0", features = ["web-poem"] } + shuttle-runtime = "1.0" poem = "1.0" + shuttle-poem = "1.0" + tokio = "1.0" "#}; assert_eq!(cargo_toml.to_string(), expected); @@ -1167,12 +1300,12 @@ mod shuttle_init_tests { let manifest_path = PathBuf::new(); let url = Url::parse("https://shuttle.rs").unwrap(); - set_inline_table_dependency_version( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-runtime", dependencies, &manifest_path, &url, - false, + true, mock_get_latest_dependency_version, ); @@ -1185,8 +1318,10 @@ mod shuttle_init_tests { let expected = indoc! {r#" [dependencies] - shuttle-service = { version = "1.0", features = ["web-salvo"] } + shuttle-runtime = "1.0" salvo = "1.0" + shuttle-salvo = "1.0" + tokio = "1.0" "#}; assert_eq!(cargo_toml.to_string(), expected); @@ -1199,12 +1334,12 @@ mod shuttle_init_tests { let manifest_path = PathBuf::new(); let url = Url::parse("https://shuttle.rs").unwrap(); - set_inline_table_dependency_version( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-runtime", dependencies, &manifest_path, &url, - false, + true, mock_get_latest_dependency_version, ); @@ -1217,10 +1352,12 @@ mod shuttle_init_tests { let expected = indoc! {r#" [dependencies] - shuttle-service = { version = "1.0", features = ["bot-serenity"] } + shuttle-runtime = "1.0" anyhow = "1.0" serenity = { version = "1.0", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } shuttle-secrets = "1.0" + shuttle-serenity = "1.0" + tokio = "1.0" tracing = "1.0" "#}; @@ -1234,12 +1371,12 @@ mod shuttle_init_tests { let manifest_path = PathBuf::new(); let url = Url::parse("https://shuttle.rs").unwrap(); - set_inline_table_dependency_version( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-runtime", dependencies, &manifest_path, &url, - false, + true, mock_get_latest_dependency_version, ); @@ -1251,12 +1388,14 @@ mod shuttle_init_tests { ); let expected = indoc! {r#" - [dependencies] - shuttle-service = { version = "1.0", features = ["bot-poise"] } - anyhow = "1.0" - poise = "1.0" - shuttle-secrets = "1.0" - tracing = "1.0" + [dependencies] + shuttle-runtime = "1.0" + anyhow = "1.0" + poise = "1.0" + shuttle-poise = "1.0" + shuttle-secrets = "1.0" + tokio = "1.0" + tracing = "1.0" "#}; assert_eq!(cargo_toml.to_string(), expected); @@ -1269,12 +1408,12 @@ mod shuttle_init_tests { let manifest_path = PathBuf::new(); let url = Url::parse("https://shuttle.rs").unwrap(); - set_inline_table_dependency_version( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-runtime", dependencies, &manifest_path, &url, - false, + true, mock_get_latest_dependency_version, ); @@ -1287,7 +1426,9 @@ mod shuttle_init_tests { let expected = indoc! {r#" [dependencies] - shuttle-service = { version = "1.0", features = ["web-warp"] } + shuttle-runtime = "1.0" + shuttle-warp = "1.0" + tokio = "1.0" warp = "1.0" "#}; @@ -1301,12 +1442,12 @@ mod shuttle_init_tests { let manifest_path = PathBuf::new(); let url = Url::parse("https://shuttle.rs").unwrap(); - set_inline_table_dependency_version( - "shuttle-service", + set_key_value_dependency_version( + "shuttle-runtime", dependencies, &manifest_path, &url, - false, + true, mock_get_latest_dependency_version, ); @@ -1319,13 +1460,17 @@ mod shuttle_init_tests { let expected = indoc! {r#" [dependencies] - shuttle-service = { version = "1.0", features = ["web-thruster"] } + shuttle-runtime = "1.0" + shuttle-thruster = "1.0" thruster = { version = "1.0", features = ["hyper_server"] } + tokio = "1.0" "#}; assert_eq!(cargo_toml.to_string(), expected); } + // TODO: unignore this test when we publish shuttle-rocket + #[ignore] #[test] /// Makes sure that Rocket uses allow_prerelease flag when fetching the latest version fn test_get_latest_dependency_version_rocket() { @@ -1334,6 +1479,15 @@ mod shuttle_init_tests { let manifest_path = PathBuf::new(); let url = Url::parse("https://github.com/rust-lang/crates.io-index").unwrap(); + set_key_value_dependency_version( + "shuttle-runtime", + dependencies, + &manifest_path, + &url, + true, + mock_get_latest_dependency_version, + ); + ShuttleInitRocket.set_cargo_dependencies( dependencies, &manifest_path, diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index 5f97aa675..9463e6b30 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -1,14 +1,16 @@ mod args; mod client; pub mod config; -mod factory; mod init; +mod provisioner_server; +use cargo::util::ToSemver; use indicatif::ProgressBar; use shuttle_common::models::project::{State, IDLE_MINUTES}; use shuttle_common::project::ProjectName; +use shuttle_proto::runtime::{self, LoadRequest, StartRequest, SubscribeLogsRequest}; -use std::collections::BTreeMap; +use std::collections::HashMap; use std::ffi::OsString; use std::fs::{read_to_string, File}; use std::io::stdout; @@ -24,25 +26,26 @@ use clap_complete::{generate, Shell}; use config::RequestContext; use crossterm::style::Stylize; use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Input, Password}; -use factory::LocalFactory; use flate2::write::GzEncoder; use flate2::Compression; -use futures::StreamExt; +use futures::{StreamExt, TryFutureExt}; use git2::{Repository, StatusOptions}; use ignore::overrides::OverrideBuilder; use ignore::WalkBuilder; use shuttle_common::models::{project, secret}; -use shuttle_service::loader::{build_crate, Loader}; -use shuttle_service::Logger; +use shuttle_service::builder::{build_crate, Runtime}; use std::fmt::Write; use strum::IntoEnumIterator; use tar::Builder; -use tokio::sync::mpsc; -use tracing::trace; +use tracing::{trace, warn}; use uuid::Uuid; use crate::args::{DeploymentCommand, ProjectCommand}; use crate::client::Client; +use crate::provisioner_server::LocalProvisioner; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); pub struct Shuttle { ctx: RequestContext, @@ -133,9 +136,9 @@ impl Shuttle { println!("First, let's log in to your Shuttle account."); self.login(args.login_args.clone()).await?; println!(); - } else if args.new && args.login_args.api_key.is_some() { + } else if args.login_args.api_key.is_some() { self.login(args.login_args.clone()).await?; - } else { + } else if args.new { bail!("Tried to login to create a Shuttle environment, but no API key was set.") } } @@ -154,11 +157,17 @@ impl Shuttle { // 3. Confirm the project directory let path = if interactive { + let path = args + .path + .to_str() + .context("path arg should always be set")?; + println!("Where should we create this project?"); let directory_str: String = Input::with_theme(&theme) .with_prompt("Directory") - .default(".".to_owned()) + .default(path.to_owned()) .interact()?; + println!(); args::parse_init_path(OsString::from(directory_str))? } else { @@ -413,7 +422,6 @@ impl Shuttle { }); let working_directory = self.ctx.working_directory(); - let id = Default::default(); trace!("building project"); println!( @@ -422,7 +430,7 @@ impl Shuttle { working_directory.display() ); - let so_path = build_crate(id, working_directory, run_args.release, tx).await?; + let runtime = build_crate(working_directory, run_args.release, tx).await?; trace!("loading secrets"); @@ -432,26 +440,137 @@ impl Shuttle { working_directory.join("Secrets.toml") }; - let secrets: BTreeMap = - if let Ok(secrets_str) = read_to_string(secrets_path) { - let secrets: BTreeMap = - secrets_str.parse::()?.try_into()?; + let secrets: HashMap = if let Ok(secrets_str) = read_to_string(secrets_path) + { + let secrets: HashMap = + secrets_str.parse::()?.try_into()?; - trace!(keys = ?secrets.keys(), "available secrets"); + trace!(keys = ?secrets.keys(), "available secrets"); - secrets - } else { - trace!("no Secrets.toml was found"); - Default::default() - }; + secrets + } else { + trace!("no Secrets.toml was found"); + Default::default() + }; + + let service_name = self.ctx.project_name().to_string(); - let loader = Loader::from_so_file(so_path)?; + let (is_wasm, executable_path) = match runtime { + Runtime::Next(path) => (true, path), + Runtime::Alpha(path) => (false, path), + }; + + let provisioner = LocalProvisioner::new()?; + let provisioner_server = provisioner.start(SocketAddr::new( + Ipv4Addr::LOCALHOST.into(), + run_args.port + 1, + )); + + let runtime_path = || { + if is_wasm { + let runtime_path = home::cargo_home() + .expect("failed to find cargo home dir") + .join("bin/shuttle-next"); + + println!("Installing shuttle runtime. This can take a while..."); + + if cfg!(debug_assertions) { + // Canonicalized path to shuttle-runtime for dev to work on windows + let path = std::fs::canonicalize(format!("{MANIFEST_DIR}/../runtime")) + .expect("path to shuttle-runtime does not exist or is invalid"); + + trace!(?path, "installing runtime from local filesystem"); + + std::process::Command::new("cargo") + .arg("install") + .arg("shuttle-runtime") + .arg("--path") + .arg(path) + .arg("--bin") + .arg("shuttle-next") + .arg("--features") + .arg("next") + .output() + .expect("failed to install the shuttle runtime"); + } else { + // If the version of cargo-shuttle is different from shuttle-runtime, + // or it isn't installed, try to install shuttle-runtime from crates.io. + if let Err(err) = check_version(&runtime_path) { + warn!("{}", err); + + trace!("installing shuttle-runtime"); + std::process::Command::new("cargo") + .arg("install") + .arg("shuttle-runtime") + .arg("--bin") + .arg("shuttle-next") + .arg("--features") + .arg("next") + .output() + .expect("failed to install the shuttle runtime"); + }; + }; + + runtime_path + } else { + trace!(path = ?executable_path, "using alpha runtime"); + executable_path.clone() + } + }; - let mut factory = LocalFactory::new( - self.ctx.project_name().clone(), + let (mut runtime, mut runtime_client) = runtime::start( + is_wasm, + runtime::StorageManagerType::WorkingDir(working_directory.to_path_buf()), + &format!("http://localhost:{}", run_args.port + 1), + None, + run_args.port + 2, + runtime_path, + ) + .await + .map_err(|err| { + provisioner_server.abort(); + + err + })?; + + let load_request = tonic::Request::new(LoadRequest { + path: executable_path + .into_os_string() + .into_string() + .expect("to convert path to string"), + service_name: service_name.clone(), + resources: Default::default(), secrets, - working_directory.to_path_buf(), - )?; + }); + trace!("loading service"); + let _ = runtime_client + .load(load_request) + .or_else(|err| async { + provisioner_server.abort(); + runtime.kill().await?; + + Err(err) + }) + .await?; + + let mut stream = runtime_client + .subscribe_logs(tonic::Request::new(SubscribeLogsRequest {})) + .or_else(|err| async { + provisioner_server.abort(); + runtime.kill().await?; + + Err(err) + }) + .await? + .into_inner(); + + tokio::spawn(async move { + while let Ok(Some(log)) = stream.message().await { + let log: shuttle_common::LogItem = log.try_into().expect("to convert log"); + println!("{log}"); + } + }); + let addr = if run_args.external { std::net::IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) } else { @@ -460,30 +579,32 @@ impl Shuttle { let addr = SocketAddr::new(addr, run_args.port); - trace!("loading project"); + let start_request = StartRequest { + ip: addr.to_string(), + }; + + trace!(?start_request, "starting service"); + let response = runtime_client + .start(tonic::Request::new(start_request)) + .or_else(|err| async { + provisioner_server.abort(); + runtime.kill().await?; + + Err(err) + }) + .await? + .into_inner(); + + trace!(response = ?response, "client response: "); + println!( "\n{:>12} {} on http://{}", "Starting".bold().green(), self.ctx.project_name(), addr ); - let (tx, mut rx) = mpsc::unbounded_channel(); - tokio::spawn(async move { - while let Some(log) = rx.recv().await { - println!("{log}"); - } - }); - - let logger = Logger::new(tx, id); - let (handle, so) = loader.load(&mut factory, addr, logger).await?; - - handle.await??; - - tokio::task::spawn_blocking(move || { - trace!("closing so file"); - so.close().unwrap(); - }); + runtime.wait().await?; Ok(()) } @@ -768,6 +889,40 @@ impl Shuttle { } } +fn check_version(runtime_path: &Path) -> Result<()> { + let valid_version = VERSION.to_semver().unwrap(); + + if !runtime_path.try_exists()? { + bail!("shuttle-runtime is not installed"); + } + + // Get runtime version from shuttle-runtime cli + let runtime_version = std::process::Command::new("cargo") + .arg("shuttle-runtime") + .arg("--version") + .output() + .context("failed to check the shuttle-runtime version")? + .stdout; + + // Parse the version, splitting the version from the name and + // and pass it to `to_semver()`. + let runtime_version = std::str::from_utf8(&runtime_version) + .expect("shuttle-runtime version should be valid utf8") + .split_once(' ') + .expect("shuttle-runtime version should be in the `name version` format") + .1 + .to_semver() + .context("failed to convert runtime version to semver")?; + + if runtime_version == valid_version { + Ok(()) + } else { + Err(anyhow!( + "shuttle-runtime and cargo-shuttle are not the same version" + )) + } +} + fn create_spinner() -> ProgressBar { let pb = indicatif::ProgressBar::new_spinner(); pb.enable_steady_tick(std::time::Duration::from_millis(350)); @@ -899,7 +1054,7 @@ mod tests { "Secrets.toml", "Secrets.toml.example", "Shuttle.toml", - "src/lib.rs", + "src/main.rs", ] ); } diff --git a/cargo-shuttle/src/factory.rs b/cargo-shuttle/src/provisioner_server.rs similarity index 84% rename from cargo-shuttle/src/factory.rs rename to cargo-shuttle/src/provisioner_server.rs index fdbf12c57..3981c70f2 100644 --- a/cargo-shuttle/src/factory.rs +++ b/cargo-shuttle/src/provisioner_server.rs @@ -9,55 +9,53 @@ use bollard::{ }; use crossterm::{ cursor::{MoveDown, MoveUp}, - style::Stylize, terminal::{Clear, ClearType}, QueueableCommand, }; use futures::StreamExt; use portpicker::pick_unused_port; -use shuttle_common::{ - database::{AwsRdsEngine, SharedEngine}, - DatabaseReadyInfo, +use shuttle_common::database::{AwsRdsEngine, SharedEngine}; +use shuttle_proto::provisioner::{ + provisioner_server::{Provisioner, ProvisionerServer}, + DatabaseDeletionResponse, DatabaseRequest, DatabaseResponse, }; -use shuttle_service::{database::Type, error::CustomError, Environment, Factory, ServiceName}; -use std::{ - collections::{BTreeMap, HashMap}, - io::stdout, - path::PathBuf, - time::Duration, +use shuttle_service::database::Type; +use std::{collections::HashMap, io::stdout, net::SocketAddr, time::Duration}; +use tokio::{task::JoinHandle, time::sleep}; +use tonic::{ + transport::{self, Server}, + Request, Response, Status, }; -use tokio::time::sleep; use tracing::{error, trace}; -pub struct LocalFactory { +/// A provisioner for local runs +/// It uses Docker to create Databases +pub struct LocalProvisioner { docker: Docker, - service_name: ServiceName, - secrets: BTreeMap, - working_directory: PathBuf, } -impl LocalFactory { - pub fn new( - service_name: ServiceName, - secrets: BTreeMap, - working_directory: PathBuf, - ) -> Result { +impl LocalProvisioner { + pub fn new() -> Result { Ok(Self { docker: Docker::connect_with_local_defaults()?, - service_name, - secrets, - working_directory, }) } -} -#[async_trait] -impl Factory for LocalFactory { + pub fn start(self, address: SocketAddr) -> JoinHandle> { + tokio::spawn(async move { + Server::builder() + .add_service(ProvisionerServer::new(self)) + .serve(address) + .await + }) + } + async fn get_db_connection_string( - &mut self, + &self, + service_name: &str, db_type: Type, - ) -> Result { - trace!("getting sql string for service '{}'", self.service_name); + ) -> Result { + trace!("getting sql string for service '{}'", service_name); let EngineConfig { r#type, @@ -70,7 +68,7 @@ impl Factory for LocalFactory { env, is_ready_cmd, } = db_type_to_config(db_type); - let container_name = format!("shuttle_{}_{}", self.service_name, r#type); + let container_name = format!("shuttle_{service_name}_{type}"); let container = match self.docker.inspect_container(&container_name, None).await { Ok(container) => { @@ -118,7 +116,7 @@ impl Factory for LocalFactory { } Err(error) => { error!("got unexpected error while inspecting docker container: {error}"); - return Err(shuttle_service::Error::Custom(CustomError::new(error))); + return Err(Status::internal(error.to_string())); } }; @@ -153,56 +151,24 @@ impl Factory for LocalFactory { self.wait_for_ready(&container_name, is_ready_cmd).await?; - let db_info = DatabaseReadyInfo::new( + let res = DatabaseResponse { engine, username, password, database_name, port, - "localhost".to_string(), - "localhost".to_string(), - ); - - let conn_str = db_info.connection_string_private(); - - println!( - "{:>12} can be reached at {}\n", - "DB ready".bold().cyan(), - conn_str - ); - - Ok(conn_str) - } - - async fn get_secrets( - &mut self, - ) -> Result, shuttle_service::Error> { - Ok(self.secrets.clone()) - } - - fn get_service_name(&self) -> ServiceName { - self.service_name.clone() - } - - fn get_environment(&self) -> Environment { - Environment::Local - } - - fn get_build_path(&self) -> Result { - Ok(self.working_directory.clone()) - } + address_private: "localhost".to_string(), + address_public: "localhost".to_string(), + }; - fn get_storage_path(&self) -> Result { - Ok(self.working_directory.clone()) + Ok(res) } -} -impl LocalFactory { async fn wait_for_ready( &self, container_name: &str, is_ready_cmd: Vec, - ) -> Result<(), shuttle_service::Error> { + ) -> Result<(), Status> { loop { trace!("waiting for '{container_name}' to be ready for connections"); @@ -280,6 +246,34 @@ impl LocalFactory { } } +#[async_trait] +impl Provisioner for LocalProvisioner { + async fn provision_database( + &self, + request: Request, + ) -> Result, Status> { + let DatabaseRequest { + project_name, + db_type, + } = request.into_inner(); + + let db_type: Option = db_type.unwrap().into(); + + let res = self + .get_db_connection_string(&project_name, db_type.unwrap()) + .await?; + + Ok(Response::new(res)) + } + + async fn delete_database( + &self, + _request: Request, + ) -> Result, Status> { + panic!("local runner should not try to delete databases"); + } +} + fn print_layers(layers: &Vec) { for info in layers { stdout() @@ -303,7 +297,7 @@ fn print_layers(layers: &Vec) { (Some(status), _) => status.to_string(), _ => "Unknown".to_string(), }; - println!("[{id} {}]", text); + println!("[{id} {text}]"); } else { println!( "{}", diff --git a/cargo-shuttle/tests/integration/init.rs b/cargo-shuttle/tests/integration/init.rs index 45551314a..223987b5b 100644 --- a/cargo-shuttle/tests/integration/init.rs +++ b/cargo-shuttle/tests/integration/init.rs @@ -27,9 +27,10 @@ async fn non_interactive_basic_init() { Shuttle::new().unwrap().run(args).await.unwrap(); let cargo_toml = read_to_string(temp_dir_path.join("Cargo.toml")).unwrap(); + // Expected: name = "basic-initRANDOM_CHARS" assert!(cargo_toml.contains("name = \"basic-init")); - assert!(cargo_toml.contains("shuttle-service = { version = ")); + assert!(cargo_toml.contains("shuttle-runtime = ")); } #[tokio::test] @@ -167,11 +168,10 @@ fn interactive_rocket_init_dont_prompt_name() -> Result<(), Box shuttle_service::ShuttleRocket { + #[shuttle_runtime::main] + async fn rocket() -> shuttle_rocket::ShuttleRocket { let rocket = rocket::build().mount("/hello", routes![index]); - Ok(rocket) + Ok(rocket.into()) }"#}; - assert_eq!(lib_file, expected); + assert_eq!(main_file, expected); } diff --git a/cargo-shuttle/tests/integration/run.rs b/cargo-shuttle/tests/integration/run.rs index e11dbe3dd..311b78385 100644 --- a/cargo-shuttle/tests/integration/run.rs +++ b/cargo-shuttle/tests/integration/run.rs @@ -58,6 +58,7 @@ async fn cargo_shuttle_run(working_directory: &str, external: bool) -> String { } #[tokio::test(flavor = "multi_thread")] +#[ignore] async fn rocket_hello_world() { let url = cargo_shuttle_run("../examples/rocket/hello-world", false).await; @@ -91,6 +92,7 @@ async fn rocket_secrets() { // This example uses a shared Postgres. Thus local runs should create a docker container for it. #[tokio::test(flavor = "multi_thread")] +#[ignore] async fn rocket_postgres() { let url = cargo_shuttle_run("../examples/rocket/postgres", false).await; let client = reqwest::Client::new(); @@ -120,6 +122,59 @@ async fn rocket_postgres() { } #[tokio::test(flavor = "multi_thread")] +async fn axum_static_files() { + let url = cargo_shuttle_run("../examples/axum/static-files", false).await; + let client = reqwest::Client::new(); + + let request_text = client + .get(format!("{url}/hello")) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + assert_eq!(request_text, "Hello, world!"); + + let request_text = client.get(url).send().await.unwrap().text().await.unwrap(); + + assert!( + request_text.contains("This is an example of serving static files with axum and shuttle.") + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn shuttle_next() { + let url = cargo_shuttle_run("../examples/next/hello-world", false).await; + let client = reqwest::Client::new(); + + let request_text = client + .get(format!("{url}/hello")) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + assert_eq!(request_text, "Hello, World!"); + + let post_text = client + .post(format!("{url}/uppercase")) + .body("uppercase this") + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + assert_eq!(post_text, "UPPERCASE THIS"); +} + +#[tokio::test(flavor = "multi_thread")] +#[ignore] async fn rocket_authentication() { let url = cargo_shuttle_run("../examples/rocket/authentication", false).await; let client = reqwest::Client::new(); @@ -176,6 +231,7 @@ async fn rocket_authentication() { } #[tokio::test(flavor = "multi_thread")] +#[ignore] async fn actix_web_hello_world() { let url = cargo_shuttle_run("../examples/actix-web/hello-world", false).await; @@ -192,6 +248,7 @@ async fn actix_web_hello_world() { } #[tokio::test(flavor = "multi_thread")] +#[ignore] async fn axum_hello_world() { let url = cargo_shuttle_run("../examples/axum/hello-world", false).await; @@ -208,6 +265,7 @@ async fn axum_hello_world() { } #[tokio::test(flavor = "multi_thread")] +#[ignore] async fn tide_hello_world() { let url = cargo_shuttle_run("../examples/tide/hello-world", false).await; @@ -224,6 +282,7 @@ async fn tide_hello_world() { } #[tokio::test(flavor = "multi_thread")] +#[ignore] async fn tower_hello_world() { let url = cargo_shuttle_run("../examples/tower/hello-world", false).await; @@ -240,6 +299,7 @@ async fn tower_hello_world() { } #[tokio::test(flavor = "multi_thread")] +#[ignore] async fn warp_hello_world() { let url = cargo_shuttle_run("../examples/warp/hello-world", false).await; @@ -256,6 +316,7 @@ async fn warp_hello_world() { } #[tokio::test(flavor = "multi_thread")] +#[ignore] async fn poem_hello_world() { let url = cargo_shuttle_run("../examples/poem/hello-world", false).await; @@ -336,6 +397,7 @@ async fn poem_mongodb() { } #[tokio::test(flavor = "multi_thread")] +#[ignore] async fn salvo_hello_world() { let url = cargo_shuttle_run("../examples/salvo/hello-world", false).await; @@ -352,6 +414,7 @@ async fn salvo_hello_world() { } #[tokio::test(flavor = "multi_thread")] +#[ignore] async fn thruster_hello_world() { let url = cargo_shuttle_run("../examples/thruster/hello-world", false).await; diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index 51c56149d..9db8929d3 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-codegen" -version = "0.11.0" +version = "0.12.0" edition.workspace = true license.workspace = true repository.workspace = true @@ -19,3 +19,7 @@ syn = { version = "1.0.104", features = ["full", "extra-traits"] } [dev-dependencies] pretty_assertions = "1.3.0" trybuild = "1.0.72" + +[features] +frameworks = [] +next = [] diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index fd88d6267..eddf72e07 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -1,10 +1,33 @@ +#[cfg(feature = "next")] +mod next; +#[cfg(feature = "frameworks")] mod shuttle_main; use proc_macro::TokenStream; use proc_macro_error::proc_macro_error; +#[cfg(feature = "frameworks")] #[proc_macro_error] #[proc_macro_attribute] pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream { shuttle_main::r#impl(attr, item) } + +#[cfg(feature = "next")] +#[proc_macro_error] +#[proc_macro] +pub fn app(item: TokenStream) -> TokenStream { + use next::App; + use syn::{parse_macro_input, File}; + + let mut file = parse_macro_input!(item as File); + + let app = App::from_file(&mut file); + let bindings = next::wasi_bindings(app); + + quote::quote!( + #file + #bindings + ) + .into() +} diff --git a/codegen/src/next/mod.rs b/codegen/src/next/mod.rs new file mode 100644 index 000000000..4f62b12e1 --- /dev/null +++ b/codegen/src/next/mod.rs @@ -0,0 +1,641 @@ +use std::collections::HashMap; + +use proc_macro_error::emit_error; +use quote::{quote, ToTokens}; +use syn::{ + parenthesized, parse::Parse, parse2, punctuated::Punctuated, token::Paren, Expr, ExprLit, File, + Ident, Item, ItemFn, Lit, LitStr, Token, +}; + +#[derive(Debug, Eq, PartialEq)] +struct Endpoint { + route: LitStr, + method: Ident, + function: Ident, +} + +#[derive(Debug, Eq, PartialEq)] +struct Parameter { + key: Ident, + equals: Token![=], + value: Expr, +} + +#[derive(Debug, Eq, PartialEq)] +struct Params { + params: Punctuated, + paren_token: Paren, +} + +impl Parse for Parameter { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + Ok(Self { + key: input.parse()?, + equals: input.parse()?, + value: input.parse()?, + }) + } +} + +impl Parse for Params { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let content; + Ok(Self { + paren_token: parenthesized!(content in input), + params: content.parse_terminated(Parameter::parse)?, + }) + } +} + +impl Endpoint { + fn from_item_fn(item: &mut ItemFn) -> Option { + let function = item.sig.ident.clone(); + + let mut endpoint_index = None; + + // Find the index of an attribute that is an endpoint + for index in 0..item.attrs.len() { + // The endpoint ident should be the last segment in the path + if let Some(segment) = item.attrs[index].path.segments.last() { + if segment.ident.to_string().as_str() == "endpoint" { + // TODO: we should allow multiple endpoint attributes per handler. + // We could refactor this to return a Vec and then check + // that the combination of endpoints is valid. + if endpoint_index.is_some() { + emit_error!( + item, + "extra endpoint attribute"; + hint = "There should only be one endpoint annotation per handler function." + ); + return None; + } + endpoint_index = Some(index); + } + } else { + return None; + } + } + + // Strip the endpoint attribute if it exists + let endpoint = if let Some(index) = endpoint_index { + item.attrs.remove(index) + } else { + // This item does not have an endpoint attribute + return None; + }; + + // Parse the endpoint's parameters + let params: Params = match parse2(endpoint.tokens) { + Ok(params) => params, + Err(err) => { + // This will error on invalid parameter syntax + emit_error!(err.span(), err); + return None; + } + }; + + // We'll use the paren span for errors later + let paren = params.paren_token; + + if params.params.is_empty() { + emit_error!( + paren.span, + "missing endpoint arguments"; + hint = "The endpoint takes two arguments: `endpoint(method = get, route = \"/hello\")`" + ); + return None; + } + + // At this point an endpoint with params and valid syntax exists, so we will check for + // all errors before returning + let mut has_err = false; + + let mut route = None; + let mut method = None; + + for Parameter { key, value, .. } in params.params { + let key_ident = key.clone(); + match key.to_string().as_str() { + "method" => { + if method.is_some() { + emit_error!( + key_ident, + "duplicate endpoint method"; + hint = "The endpoint `method` should only be set once." + ); + has_err = true; + } + if let Expr::Path(path) = value { + let method_ident = path.path.segments[0].ident.clone(); + + match method_ident.to_string().as_str() { + "get" | "post" | "delete" | "put" | "options" | "head" | "trace" + | "patch" => { + method = Some(method_ident); + } + _ => { + emit_error!( + method_ident, + "method is not supported"; + hint = "Try one of the following: `get`, `post`, `delete`, `put`, `options`, `head`, `trace` or `patch`" + ); + has_err = true; + } + }; + }; + } + "route" => { + if route.is_some() { + emit_error!( + key_ident, + "duplicate endpoint route"; + hint = "The endpoint `route` should only be set once." + ); + has_err = true; + } + + if let Expr::Lit(ExprLit { + lit: Lit::Str(literal), + .. + }) = value + { + route = Some(literal); + } + } + _ => { + emit_error!( + key_ident, + "invalid endpoint argument"; + hint = "Only `method` and `route` are valid endpoint arguments." + ); + has_err = true; + } + } + } + + if route.is_none() { + emit_error!( + paren.span, + "no route provided"; + hint = "Add a route to your endpoint: `route = \"/hello\")`" + ); + has_err = true; + }; + + if method.is_none() { + emit_error!( + paren.span, + "no method provided"; + hint = "Add a method to your endpoint: `method = get`" + ); + has_err = true; + }; + + if has_err { + None + } else { + // Safe to unwrap because `has_err` is true if `route` or `method` is `None` + Some(Endpoint { + route: route.unwrap(), + method: method.unwrap(), + function, + }) + } + } +} + +#[derive(Debug, Eq, PartialEq)] +pub struct EndpointChain<'a> { + route: &'a LitStr, + handlers: Vec, +} + +#[derive(Debug, Eq, PartialEq)] +struct Handler { + method: Ident, + function: Ident, +} + +impl ToTokens for Endpoint { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { + route, + method, + function, + } = self; + + let route = quote!(.route(#route, #method(#function))); + + route.to_tokens(tokens); + } +} + +impl ToTokens for Handler { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { method, function } = self; + + let handler = quote!(#method(#function)); + + handler.to_tokens(tokens); + } +} + +impl<'a> ToTokens for EndpointChain<'a> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { route, handlers } = self; + + let route = quote!(.route(#route, shuttle_next::routing::#(#handlers).*)); + + route.to_tokens(tokens); + } +} + +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct App { + endpoints: Vec, +} + +impl App { + pub(crate) fn from_file(file: &mut File) -> Self { + let endpoints = file + .items + .iter_mut() + .filter_map(|item| { + if let Item::Fn(item_fn) = item { + Some(item_fn) + } else { + None + } + }) + .filter_map(Endpoint::from_item_fn) + .collect(); + + Self { endpoints } + } +} + +impl ToTokens for App { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { endpoints } = self; + + let mut endpoint_chains = endpoints + .iter() + .fold(HashMap::new(), |mut chain, endpoint| { + let entry = chain + .entry(&endpoint.route) + .or_insert_with(Vec::::new); + + let method = endpoint.method.clone(); + let function = endpoint.function.clone(); + + if entry.iter().any(|handler| handler.method == method) { + emit_error!( + method, + "only one method of each type is allowed per route"; + hint = format!("Remove one of the {} methods on the \"{}\" route.", method, endpoint.route.value()) + ); + } else { + entry.push(Handler { method, function }); + } + + chain + }) + .into_iter() + .map(|(key, value)| EndpointChain { + route: key, + handlers: value, + }) + .collect::>(); + + // syn::LitStr does not implement Ord, so rather than using a BTreeMap to build the chains, we + // use a HashMap and then sort the endpoint chains to ensure the output is deterministic. + endpoint_chains.sort_by(|a, b| a.route.value().cmp(&b.route.value())); + + let app = quote!( + async fn __app(request: shuttle_next::Request,) -> shuttle_next::response::Response + { + use shuttle_next::Service; + + let mut router = shuttle_next::Router::new() + #(#endpoint_chains)*; + + let response = router.call(request).await.unwrap(); + + response + } + ); + + app.to_tokens(tokens); + } +} + +pub(crate) fn wasi_bindings(app: App) -> proc_macro2::TokenStream { + quote!( + #app + + #[cfg(not(test))] + #[no_mangle] + #[allow(non_snake_case)] + pub extern "C" fn __SHUTTLE_Axum_call( + logs_fd: std::os::wasi::prelude::RawFd, + parts_fd: std::os::wasi::prelude::RawFd, + body_fd: std::os::wasi::prelude::RawFd, + ) { + use shuttle_next::body::{Body, HttpBody}; + use shuttle_next::tracing_prelude::*; + use shuttle_next::Logger; + use std::io::{Read, Write}; + use std::os::wasi::io::FromRawFd; + + // file descriptor 2 for writing logs to + let logs_fd = unsafe { std::fs::File::from_raw_fd(logs_fd) }; + + shuttle_next::tracing_registry() + .with(Logger::new(logs_fd)) + .init(); // this sets the subscriber as the global default and also adds a compatibility layer for capturing `log::Record`s + + // file descriptor 3 for reading and writing http parts + let mut parts_fd = unsafe { std::fs::File::from_raw_fd(parts_fd) }; + + let reader = std::io::BufReader::new(&mut parts_fd); + + // deserialize request parts from rust messagepack + let wrapper: shuttle_next::RequestWrapper = shuttle_next::from_read(reader).unwrap(); + + // file descriptor 4 for reading and writing http body + let mut body_stream = unsafe { std::fs::File::from_raw_fd(body_fd) }; + + let mut reader = std::io::BufReader::new(&mut body_stream); + let mut body_buf = Vec::new(); + reader.read_to_end(&mut body_buf).unwrap(); + + let body = Body::from(body_buf); + + let request = wrapper + .into_request_builder() + .body(shuttle_next::body::boxed(body)) + .unwrap(); + + let res = shuttle_next::block_on(__app(request)); + + let (parts, mut body) = res.into_parts(); + + // wrap and serialize response parts as rmp + let response_parts = shuttle_next::ResponseWrapper::from(parts) + .into_rmp() + .expect("failed to serialize response parts"); + + // write response parts + parts_fd.write_all(&response_parts).unwrap(); + + // write body if there is one + if let Some(body) = shuttle_next::block_on(body.data()) { + body_stream.write_all(body.unwrap().as_ref()).unwrap(); + } + } + ) +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use quote::quote; + use syn::parse_quote; + + use crate::next::{App, Parameter}; + + use super::{Endpoint, Params}; + + #[test] + fn endpoint_to_token() { + let endpoint = Endpoint { + route: parse_quote!("/hello"), + method: parse_quote!(get), + function: parse_quote!(hello), + }; + + let actual = quote!(#endpoint); + let expected = quote!(.route("/hello", get(hello))); + + assert_eq!(actual.to_string(), expected.to_string()); + } + + #[test] + #[rustfmt::skip::macros(quote)] + fn app_to_token() { + let cases = vec![ + ( + App { + endpoints: vec![ + Endpoint { + route: parse_quote!("/hello"), + method: parse_quote!(get), + function: parse_quote!(hello), + }, + Endpoint { + route: parse_quote!("/goodbye"), + method: parse_quote!(post), + function: parse_quote!(goodbye), + }, + ], + }, + quote!( + async fn __app( + request: shuttle_next::Request, + ) -> shuttle_next::response::Response { + use shuttle_next::Service; + + let mut router = shuttle_next::Router::new() + .route("/goodbye", shuttle_next::routing::post(goodbye)) + .route("/hello", shuttle_next::routing::get(hello)); + + let response = router.call(request).await.unwrap(); + + response + } + ), + ), + ( + App { + endpoints: vec![ + Endpoint { + route: parse_quote!("/hello"), + method: parse_quote!(get), + function: parse_quote!(hello), + }, + Endpoint { + route: parse_quote!("/goodbye"), + method: parse_quote!(get), + function: parse_quote!(get_goodbye), + }, + Endpoint { + route: parse_quote!("/goodbye"), + method: parse_quote!(post), + function: parse_quote!(post_goodbye), + }, + ], + }, + quote!( + async fn __app( + request: shuttle_next::Request, + ) -> shuttle_next::response::Response { + use shuttle_next::Service; + + let mut router = shuttle_next::Router::new() + .route( + "/goodbye", + shuttle_next::routing::get(get_goodbye).post(post_goodbye) + ) + .route("/hello", shuttle_next::routing::get(hello)); + + let response = router.call(request).await.unwrap(); + + response + } + ), + ), + ]; + + for (app, expected) in cases { + let actual = quote!(#app); + assert_eq!(actual.to_string(), expected.to_string()); + } + } + + #[test] + fn parse_endpoint() { + let cases = vec![ + ( + parse_quote! { + #[shuttle_codegen::endpoint(method = get, route = "/hello")] + async fn hello() -> &'static str { + "Hello, World!" + }}, + Some(Endpoint { + route: parse_quote!("/hello"), + method: parse_quote!(get), + function: parse_quote!(hello), + }), + 0, + ), + ( + parse_quote! { + #[doc = r" This attribute is not an endpoint so keep it"] + #[shuttle_codegen::endpoint(method = get, route = "/hello")] + async fn hello() -> &'static str { + "Hello, World!" + }}, + Some(Endpoint { + route: parse_quote!("/hello"), + method: parse_quote!(get), + function: parse_quote!(hello), + }), + 1, + ), + ( + parse_quote! { + /// This attribute is not an endpoint so keep it + async fn say_hello() -> &'static str { + "Hello, World!" + } + }, + None, + 1, + ), + ]; + + for (mut input, expected, remaining_attributes) in cases { + let actual = Endpoint::from_item_fn(&mut input); + + assert_eq!(actual, expected); + + // Verify that only endpoint attributes have been stripped + assert_eq!(input.attrs.len(), remaining_attributes); + } + } + + #[test] + fn parse_parameter() { + // test method param + let cases: Vec<(Parameter, Parameter)> = vec![ + ( + // parsing an identifier + parse_quote! { + method = get + }, + Parameter { + key: parse_quote!(method), + equals: parse_quote!(=), + value: parse_quote!(get), + }, + ), + ( + // parsing a string literal + parse_quote! { + route = "/hello" + }, + Parameter { + key: parse_quote!(route), + equals: parse_quote!(=), + value: parse_quote!("/hello"), + }, + ), + ]; + for (actual, expected) in cases { + assert_eq!(actual, expected); + } + } + + #[test] + fn parse_params() { + let actual: Params = parse_quote![(method = get, route = "/hello")]; + + let mut expected = Params { + params: Default::default(), + paren_token: Default::default(), + }; + expected.params.push(parse_quote!(method = get)); + expected.params.push(parse_quote!(route = "/hello")); + + assert_eq!(actual, expected); + } + + #[test] + fn parse_app() { + let mut input = parse_quote! { + #[shuttle_codegen::endpoint(method = get, route = "/hello")] + async fn hello() -> &'static str { + "Hello, World!" + } + + #[shuttle_codegen::endpoint(method = post, route = "/goodbye")] + async fn goodbye() -> &'static str { + "Goodbye, World!" + } + }; + + let actual = App::from_file(&mut input); + let expected = App { + endpoints: vec![ + Endpoint { + route: parse_quote!("/hello"), + method: parse_quote!(get), + function: parse_quote!(hello), + }, + Endpoint { + route: parse_quote!("/goodbye"), + method: parse_quote!(post), + function: parse_quote!(goodbye), + }, + ], + }; + + assert_eq!(actual, expected); + } + + #[test] + fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/next/*.rs"); + } +} diff --git a/codegen/src/shuttle_main/mod.rs b/codegen/src/shuttle_main/mod.rs index eb31904de..49d67101f 100644 --- a/codegen/src/shuttle_main/mod.rs +++ b/codegen/src/shuttle_main/mod.rs @@ -4,50 +4,32 @@ use quote::{quote, ToTokens}; use syn::{ parenthesized, parse::Parse, parse2, parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Paren, Attribute, Expr, ExprLit, FnArg, Ident, ItemFn, Lit, Pat, - PatIdent, Path, ReturnType, Signature, Stmt, Token, Type, + PatIdent, Path, ReturnType, Signature, Stmt, Token, Type, TypePath, }; pub(crate) fn r#impl(_attr: TokenStream, item: TokenStream) -> TokenStream { let mut fn_decl = parse_macro_input!(item as ItemFn); - let wrapper = Wrapper::from_item_fn(&mut fn_decl); + let loader = Loader::from_item_fn(&mut fn_decl); let expanded = quote! { - #wrapper - - fn __binder( - service: Box, - addr: std::net::SocketAddr, - runtime: &shuttle_service::Runtime, - ) -> shuttle_service::ServeHandle { - use shuttle_service::Context; - runtime.spawn(async move { service.bind(addr).await.context("failed to bind service").map_err(Into::into) }) + #[tokio::main] + async fn main() { + shuttle_runtime::start(loader).await; } - #fn_decl - - #[no_mangle] - pub extern "C" fn _create_service() -> *mut shuttle_service::Bootstrapper { - let builder: shuttle_service::StateBuilder> = - |factory, runtime, logger| Box::pin(__shuttle_wrapper(factory, runtime, logger)); + #loader - let bootstrapper = shuttle_service::Bootstrapper::new( - builder, - __binder, - shuttle_service::Runtime::new().unwrap(), - ); - - let boxed = Box::new(bootstrapper); - Box::into_raw(boxed) - } + #fn_decl }; expanded.into() } -struct Wrapper { +struct Loader { fn_ident: Ident, fn_inputs: Vec, + fn_return: TypePath, } #[derive(Debug, PartialEq)] @@ -55,7 +37,7 @@ struct Input { /// The identifier for a resource input ident: Ident, - /// The shuttle_service builder for this resource + /// The shuttle_runtime builder for this resource builder: Builder, } @@ -107,8 +89,18 @@ impl Parse for BuilderOption { } } -impl Wrapper { - pub(crate) fn from_item_fn(item_fn: &mut ItemFn) -> Self { +impl Loader { + pub(crate) fn from_item_fn(item_fn: &mut ItemFn) -> Option { + let fn_ident = item_fn.sig.ident.clone(); + + if fn_ident.to_string().as_str() == "main" { + emit_error!( + fn_ident, + "shuttle_runtime::main functions cannot be named `main`" + ); + return None; + } + let inputs: Vec<_> = item_fn .sig .inputs @@ -135,31 +127,36 @@ impl Wrapper { }) .collect(); - check_return_type(&item_fn.sig); - - Self { - fn_ident: item_fn.sig.ident.clone(), + check_return_type(item_fn.sig.clone()).map(|type_path| Self { + fn_ident: fn_ident.clone(), fn_inputs: inputs, - } + fn_return: type_path, + }) } } -fn check_return_type(signature: &Signature) { - match &signature.output { - ReturnType::Default => emit_error!( - signature, - "shuttle_service::main functions need to return a service"; - hint = "See the docs for services with first class support"; - doc = "https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services" - ), - ReturnType::Type(_, r#type) => match r#type.as_ref() { - Type::Path(_) => {} - _ => emit_error!( - r#type, - "shuttle_service::main functions need to return a first class service or 'Result"; +fn check_return_type(signature: Signature) -> Option { + match signature.output { + ReturnType::Default => { + emit_error!( + signature, + "shuttle_runtime::main functions need to return a service"; hint = "See the docs for services with first class support"; - doc = "https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services" - ), + doc = "https://docs.rs/shuttle-service/latest/shuttle_runtime/attr.main.html#shuttle-supported-services" + ); + None + } + ReturnType::Type(_, r#type) => match *r#type { + Type::Path(path) => Some(path), + _ => { + emit_error!( + r#type, + "shuttle_runtime::main functions need to return a first class service or 'Result"; + hint = "See the docs for services with first class support"; + doc = "https://docs.rs/shuttle-service/latest/shuttle_runtime/attr.main.html#shuttle-supported-services" + ); + None + } }, } } @@ -186,9 +183,12 @@ fn attribute_to_builder(pat_ident: &PatIdent, attrs: Vec) -> syn::Res Ok(builder) } -impl ToTokens for Wrapper { +impl ToTokens for Loader { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let fn_ident = &self.fn_ident; + + let return_type = &self.fn_return; + let mut fn_inputs: Vec<_> = Vec::with_capacity(self.fn_inputs.len()); let mut fn_inputs_builder: Vec<_> = Vec::with_capacity(self.fn_inputs.len()); let mut fn_inputs_builder_options: Vec<_> = Vec::with_capacity(self.fn_inputs.len()); @@ -210,7 +210,7 @@ impl ToTokens for Wrapper { lit: Lit::Str(str), .. }) => { needs_vars = true; - quote!(&shuttle_service::strfmt(#str, &vars)?) + quote!(&shuttle_runtime::strfmt(#str, &vars)?) } other => quote!(#other), }; @@ -232,7 +232,7 @@ impl ToTokens for Wrapper { None } else { Some(parse_quote!( - use shuttle_service::ResourceBuilder; + use shuttle_runtime::{Factory, ResourceBuilder}; )) }; @@ -244,68 +244,33 @@ impl ToTokens for Wrapper { None }; - let wrapper = quote! { - async fn __shuttle_wrapper( - #factory_ident: &mut dyn shuttle_service::Factory, - runtime: &shuttle_service::Runtime, - logger: shuttle_service::Logger, - ) -> Result, shuttle_service::Error> { - use shuttle_service::Context; - use shuttle_service::tracing_subscriber::prelude::*; + let loader = quote! { + async fn loader( + mut #factory_ident: shuttle_runtime::ProvisionerFactory, + logger: shuttle_runtime::Logger, + ) -> #return_type { + use shuttle_runtime::Context; + use shuttle_runtime::tracing_subscriber::prelude::*; #extra_imports - runtime.spawn_blocking(move || { - let filter_layer = - shuttle_service::tracing_subscriber::EnvFilter::try_from_default_env() - .or_else(|_| shuttle_service::tracing_subscriber::EnvFilter::try_new("INFO")) - .unwrap(); + let filter_layer = + shuttle_runtime::tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| shuttle_runtime::tracing_subscriber::EnvFilter::try_new("INFO")) + .unwrap(); - shuttle_service::tracing_subscriber::registry() - .with(filter_layer) - .with(logger) - .init(); // this sets the subscriber as the global default and also adds a compatibility layer for capturing `log::Record`s - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked setting logger".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to set logger")) - } - })?; + shuttle_runtime::tracing_subscriber::registry() + .with(filter_layer) + .with(logger) + .init(); #vars - #(let #fn_inputs = #fn_inputs_builder::new()#fn_inputs_builder_options.build(#factory_ident, runtime).await.context(format!("failed to provision {}", stringify!(#fn_inputs_builder)))?;)* + #(let #fn_inputs = #fn_inputs_builder::new()#fn_inputs_builder_options.build(&mut #factory_ident).await.context(format!("failed to provision {}", stringify!(#fn_inputs_builder)))?;)* - runtime.spawn(async { - #fn_ident(#(#fn_inputs),*) - .await - .map(|ok| Box::new(ok) as Box) - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked calling main".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to call main")) - } - })? + #fn_ident(#(#fn_inputs),*).await } }; - wrapper.to_tokens(tokens); + loader.to_tokens(tokens); } } @@ -315,7 +280,7 @@ mod tests { use quote::quote; use syn::{parse_quote, Ident}; - use super::{Builder, BuilderOptions, Input, Wrapper}; + use super::{Builder, BuilderOptions, Input, Loader}; #[test] fn from_with_return() { @@ -323,7 +288,7 @@ mod tests { async fn simple() -> ShuttleAxum {} ); - let actual = Wrapper::from_item_fn(&mut input); + let actual = Loader::from_item_fn(&mut input).unwrap(); let expected_ident: Ident = parse_quote!(simple); assert_eq!(actual.fn_ident, expected_ident); @@ -332,65 +297,32 @@ mod tests { #[test] fn output_with_return() { - let input = Wrapper { + let input = Loader { fn_ident: parse_quote!(simple), fn_inputs: Vec::new(), + fn_return: parse_quote!(ShuttleSimple), }; let actual = quote!(#input); let expected = quote! { - async fn __shuttle_wrapper( - _factory: &mut dyn shuttle_service::Factory, - runtime: &shuttle_service::Runtime, - logger: shuttle_service::Logger, - ) -> Result, shuttle_service::Error> { - use shuttle_service::Context; - use shuttle_service::tracing_subscriber::prelude::*; - runtime.spawn_blocking(move || { - let filter_layer = - shuttle_service::tracing_subscriber::EnvFilter::try_from_default_env() - .or_else(|_| shuttle_service::tracing_subscriber::EnvFilter::try_new("INFO")) - .unwrap(); - - shuttle_service::tracing_subscriber::registry() - .with(filter_layer) - .with(logger) - .init(); - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked setting logger".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to set logger")) - } - })?; - - runtime.spawn(async { - simple() - .await - .map(|ok| Box::new(ok) as Box) - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked calling main".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to call main")) - } - })? + async fn loader( + mut _factory: shuttle_runtime::ProvisionerFactory, + logger: shuttle_runtime::Logger, + ) -> ShuttleSimple { + use shuttle_runtime::Context; + use shuttle_runtime::tracing_subscriber::prelude::*; + + let filter_layer = + shuttle_runtime::tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| shuttle_runtime::tracing_subscriber::EnvFilter::try_new("INFO")) + .unwrap(); + + shuttle_runtime::tracing_subscriber::registry() + .with(filter_layer) + .with(logger) + .init(); + + simple().await } }; @@ -403,7 +335,7 @@ mod tests { async fn complex(#[shuttle_shared_db::Postgres] pool: PgPool) -> ShuttleTide {} ); - let actual = Wrapper::from_item_fn(&mut input); + let actual = Loader::from_item_fn(&mut input).unwrap(); let expected_ident: Ident = parse_quote!(complex); let expected_inputs: Vec = vec![Input { ident: parse_quote!(pool), @@ -430,7 +362,7 @@ mod tests { #[test] fn output_with_inputs() { - let input = Wrapper { + let input = Loader { fn_ident: parse_quote!(complex), fn_inputs: vec![ Input { @@ -448,67 +380,33 @@ mod tests { }, }, ], + fn_return: parse_quote!(ShuttleComplex), }; let actual = quote!(#input); let expected = quote! { - async fn __shuttle_wrapper( - factory: &mut dyn shuttle_service::Factory, - runtime: &shuttle_service::Runtime, - logger: shuttle_service::Logger, - ) -> Result, shuttle_service::Error> { - use shuttle_service::Context; - use shuttle_service::tracing_subscriber::prelude::*; - use shuttle_service::ResourceBuilder; - - runtime.spawn_blocking(move || { - let filter_layer = - shuttle_service::tracing_subscriber::EnvFilter::try_from_default_env() - .or_else(|_| shuttle_service::tracing_subscriber::EnvFilter::try_new("INFO")) - .unwrap(); - - shuttle_service::tracing_subscriber::registry() - .with(filter_layer) - .with(logger) - .init(); - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked setting logger".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to set logger")) - } - })?; - - let pool = shuttle_shared_db::Postgres::new().build(factory, runtime).await.context(format!("failed to provision {}", stringify!(shuttle_shared_db::Postgres)))?; - let redis = shuttle_shared_db::Redis::new().build(factory, runtime).await.context(format!("failed to provision {}", stringify!(shuttle_shared_db::Redis)))?; - - runtime.spawn(async { - complex(pool, redis) - .await - .map(|ok| Box::new(ok) as Box) - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked calling main".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to call main")) - } - })? + async fn loader( + mut factory: shuttle_runtime::ProvisionerFactory, + logger: shuttle_runtime::Logger, + ) -> ShuttleComplex { + use shuttle_runtime::Context; + use shuttle_runtime::tracing_subscriber::prelude::*; + use shuttle_runtime::{Factory, ResourceBuilder}; + + let filter_layer = + shuttle_runtime::tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| shuttle_runtime::tracing_subscriber::EnvFilter::try_new("INFO")) + .unwrap(); + + shuttle_runtime::tracing_subscriber::registry() + .with(filter_layer) + .with(logger) + .init(); + + let pool = shuttle_shared_db::Postgres::new().build(&mut factory).await.context(format!("failed to provision {}", stringify!(shuttle_shared_db::Postgres)))?; + let redis = shuttle_shared_db::Redis::new().build(&mut factory).await.context(format!("failed to provision {}", stringify!(shuttle_shared_db::Redis)))?; + + complex(pool, redis).await } }; @@ -550,7 +448,7 @@ mod tests { } ); - let actual = Wrapper::from_item_fn(&mut input); + let actual = Loader::from_item_fn(&mut input).unwrap(); let expected_ident: Ident = parse_quote!(complex); let mut expected_inputs: Vec = vec![Input { ident: parse_quote!(pool), @@ -577,7 +475,7 @@ mod tests { #[test] fn output_with_input_options() { - let mut input = Wrapper { + let mut input = Loader { fn_ident: parse_quote!(complex), fn_inputs: vec![Input { ident: parse_quote!(pool), @@ -586,6 +484,7 @@ mod tests { options: Default::default(), }, }], + fn_return: parse_quote!(ShuttleComplex), }; input.fn_inputs[0] @@ -601,63 +500,28 @@ mod tests { let actual = quote!(#input); let expected = quote! { - async fn __shuttle_wrapper( - factory: &mut dyn shuttle_service::Factory, - runtime: &shuttle_service::Runtime, - logger: shuttle_service::Logger, - ) -> Result, shuttle_service::Error> { - use shuttle_service::Context; - use shuttle_service::tracing_subscriber::prelude::*; - use shuttle_service::ResourceBuilder; - - runtime.spawn_blocking(move || { - let filter_layer = - shuttle_service::tracing_subscriber::EnvFilter::try_from_default_env() - .or_else(|_| shuttle_service::tracing_subscriber::EnvFilter::try_new("INFO")) - .unwrap(); - - shuttle_service::tracing_subscriber::registry() - .with(filter_layer) - .with(logger) - .init(); - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked setting logger".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to set logger")) - } - })?; + async fn loader( + mut factory: shuttle_runtime::ProvisionerFactory, + logger: shuttle_runtime::Logger, + ) -> ShuttleComplex { + use shuttle_runtime::Context; + use shuttle_runtime::tracing_subscriber::prelude::*; + use shuttle_runtime::{Factory, ResourceBuilder}; + + let filter_layer = + shuttle_runtime::tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| shuttle_runtime::tracing_subscriber::EnvFilter::try_new("INFO")) + .unwrap(); + + shuttle_runtime::tracing_subscriber::registry() + .with(filter_layer) + .with(logger) + .init(); let vars = std::collections::HashMap::from_iter(factory.get_secrets().await?.into_iter().map(|(key, value)| (format!("secrets.{}", key), value))); - let pool = shuttle_shared_db::Postgres::new().size(&shuttle_service::strfmt("10Gb", &vars)?).public(false).build(factory, runtime).await.context(format!("failed to provision {}", stringify!(shuttle_shared_db::Postgres)))?; + let pool = shuttle_shared_db::Postgres::new().size(&shuttle_runtime::strfmt("10Gb", &vars)?).public(false).build(&mut factory).await.context(format!("failed to provision {}", stringify!(shuttle_shared_db::Postgres)))?; - runtime.spawn(async { - complex(pool) - .await - .map(|ok| Box::new(ok) as Box) - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked calling main".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to call main")) - } - })? + complex(pool).await } }; @@ -667,6 +531,6 @@ mod tests { #[test] fn ui() { let t = trybuild::TestCases::new(); - t.compile_fail("tests/ui/*.rs"); + t.compile_fail("tests/ui/main/*.rs"); } } diff --git a/codegen/tests/ui/main/duplicate-main-fn.rs b/codegen/tests/ui/main/duplicate-main-fn.rs new file mode 100644 index 000000000..ed8e38c30 --- /dev/null +++ b/codegen/tests/ui/main/duplicate-main-fn.rs @@ -0,0 +1,2 @@ +#[shuttle_codegen::main] +async fn main() -> ShuttleRocket {} diff --git a/codegen/tests/ui/main/duplicate-main-fn.stderr b/codegen/tests/ui/main/duplicate-main-fn.stderr new file mode 100644 index 000000000..4b516b5c8 --- /dev/null +++ b/codegen/tests/ui/main/duplicate-main-fn.stderr @@ -0,0 +1,11 @@ +error: shuttle_runtime::main functions cannot be named `main` + --> tests/ui/main/duplicate-main-fn.rs:2:10 + | +2 | async fn main() -> ShuttleRocket {} + | ^^^^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/main/duplicate-main-fn.rs:2:36 + | +2 | async fn main() -> ShuttleRocket {} + | ^ consider adding a `main` function to `$DIR/tests/ui/main/duplicate-main-fn.rs` diff --git a/codegen/tests/ui/main/missing-return.stderr b/codegen/tests/ui/main/missing-return.stderr index e84ed9923..7ec24dffd 100644 --- a/codegen/tests/ui/main/missing-return.stderr +++ b/codegen/tests/ui/main/missing-return.stderr @@ -1,7 +1,7 @@ -error: shuttle_service::main functions need to return a service +error: shuttle_runtime::main functions need to return a service = help: See the docs for services with first class support - = note: https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services + = note: https://docs.rs/shuttle-service/latest/shuttle_runtime/attr.main.html#shuttle-supported-services --> tests/ui/main/missing-return.rs:2:1 | diff --git a/codegen/tests/ui/main/return-tuple.stderr b/codegen/tests/ui/main/return-tuple.stderr index b9fed7820..94da0bcb5 100644 --- a/codegen/tests/ui/main/return-tuple.stderr +++ b/codegen/tests/ui/main/return-tuple.stderr @@ -1,7 +1,7 @@ -error: shuttle_service::main functions need to return a first class service or 'Result +error: shuttle_runtime::main functions need to return a first class service or 'Result = help: See the docs for services with first class support - = note: https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services + = note: https://docs.rs/shuttle-service/latest/shuttle_runtime/attr.main.html#shuttle-supported-services --> tests/ui/main/return-tuple.rs:2:28 | diff --git a/codegen/tests/ui/next/duplicate-endpoint-param.rs b/codegen/tests/ui/next/duplicate-endpoint-param.rs new file mode 100644 index 000000000..7830c9ad3 --- /dev/null +++ b/codegen/tests/ui/next/duplicate-endpoint-param.rs @@ -0,0 +1,11 @@ +shuttle_codegen::app! { + #[shuttle_codegen::endpoint(method = get, method = get)] + async fn hello() -> &'static str { + "Hello, World!" + } + + #[shuttle_codegen::endpoint(route = "/goodbye", route = "/goodbye")] + async fn goodbye() -> &'static str { + "Goodbye, World!" + } +} diff --git a/codegen/tests/ui/next/duplicate-endpoint-param.stderr b/codegen/tests/ui/next/duplicate-endpoint-param.stderr new file mode 100644 index 000000000..25dd13f7e --- /dev/null +++ b/codegen/tests/ui/next/duplicate-endpoint-param.stderr @@ -0,0 +1,41 @@ +error: duplicate endpoint method + + = help: The endpoint `method` should only be set once. + + --> tests/ui/next/duplicate-endpoint-param.rs:2:47 + | +2 | #[shuttle_codegen::endpoint(method = get, method = get)] + | ^^^^^^ + +error: no route provided + + = help: Add a route to your endpoint: `route = "/hello")` + + --> tests/ui/next/duplicate-endpoint-param.rs:2:32 + | +2 | #[shuttle_codegen::endpoint(method = get, method = get)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: duplicate endpoint route + + = help: The endpoint `route` should only be set once. + + --> tests/ui/next/duplicate-endpoint-param.rs:7:53 + | +7 | #[shuttle_codegen::endpoint(route = "/goodbye", route = "/goodbye")] + | ^^^^^ + +error: no method provided + + = help: Add a method to your endpoint: `method = get` + + --> tests/ui/next/duplicate-endpoint-param.rs:7:32 + | +7 | #[shuttle_codegen::endpoint(route = "/goodbye", route = "/goodbye")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/next/duplicate-endpoint-param.rs:11:2 + | +11 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/next/duplicate-endpoint-param.rs` diff --git a/codegen/tests/ui/next/duplicate-methods.rs b/codegen/tests/ui/next/duplicate-methods.rs new file mode 100644 index 000000000..6c33ac070 --- /dev/null +++ b/codegen/tests/ui/next/duplicate-methods.rs @@ -0,0 +1,16 @@ +shuttle_codegen::app! { + #[shuttle_codegen::endpoint(method = get, route = "/hello")] + async fn hello() -> &'static str { + "Hello, World!" + } + + #[shuttle_codegen::endpoint(method = post, route = "/hello")] + async fn goodbye() -> &'static str { + "Goodbye, World!" + } + + #[shuttle_codegen::endpoint(method = post, route = "/hello")] + async fn goodbye() -> &'static str { + "Goodbye, World!" + } +} diff --git a/codegen/tests/ui/next/duplicate-methods.stderr b/codegen/tests/ui/next/duplicate-methods.stderr new file mode 100644 index 000000000..562a6b32c --- /dev/null +++ b/codegen/tests/ui/next/duplicate-methods.stderr @@ -0,0 +1,14 @@ +error: only one method of each type is allowed per route + + = help: Remove one of the post methods on the "/hello" route. + + --> tests/ui/next/duplicate-methods.rs:12:42 + | +12 | #[shuttle_codegen::endpoint(method = post, route = "/hello")] + | ^^^^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/next/duplicate-methods.rs:16:2 + | +16 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/next/duplicate-methods.rs` diff --git a/codegen/tests/ui/next/extra-endpoint-attributes.rs b/codegen/tests/ui/next/extra-endpoint-attributes.rs new file mode 100644 index 000000000..bafa5b92b --- /dev/null +++ b/codegen/tests/ui/next/extra-endpoint-attributes.rs @@ -0,0 +1,7 @@ +shuttle_codegen::app! { + #[shuttle_codegen::endpoint(method = get, route = "/hello")] + #[shuttle_codegen::endpoint(method = post, route = "/hello")] + async fn hello() -> &'static str { + "Hello, World!" + } +} diff --git a/codegen/tests/ui/next/extra-endpoint-attributes.stderr b/codegen/tests/ui/next/extra-endpoint-attributes.stderr new file mode 100644 index 000000000..4bcc17574 --- /dev/null +++ b/codegen/tests/ui/next/extra-endpoint-attributes.stderr @@ -0,0 +1,18 @@ +error: extra endpoint attribute + + = help: There should only be one endpoint annotation per handler function. + + --> tests/ui/next/extra-endpoint-attributes.rs:2:5 + | +2 | / #[shuttle_codegen::endpoint(method = get, route = "/hello")] +3 | | #[shuttle_codegen::endpoint(method = post, route = "/hello")] +4 | | async fn hello() -> &'static str { +5 | | "Hello, World!" +6 | | } + | |_____^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/next/extra-endpoint-attributes.rs:7:2 + | +7 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/next/extra-endpoint-attributes.rs` diff --git a/codegen/tests/ui/next/invalid-endpoint-param.rs b/codegen/tests/ui/next/invalid-endpoint-param.rs new file mode 100644 index 000000000..e77b39f99 --- /dev/null +++ b/codegen/tests/ui/next/invalid-endpoint-param.rs @@ -0,0 +1,6 @@ +shuttle_codegen::app! { + #[shuttle_codegen::endpoint(method = get, route = "/goodbye", invalid = bad)] + async fn goodbye() -> &'static str { + "Goodbye, World!" + } +} diff --git a/codegen/tests/ui/next/invalid-endpoint-param.stderr b/codegen/tests/ui/next/invalid-endpoint-param.stderr new file mode 100644 index 000000000..f52aa0858 --- /dev/null +++ b/codegen/tests/ui/next/invalid-endpoint-param.stderr @@ -0,0 +1,14 @@ +error: invalid endpoint argument + + = help: Only `method` and `route` are valid endpoint arguments. + + --> tests/ui/next/invalid-endpoint-param.rs:2:67 + | +2 | #[shuttle_codegen::endpoint(method = get, route = "/goodbye", invalid = bad)] + | ^^^^^^^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/next/invalid-endpoint-param.rs:6:2 + | +6 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/next/invalid-endpoint-param.rs` diff --git a/codegen/tests/ui/next/invalid-endpoint-syntax.rs b/codegen/tests/ui/next/invalid-endpoint-syntax.rs new file mode 100644 index 000000000..9bf7a2d8c --- /dev/null +++ b/codegen/tests/ui/next/invalid-endpoint-syntax.rs @@ -0,0 +1,11 @@ +shuttle_codegen::app! { + #[shuttle_codegen::endpoint(method = get, route = "/hello" extra = abundant)] + async fn hello() -> &'static str { + "Hello, World!" + } + + #[shuttle_codegen::endpoint(method = get, route = "/goodbye", invalid)] + async fn goodbye() -> &'static str { + "Goodbye, World!" + } +} diff --git a/codegen/tests/ui/next/invalid-endpoint-syntax.stderr b/codegen/tests/ui/next/invalid-endpoint-syntax.stderr new file mode 100644 index 000000000..b1fad595a --- /dev/null +++ b/codegen/tests/ui/next/invalid-endpoint-syntax.stderr @@ -0,0 +1,17 @@ +error: expected `,` + --> tests/ui/next/invalid-endpoint-syntax.rs:2:64 + | +2 | #[shuttle_codegen::endpoint(method = get, route = "/hello" extra = abundant)] + | ^^^^^ + +error: expected `=` + --> tests/ui/next/invalid-endpoint-syntax.rs:7:74 + | +7 | #[shuttle_codegen::endpoint(method = get, route = "/goodbye", invalid)] + | ^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/next/invalid-endpoint-syntax.rs:11:2 + | +11 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/next/invalid-endpoint-syntax.rs` diff --git a/codegen/tests/ui/next/invalid-method.rs b/codegen/tests/ui/next/invalid-method.rs new file mode 100644 index 000000000..73f29bb87 --- /dev/null +++ b/codegen/tests/ui/next/invalid-method.rs @@ -0,0 +1,11 @@ +shuttle_codegen::app! { + #[shuttle_codegen::endpoint(method = pet, route = "/hello")] + async fn hello() -> &'static str { + "Hello, World!" + } + + #[shuttle_codegen::endpoint(method =, route = "/hello")] + async fn hello() -> &'static str { + "Hello, World!" + } +} diff --git a/codegen/tests/ui/next/invalid-method.stderr b/codegen/tests/ui/next/invalid-method.stderr new file mode 100644 index 000000000..fcd7acacc --- /dev/null +++ b/codegen/tests/ui/next/invalid-method.stderr @@ -0,0 +1,29 @@ +error: method is not supported + + = help: Try one of the following: `get`, `post`, `delete`, `put`, `options`, `head`, `trace` or `patch` + + --> tests/ui/next/invalid-method.rs:2:42 + | +2 | #[shuttle_codegen::endpoint(method = pet, route = "/hello")] + | ^^^ + +error: no method provided + + = help: Add a method to your endpoint: `method = get` + + --> tests/ui/next/invalid-method.rs:2:32 + | +2 | #[shuttle_codegen::endpoint(method = pet, route = "/hello")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: expected expression + --> tests/ui/next/invalid-method.rs:7:41 + | +7 | #[shuttle_codegen::endpoint(method =, route = "/hello")] + | ^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/next/invalid-method.rs:11:2 + | +11 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/next/invalid-method.rs` diff --git a/codegen/tests/ui/next/missing-endpoint-param.rs b/codegen/tests/ui/next/missing-endpoint-param.rs new file mode 100644 index 000000000..a4fa91bf4 --- /dev/null +++ b/codegen/tests/ui/next/missing-endpoint-param.rs @@ -0,0 +1,16 @@ +shuttle_codegen::app! { + #[shuttle_codegen::endpoint(method = get)] + async fn only_method_param() -> &'static str { + "Hello, World!" + } + + #[shuttle_codegen::endpoint(route = "/goodbye")] + async fn only_route_param() -> &'static str { + "Goodbye, World!" + } + + #[shuttle_codegen::endpoint()] + async fn no_params() -> &'static str { + "Goodbye, World!" + } +} diff --git a/codegen/tests/ui/next/missing-endpoint-param.stderr b/codegen/tests/ui/next/missing-endpoint-param.stderr new file mode 100644 index 000000000..99150e32f --- /dev/null +++ b/codegen/tests/ui/next/missing-endpoint-param.stderr @@ -0,0 +1,32 @@ +error: no route provided + + = help: Add a route to your endpoint: `route = "/hello")` + + --> tests/ui/next/missing-endpoint-param.rs:2:32 + | +2 | #[shuttle_codegen::endpoint(method = get)] + | ^^^^^^^^^^^^^^ + +error: no method provided + + = help: Add a method to your endpoint: `method = get` + + --> tests/ui/next/missing-endpoint-param.rs:7:32 + | +7 | #[shuttle_codegen::endpoint(route = "/goodbye")] + | ^^^^^^^^^^^^^^^^^^^^ + +error: missing endpoint arguments + + = help: The endpoint takes two arguments: `endpoint(method = get, route = "/hello")` + + --> tests/ui/next/missing-endpoint-param.rs:12:32 + | +12 | #[shuttle_codegen::endpoint()] + | ^^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/next/missing-endpoint-param.rs:16:2 + | +16 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/next/missing-endpoint-param.rs` diff --git a/common/Cargo.toml b/common/Cargo.toml index b197ddb0f..6ed9b21c4 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-common" -version = "0.11.2" +version = "0.12.0" edition.workspace = true license.workspace = true repository.workspace = true @@ -12,45 +12,53 @@ anyhow = { workspace = true, optional = true } async-trait = { workspace = true , optional = true } axum = { workspace = true, optional = true } bytes = { version = "1.3.0", optional = true } -chrono = { workspace = true, features = ["serde"] } +chrono = { workspace = true } comfy-table = { version = "6.1.3", optional = true } crossterm = { version = "0.25.0", optional = true } -headers = { workspace = true } +headers = { workspace = true, optional = true } http = { workspace = true, optional = true } http-body = { version = "0.4.5", optional = true } +http-serde = { version = "1.1.2", optional = true } hyper = { workspace = true, optional = true } jsonwebtoken = { workspace = true, optional = true } -once_cell = { workspace = true } +once_cell = { workspace = true, optional = true } opentelemetry = { workspace = true, optional = true } opentelemetry-http = { workspace = true, optional = true } opentelemetry-otlp = { version = "0.11.0", optional = true } -pin-project = { workspace = true } +pin-project = { workspace = true, optional = true } +prost-types = { workspace = true, optional = true } reqwest = { version = "0.11.13", optional = true } -rustrict = "0.5.5" -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true, optional = true } -strum = { workspace = true } +rmp-serde = { version = "1.1.1", optional = true } +rustrict = { version = "0.5.5", optional = true } +serde = { workspace = true, features = ["derive", "std"] } +serde_json = { workspace = true } +strum = { workspace = true, features = ["derive"] } thiserror = { workspace = true, optional = true } tonic = { version = "0.8.3", optional = true } tower = { workspace = true, optional = true } tower-http = { workspace = true, optional = true } -tracing = { workspace = true } +tracing = { workspace = true, features = ["std"] } tracing-opentelemetry = { workspace = true, optional = true } tracing-subscriber = { workspace = true, optional = true } ttl_cache = { workspace = true, optional = true } -uuid = { workspace = true, features = ["v4", "serde"] } +uuid = { workspace = true, features = ["v4", "serde"], optional = true } [features] -backend = ["async-trait", "axum", "bytes", "http", "http-body", "hyper/client", "jsonwebtoken", "opentelemetry", "opentelemetry-http", "opentelemetry-otlp", "thiserror", "tower", "tower-http", "tracing-opentelemetry", "tracing-subscriber/env-filter", "ttl_cache"] -display = ["comfy-table", "crossterm"] -models = ["anyhow", "async-trait", "display", "http", "reqwest", "serde_json"] +backend = ["async-trait", "axum/matched-path", "claims", "hyper/client", "opentelemetry-otlp", "thiserror", "tower-http", "tracing-subscriber/env-filter", "tracing-subscriber/fmt", "ttl_cache"] +claims = ["bytes", "chrono/clock", "headers", "http", "http-body", "jsonwebtoken", "opentelemetry", "opentelemetry-http", "pin-project", "tower", "tracing", "tracing-opentelemetry"] +display = ["chrono/clock", "comfy-table", "crossterm"] +error = ["prost-types", "thiserror", "uuid"] +models = ["anyhow", "async-trait", "display", "http", "reqwest", "service"] +service = ["chrono/serde", "once_cell", "rustrict", "serde/derive", "uuid"] +tracing = [] +wasm = ["chrono/clock", "http-serde", "http", "rmp-serde", "tracing", "tracing-subscriber"] [dev-dependencies] axum = { workspace = true } base64 = "0.13.1" +cap-std = "1.0.2" hyper = { workspace = true } ring = { workspace = true } -serde_json = { workspace = true } tokio = { version = "1.22.0", features = ["macros", "rt-multi-thread"] } tower = { workspace = true, features = ["util"] } tracing-fluent-assertions = "0.3.0" diff --git a/common/src/backends/auth.rs b/common/src/backends/auth.rs index f36029211..f87bd3fed 100644 --- a/common/src/backends/auth.rs +++ b/common/src/backends/auth.rs @@ -1,13 +1,11 @@ -use std::{convert::Infallible, future::Future, ops::Add, pin::Pin, sync::Arc}; +use std::{convert::Infallible, future::Future, pin::Pin, sync::Arc}; use async_trait::async_trait; use bytes::Bytes; -use chrono::{Duration, Utc}; use headers::{authorization::Bearer, Authorization, HeaderMapExt}; use http::{Request, Response, StatusCode, Uri}; use http_body::combinators::UnsyncBoxBody; use hyper::{body, Body, Client}; -use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header as JwtHeader, Validation}; use opentelemetry::global; use opentelemetry_http::HeaderInjector; use serde::{Deserialize, Serialize}; @@ -16,14 +14,14 @@ use tower::{Layer, Service}; use tracing::{error, trace, Span}; use tracing_opentelemetry::OpenTelemetrySpanExt; +use crate::claims::{Claim, Scope}; + use super::{ cache::{CacheManagement, CacheManager}, - future::{ResponseFuture, StatusCodeFuture}, + future::StatusCodeFuture, headers::XShuttleAdminSecret, }; -pub const EXP_MINUTES: i64 = 5; -const ISS: &str = "shuttle"; const PUBLIC_KEY_CACHE_KEY: &str = "shuttle.public-key"; /// Layer to check the admin secret set by deployer is correct @@ -86,165 +84,13 @@ where } } -/// The scope of operations that can be performed on shuttle -/// Every scope defaults to read and will use a suffix for updating tasks -#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum Scope { - /// Read the details, such as status and address, of a deployment - Deployment, - - /// Push a new deployment - DeploymentPush, - - /// Read the logs of a deployment - Logs, - - /// Read the details of a service - Service, - - /// Create a new service - ServiceCreate, - - /// Read the status of a project - Project, - - /// Create a new project - ProjectCreate, - - /// Get the resources for a project - Resources, - - /// Provision new resources for a project or update existing ones - ResourcesWrite, - - /// List the secrets of a project - Secret, - - /// Add or update secrets of a project - SecretWrite, - - /// Get list of users - User, - - /// Add or update users - UserCreate, - - /// Create an ACME account - AcmeCreate, - - /// Create a custom domain, - CustomDomainCreate, - - /// Admin level scope to internals - Admin, -} - #[derive(Deserialize, Serialize)] /// Response used internally to pass around JWT token pub struct ConvertResponse { pub token: String, } -#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] -pub struct Claim { - /// Expiration time (as UTC timestamp). - pub exp: usize, - /// Issued at (as UTC timestamp). - iat: usize, - /// Issuer. - iss: String, - /// Not Before (as UTC timestamp). - nbf: usize, - /// Subject (whom token refers to). - pub sub: String, - /// Scopes this token can access - pub scopes: Vec, - /// The original token that was parsed - token: Option, -} - -impl Claim { - /// Create a new claim for a user with the given scopes - pub fn new(sub: String, scopes: Vec) -> Self { - let iat = Utc::now(); - let exp = iat.add(Duration::minutes(EXP_MINUTES)); - - Self { - exp: exp.timestamp() as usize, - iat: iat.timestamp() as usize, - iss: ISS.to_string(), - nbf: iat.timestamp() as usize, - sub, - scopes, - token: None, - } - } - - pub fn into_token(self, encoding_key: &EncodingKey) -> Result { - if let Some(token) = self.token { - Ok(token) - } else { - encode( - &JwtHeader::new(jsonwebtoken::Algorithm::EdDSA), - &self, - encoding_key, - ) - .map_err(|err| { - error!( - error = &err as &dyn std::error::Error, - "failed to convert claim to token" - ); - match err.kind() { - jsonwebtoken::errors::ErrorKind::Json(_) => StatusCode::INTERNAL_SERVER_ERROR, - jsonwebtoken::errors::ErrorKind::Crypto(_) => StatusCode::SERVICE_UNAVAILABLE, - _ => StatusCode::INTERNAL_SERVER_ERROR, - } - }) - } - } - - pub fn from_token(token: &str, public_key: &[u8]) -> Result { - let decoding_key = DecodingKey::from_ed_der(public_key); - let mut validation = Validation::new(jsonwebtoken::Algorithm::EdDSA); - validation.set_issuer(&[ISS]); - - trace!(token, "converting token to claim"); - let mut claim: Self = decode(token, &decoding_key, &validation) - .map_err(|err| { - error!( - error = &err as &dyn std::error::Error, - "failed to convert token to claim" - ); - match err.kind() { - jsonwebtoken::errors::ErrorKind::InvalidSignature - | jsonwebtoken::errors::ErrorKind::InvalidAlgorithmName - | jsonwebtoken::errors::ErrorKind::ExpiredSignature - | jsonwebtoken::errors::ErrorKind::InvalidIssuer - | jsonwebtoken::errors::ErrorKind::ImmatureSignature => { - StatusCode::UNAUTHORIZED - } - jsonwebtoken::errors::ErrorKind::InvalidToken - | jsonwebtoken::errors::ErrorKind::InvalidAlgorithm - | jsonwebtoken::errors::ErrorKind::Base64(_) - | jsonwebtoken::errors::ErrorKind::Json(_) - | jsonwebtoken::errors::ErrorKind::Utf8(_) => StatusCode::BAD_REQUEST, - jsonwebtoken::errors::ErrorKind::MissingAlgorithm => { - StatusCode::INTERNAL_SERVER_ERROR - } - jsonwebtoken::errors::ErrorKind::Crypto(_) => StatusCode::SERVICE_UNAVAILABLE, - _ => StatusCode::INTERNAL_SERVER_ERROR, - } - })? - .claims; - - claim.token = Some(token.to_string()); - - Ok(claim) - } -} - -/// Trait to get a public key asyncronously +/// Trait to get a public key asynchronously #[async_trait] pub trait PublicKeyFn: Send + Sync + Clone { type Error: std::error::Error + Send; @@ -439,53 +285,6 @@ where } } -/// This layer takes a claim on a request extension and uses it's internal token to set the Authorization Bearer -#[derive(Clone)] -pub struct ClaimLayer; - -impl Layer for ClaimLayer { - type Service = ClaimService; - - fn layer(&self, inner: S) -> Self::Service { - ClaimService { inner } - } -} - -#[derive(Clone)] -pub struct ClaimService { - inner: S, -} - -impl Service>> for ClaimService -where - S: Service>> + Send + 'static, - S::Future: Send + 'static, -{ - type Response = S::Response; - type Error = S::Error; - type Future = ResponseFuture; - - fn poll_ready( - &mut self, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, mut req: Request>) -> Self::Future { - if let Some(claim) = req.extensions().get::() { - if let Some(token) = claim.token.clone() { - req.headers_mut() - .typed_insert(Authorization::bearer(&token).expect("to set JWT token")); - } - } - - let future = self.inner.call(req); - - ResponseFuture(future) - } -} - /// Check that the required scopes are set on the [Claim] extension on a [Request] #[derive(Clone)] pub struct ScopedLayer { @@ -568,7 +367,9 @@ mod tests { use serde_json::json; use tower::{ServiceBuilder, ServiceExt}; - use super::{Claim, JwtAuthenticationLayer, Scope, ScopedLayer}; + use crate::claims::{Claim, Scope}; + + use super::{JwtAuthenticationLayer, ScopedLayer}; #[test] fn to_token_and_back() { diff --git a/common/src/backends/future.rs b/common/src/backends/future.rs index 5603fdaa0..e50bd41d4 100644 --- a/common/src/backends/future.rs +++ b/common/src/backends/future.rs @@ -8,23 +8,6 @@ use axum::response::Response; use http::StatusCode; use pin_project::pin_project; -// Future for layers that just return the inner response -#[pin_project] -pub struct ResponseFuture(#[pin] pub F); - -impl Future for ResponseFuture -where - F: Future>, -{ - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); - - this.0.poll(cx) - } -} - /// Future for layers that might return a different status code #[pin_project(project = StatusCodeProj)] pub enum StatusCodeFuture { diff --git a/common/src/backends/metrics.rs b/common/src/backends/metrics.rs index 9d7fc2b77..0ecc7ebec 100644 --- a/common/src/backends/metrics.rs +++ b/common/src/backends/metrics.rs @@ -54,6 +54,9 @@ impl TraceLayer { /// /// # Example /// ``` + /// use shuttle_common::{request_span, backends::metrics::TraceLayer}; + /// use tracing::field; + /// /// TraceLayer::new(|request| { /// request_span!( /// request, @@ -206,7 +209,7 @@ mod tests { use tower::ServiceExt; use tracing::field; use tracing_fluent_assertions::{AssertionRegistry, AssertionsLayer}; - use tracing_subscriber::{layer::SubscriberExt, Registry}; + use tracing_subscriber::layer::SubscriberExt; use super::{Metrics, TraceLayer}; @@ -221,9 +224,9 @@ mod tests { #[tokio::test] async fn trace_layer() { let assertion_registry = AssertionRegistry::default(); - let base_subscriber = Registry::default(); - let subscriber = base_subscriber.with(AssertionsLayer::new(&assertion_registry)); - tracing::subscriber::set_global_default(subscriber).unwrap(); + let subscriber = + tracing_subscriber::registry().with(AssertionsLayer::new(&assertion_registry)); + let _guard = tracing::subscriber::set_default(subscriber); // Put in own block to make sure assertion to not interfere with the next test { diff --git a/common/src/backends/tracing.rs b/common/src/backends/tracing.rs index 64a95cb8f..a3fa04eb7 100644 --- a/common/src/backends/tracing.rs +++ b/common/src/backends/tracing.rs @@ -11,7 +11,7 @@ use opentelemetry::{ sdk::{propagation::TraceContextPropagator, trace, Resource}, KeyValue, }; -use opentelemetry_http::{HeaderExtractor, HeaderInjector}; +use opentelemetry_http::HeaderExtractor; use opentelemetry_otlp::WithExportConfig; use pin_project::pin_project; use tower::{Layer, Service}; @@ -19,8 +19,6 @@ use tracing::{debug_span, instrument::Instrumented, Instrument, Span, Subscriber use tracing_opentelemetry::OpenTelemetrySpanExt; use tracing_subscriber::{fmt, prelude::*, registry::LookupSpan, EnvFilter}; -use super::future::ResponseFuture; - pub fn setup_tracing(subscriber: S, service_name: &str) where S: Subscriber + for<'a> LookupSpan<'a> + Send + Sync, @@ -139,49 +137,3 @@ where ExtractPropagationFuture { response_future } } } - -/// This layer adds the current tracing span to any outgoing request -#[derive(Clone)] -pub struct InjectPropagationLayer; - -impl Layer for InjectPropagationLayer { - type Service = InjectPropagation; - - fn layer(&self, inner: S) -> Self::Service { - InjectPropagation { inner } - } -} - -#[derive(Clone)] -pub struct InjectPropagation { - inner: S, -} - -impl Service> for InjectPropagation -where - S: Service> + Send + 'static, - S::Future: Send + 'static, -{ - type Response = S::Response; - type Error = S::Error; - type Future = ResponseFuture; - - fn poll_ready( - &mut self, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, mut req: Request) -> Self::Future { - let cx = Span::current().context(); - - global::get_text_map_propagator(|propagator| { - propagator.inject_context(&cx, &mut HeaderInjector(req.headers_mut())) - }); - - let future = self.inner.call(req); - - ResponseFuture(future) - } -} diff --git a/common/src/claims.rs b/common/src/claims.rs new file mode 100644 index 000000000..08d2c7806 --- /dev/null +++ b/common/src/claims.rs @@ -0,0 +1,291 @@ +use std::{ + future::Future, + ops::Add, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; +use chrono::{Duration, Utc}; +use headers::{Authorization, HeaderMapExt}; +use http::{Request, StatusCode}; +use http_body::combinators::UnsyncBoxBody; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use opentelemetry::global; +use opentelemetry_http::HeaderInjector; +use pin_project::pin_project; +use serde::{Deserialize, Serialize}; +use tower::{Layer, Service}; +use tracing::{error, trace, Span}; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +/// Minutes before a claim expires +/// +/// We don't use the convention of 5 minutes because builds can take longer than 5 minutes. When this happens, requests +/// to provisioner will fail as the token expired. +pub const EXP_MINUTES: i64 = 15; +const ISS: &str = "shuttle"; + +/// The scope of operations that can be performed on shuttle +/// Every scope defaults to read and will use a suffix for updating tasks +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum Scope { + /// Read the details, such as status and address, of a deployment + Deployment, + + /// Push a new deployment + DeploymentPush, + + /// Read the logs of a deployment + Logs, + + /// Read the details of a service + Service, + + /// Create a new service + ServiceCreate, + + /// Read the status of a project + Project, + + /// Create a new project + ProjectCreate, + + /// Get the resources for a project + Resources, + + /// Provision new resources for a project or update existing ones + ResourcesWrite, + + /// List the secrets of a project + Secret, + + /// Add or update secrets of a project + SecretWrite, + + /// Get list of users + User, + + /// Add or update users + UserCreate, + + /// Create an ACME account + AcmeCreate, + + /// Create a custom domain, + CustomDomainCreate, + + /// Admin level scope to internals + Admin, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +pub struct Claim { + /// Expiration time (as UTC timestamp). + pub exp: usize, + /// Issued at (as UTC timestamp). + iat: usize, + /// Issuer. + iss: String, + /// Not Before (as UTC timestamp). + nbf: usize, + /// Subject (whom token refers to). + pub sub: String, + /// Scopes this token can access + pub scopes: Vec, + /// The original token that was parsed + pub(crate) token: Option, +} + +impl Claim { + /// Create a new claim for a user with the given scopes + pub fn new(sub: String, scopes: Vec) -> Self { + let iat = Utc::now(); + let exp = iat.add(Duration::minutes(EXP_MINUTES)); + + Self { + exp: exp.timestamp() as usize, + iat: iat.timestamp() as usize, + iss: ISS.to_string(), + nbf: iat.timestamp() as usize, + sub, + scopes, + token: None, + } + } + + pub fn into_token(self, encoding_key: &EncodingKey) -> Result { + if let Some(token) = self.token { + Ok(token) + } else { + encode( + &Header::new(jsonwebtoken::Algorithm::EdDSA), + &self, + encoding_key, + ) + .map_err(|err| { + error!( + error = &err as &dyn std::error::Error, + "failed to convert claim to token" + ); + match err.kind() { + jsonwebtoken::errors::ErrorKind::Json(_) => StatusCode::INTERNAL_SERVER_ERROR, + jsonwebtoken::errors::ErrorKind::Crypto(_) => StatusCode::SERVICE_UNAVAILABLE, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + }) + } + } + + pub fn from_token(token: &str, public_key: &[u8]) -> Result { + let decoding_key = DecodingKey::from_ed_der(public_key); + let mut validation = Validation::new(jsonwebtoken::Algorithm::EdDSA); + validation.set_issuer(&[ISS]); + + trace!(token, "converting token to claim"); + let mut claim: Self = decode(token, &decoding_key, &validation) + .map_err(|err| { + error!( + error = &err as &dyn std::error::Error, + "failed to convert token to claim" + ); + match err.kind() { + jsonwebtoken::errors::ErrorKind::ExpiredSignature => { + StatusCode::from_u16(499).unwrap() // Expired status code which is safe to unwrap + } + jsonwebtoken::errors::ErrorKind::InvalidSignature + | jsonwebtoken::errors::ErrorKind::InvalidAlgorithmName + | jsonwebtoken::errors::ErrorKind::InvalidIssuer + | jsonwebtoken::errors::ErrorKind::ImmatureSignature => { + StatusCode::UNAUTHORIZED + } + jsonwebtoken::errors::ErrorKind::InvalidToken + | jsonwebtoken::errors::ErrorKind::InvalidAlgorithm + | jsonwebtoken::errors::ErrorKind::Base64(_) + | jsonwebtoken::errors::ErrorKind::Json(_) + | jsonwebtoken::errors::ErrorKind::Utf8(_) => StatusCode::BAD_REQUEST, + jsonwebtoken::errors::ErrorKind::MissingAlgorithm => { + StatusCode::INTERNAL_SERVER_ERROR + } + jsonwebtoken::errors::ErrorKind::Crypto(_) => StatusCode::SERVICE_UNAVAILABLE, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + })? + .claims; + + claim.token = Some(token.to_string()); + + Ok(claim) + } +} + +// Future for layers that just return the inner response +#[pin_project] +pub struct ResponseFuture(#[pin] pub F); + +impl Future for ResponseFuture +where + F: Future>, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + + this.0.poll(cx) + } +} + +/// This layer takes a claim on a request extension and uses it's internal token to set the Authorization Bearer +#[derive(Clone)] +pub struct ClaimLayer; + +impl Layer for ClaimLayer { + type Service = ClaimService; + + fn layer(&self, inner: S) -> Self::Service { + ClaimService { inner } + } +} + +#[derive(Clone)] +pub struct ClaimService { + inner: S, +} + +impl Service>> for ClaimService +where + S: Service>> + Send + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = ResponseFuture; + + fn poll_ready( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, mut req: Request>) -> Self::Future { + if let Some(claim) = req.extensions().get::() { + if let Some(token) = claim.token.clone() { + req.headers_mut() + .typed_insert(Authorization::bearer(&token).expect("to set JWT token")); + } + } + + let future = self.inner.call(req); + + ResponseFuture(future) + } +} + +/// This layer adds the current tracing span to any outgoing request +#[derive(Clone)] +pub struct InjectPropagationLayer; + +impl Layer for InjectPropagationLayer { + type Service = InjectPropagation; + + fn layer(&self, inner: S) -> Self::Service { + InjectPropagation { inner } + } +} + +#[derive(Clone)] +pub struct InjectPropagation { + inner: S, +} + +impl Service> for InjectPropagation +where + S: Service> + Send + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = ResponseFuture; + + fn poll_ready( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + let cx = Span::current().context(); + + global::get_text_map_propagator(|propagator| { + propagator.inject_context(&cx, &mut HeaderInjector(req.headers_mut())) + }); + + let future = self.inner.call(req); + + ResponseFuture(future) + } +} diff --git a/common/src/database.rs b/common/src/database.rs index c1617b47d..1d3956ea0 100644 --- a/common/src/database.rs +++ b/common/src/database.rs @@ -3,14 +3,14 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; use strum::Display; -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Type { AwsRds(AwsRdsEngine), Shared(SharedEngine), } -#[derive(Clone, Debug, Deserialize, Display, Serialize)] +#[derive(Clone, Debug, Deserialize, Display, Serialize, Eq, PartialEq)] #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] pub enum AwsRdsEngine { @@ -19,7 +19,7 @@ pub enum AwsRdsEngine { MariaDB, } -#[derive(Clone, Debug, Deserialize, Display, Serialize)] +#[derive(Clone, Debug, Deserialize, Display, Serialize, Eq, PartialEq)] #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] pub enum SharedEngine { diff --git a/common/src/deployment.rs b/common/src/deployment.rs index 326503a1d..9b924709e 100644 --- a/common/src/deployment.rs +++ b/common/src/deployment.rs @@ -17,6 +17,7 @@ pub enum State { } /// This which environment is this deployment taking place +#[derive(Clone, Copy)] pub enum Environment { Local, Production, diff --git a/common/src/lib.rs b/common/src/lib.rs index 5f7be0326..9d8162e9b 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,16 +1,31 @@ #[cfg(feature = "backend")] pub mod backends; +#[cfg(feature = "claims")] +pub mod claims; pub mod database; +#[cfg(feature = "service")] pub mod deployment; +#[cfg(feature = "service")] pub mod log; #[cfg(feature = "models")] pub mod models; +#[cfg(feature = "service")] pub mod project; +pub mod resource; +#[cfg(feature = "service")] +pub mod storage_manager; +#[cfg(feature = "tracing")] +pub mod tracing; +#[cfg(feature = "wasm")] +pub mod wasm; use serde::{Deserialize, Serialize}; +#[cfg(feature = "service")] use uuid::Uuid; +#[cfg(feature = "service")] pub use log::Item as LogItem; +#[cfg(feature = "service")] pub use log::STATE_MESSAGE; #[cfg(debug_assertions)] @@ -22,8 +37,20 @@ pub const API_URL_DEFAULT: &str = "https://api.shuttle.rs"; pub type ApiKey = String; pub type ApiUrl = String; pub type Host = String; +#[cfg(feature = "service")] pub type DeploymentId = Uuid; -pub type Port = u16; + +#[cfg(feature = "error")] +/// Errors that can occur when changing types. Especially from prost +#[derive(thiserror::Error, Debug)] +pub enum ParseError { + #[error("failed to parse UUID: {0}")] + Uuid(#[from] uuid::Error), + #[error("failed to parse timestamp: {0}")] + Timestamp(#[from] prost_types::TimestampError), + #[error("failed to parse serde: {0}")] + Serde(#[from] serde_json::Error), +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DatabaseReadyInfo { diff --git a/common/src/models/error.rs b/common/src/models/error.rs index 7b20d5571..05ba44669 100644 --- a/common/src/models/error.rs +++ b/common/src/models/error.rs @@ -115,7 +115,7 @@ impl From for ApiError { fn from(code: StatusCode) -> Self { let message = match code { StatusCode::OK | StatusCode::ACCEPTED | StatusCode::FOUND | StatusCode::SWITCHING_PROTOCOLS => { - unreachable!("we should not have an API error with a successfull status code") + unreachable!("we should not have an API error with a successful status code") } StatusCode::FORBIDDEN => "this request is not allowed", StatusCode::UNAUTHORIZED => { @@ -137,7 +137,7 @@ impl From for ApiError { }, _ => { error!(%code, "got an unexpected status code"); - "an unexpected error occured. Please create a ticket to report this" + "an unexpected error occurred. Please create a ticket to report this" }, }; diff --git a/common/src/models/mod.rs b/common/src/models/mod.rs index 0236c641d..463e04192 100644 --- a/common/src/models/mod.rs +++ b/common/src/models/mod.rs @@ -1,7 +1,6 @@ pub mod deployment; pub mod error; pub mod project; -pub mod resource; pub mod secret; pub mod service; pub mod stats; @@ -13,6 +12,7 @@ use http::StatusCode; use serde::de::DeserializeOwned; use tracing::trace; +/// A to_json wrapper for handling our error states #[async_trait] pub trait ToJson { async fn to_json(self) -> Result; @@ -33,7 +33,7 @@ impl ToJson for reqwest::Response { status_code, StatusCode::OK | StatusCode::SWITCHING_PROTOCOLS ) { - serde_json::from_slice(&full).context("failed to parse a successfull response") + serde_json::from_slice(&full).context("failed to parse a successful response") } else { trace!("parsing response to common error"); let res: error::ApiError = match serde_json::from_slice(&full) { diff --git a/common/src/models/project.rs b/common/src/models/project.rs index eac73b113..70f14e137 100644 --- a/common/src/models/project.rs +++ b/common/src/models/project.rs @@ -135,7 +135,7 @@ impl Display for State { State::Destroyed => write!(f, "{}", "destroyed".blue()), State::Errored { message } => { writeln!(f, "{}", "errored".red())?; - write!(f, "\tmessage: {}", message) + write!(f, "\tmessage: {message}") } } } @@ -199,9 +199,8 @@ pub fn get_table(projects: &Vec) -> String { format!( r#" These projects are linked to this account -{} +{table} "#, - table, ) } } diff --git a/common/src/models/secret.rs b/common/src/models/secret.rs index 2f9cddf6d..a89dfa1d0 100644 --- a/common/src/models/secret.rs +++ b/common/src/models/secret.rs @@ -38,9 +38,8 @@ pub fn get_table(secrets: &Vec) -> String { format!( r#"These secrets are linked to this service -{} +{table} "#, - table ) } } diff --git a/common/src/models/service.rs b/common/src/models/service.rs index 5537567b4..68d569050 100644 --- a/common/src/models/service.rs +++ b/common/src/models/service.rs @@ -1,6 +1,6 @@ use crate::{ - models::{deployment, resource, resource::ResourceInfo, secret}, - DatabaseReadyInfo, + models::{deployment, secret}, + resource::{self, ResourceInfo}, }; use comfy_table::{ @@ -34,19 +34,13 @@ pub struct Summary { pub uri: String, } -impl ResourceInfo for DatabaseReadyInfo { - fn connection_string_public(&self) -> String { - self.connection_string_public() - } -} - impl Display for Detailed { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let deploys = get_deployments_table(&self.deployments, &self.name); let resources = get_resources_table(&self.resources); let secrets = secret::get_table(&self.secrets); - write!(f, "{}{}{}", deploys, resources, secrets) + write!(f, "{deploys}{resources}{secrets}") } } @@ -82,7 +76,7 @@ URI: {} let resources = get_resources_table(&self.resources); - write!(f, "{}{}", deployment, resources) + write!(f, "{deployment}{resources}") } } @@ -150,9 +144,8 @@ fn get_resources_table(resources: &Vec) -> String { format!( r#"These resources are linked to this service -{} +{table} "#, - table ) } } diff --git a/common/src/project.rs b/common/src/project.rs index d4816c50b..5846de6f8 100644 --- a/common/src/project.rs +++ b/common/src/project.rs @@ -99,13 +99,12 @@ impl Display for ProjectNameError { ProjectNameError::InvalidName(name) => write!( f, r#" -`{}` is an invalid project name. project name must +`{name}` is an invalid project name. project name must 1. start and end with alphanumeric characters. 2. only contain characters inside of the alphanumeric range, except for `-`, or `_`. 3. not be empty., 4. not contain profanity. 5. not be a reserved word."#, - name ), } } diff --git a/common/src/models/resource.rs b/common/src/resource.rs similarity index 57% rename from common/src/models/resource.rs rename to common/src/resource.rs index 3b97bfb75..976020729 100644 --- a/common/src/models/resource.rs +++ b/common/src/resource.rs @@ -2,13 +2,11 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; use serde_json::Value; -use uuid::Uuid; use crate::{database, DatabaseReadyInfo}; -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] pub struct Response { - pub service_id: Uuid, pub r#type: Type, pub data: Value, } @@ -17,9 +15,22 @@ pub struct Response { pub trait ResourceInfo { /// String to connect to this resource from a public location fn connection_string_public(&self) -> String; + + /// String to connect to this resource from within shuttle + fn connection_string_private(&self) -> String; +} + +impl ResourceInfo for DatabaseReadyInfo { + fn connection_string_public(&self) -> String { + self.connection_string_public() + } + + fn connection_string_private(&self) -> String { + self.connection_string_private() + } } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Type { Database(database::Type), @@ -33,6 +44,14 @@ impl Response { } } } + + pub fn into_bytes(self) -> Vec { + serde_json::to_vec(&self).expect("to turn resource into a vec") + } + + pub fn from_bytes(bytes: Vec) -> Self { + serde_json::from_slice(&bytes).expect("to turn bytes into a resource") + } } impl Display for Type { diff --git a/common/src/storage_manager.rs b/common/src/storage_manager.rs new file mode 100644 index 000000000..51a0fe892 --- /dev/null +++ b/common/src/storage_manager.rs @@ -0,0 +1,92 @@ +use std::{fs, io, path::PathBuf}; + +use uuid::Uuid; + +pub trait StorageManager: Sync + Send { + /// Path for a specific service build files + fn service_build_path(&self, service_name: &str) -> Result; + + /// Path to folder for storing service files + fn service_storage_path(&self, service_name: &str) -> Result; +} + +/// Manager to take care of directories for storing project, services and deployment files for deployer +#[derive(Clone)] +pub struct ArtifactsStorageManager { + artifacts_path: PathBuf, +} + +impl ArtifactsStorageManager { + pub fn new(artifacts_path: PathBuf) -> Self { + Self { artifacts_path } + } + + /// Path of the directory that contains extracted service Cargo projects. + pub fn builds_path(&self) -> Result { + let builds_path = self.artifacts_path.join("shuttle-builds"); + fs::create_dir_all(&builds_path)?; + + Ok(builds_path) + } + + /// The directory in which compiled executables are stored. + pub fn executables_path(&self) -> Result { + let executables_path = self.artifacts_path.join("shuttle-executables"); + fs::create_dir_all(&executables_path)?; + + Ok(executables_path) + } + + /// Path to executable for a service + pub fn deployment_executable_path(&self, deployment_id: &Uuid) -> Result { + let executable_path = self.executables_path()?.join(deployment_id.to_string()); + + Ok(executable_path) + } + + /// Path of the directory to store user files + pub fn storage_path(&self) -> Result { + let storage_path = self.artifacts_path.join("shuttle-storage"); + fs::create_dir_all(&storage_path)?; + + Ok(storage_path) + } +} + +impl StorageManager for ArtifactsStorageManager { + fn service_build_path(&self, service_name: &str) -> Result { + let builds_path = self.builds_path()?.join(service_name); + fs::create_dir_all(&builds_path)?; + + Ok(builds_path) + } + + fn service_storage_path(&self, service_name: &str) -> Result { + let storage_path = self.storage_path()?.join(service_name); + fs::create_dir_all(&storage_path)?; + + Ok(storage_path) + } +} + +/// Manager to take care of directories for storing project, services and deployment files for the local runner +#[derive(Clone)] +pub struct WorkingDirStorageManager { + working_dir: PathBuf, +} + +impl WorkingDirStorageManager { + pub fn new(working_dir: PathBuf) -> Self { + Self { working_dir } + } +} + +impl StorageManager for WorkingDirStorageManager { + fn service_build_path(&self, _service_name: &str) -> Result { + Ok(self.working_dir.clone()) + } + + fn service_storage_path(&self, _service_name: &str) -> Result { + Ok(self.working_dir.clone()) + } +} diff --git a/common/src/tracing.rs b/common/src/tracing.rs new file mode 100644 index 000000000..2532262fe --- /dev/null +++ b/common/src/tracing.rs @@ -0,0 +1,53 @@ +use serde_json::json; +use tracing::field::Visit; + +// Boilerplate for extracting the fields from the event +#[derive(Default)] +pub struct JsonVisitor { + pub fields: serde_json::Map, + pub target: Option, + pub file: Option, + pub line: Option, +} + +impl JsonVisitor { + /// Ignores log metadata as it is included in the other LogItem fields (target, file, line...) + fn filter_insert(&mut self, field: &tracing::field::Field, value: serde_json::Value) { + match field.name() { + "log.line" => self.line = value.as_u64().map(|u| u as u32), + "log.target" => self.target = value.as_str().map(ToOwned::to_owned), + "log.file" => self.file = value.as_str().map(ToOwned::to_owned), + "log.module_path" => {} + name => { + self.fields.insert(name.to_string(), json!(value)); + } + } + } +} +impl Visit for JsonVisitor { + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + self.filter_insert(field, json!(value)); + } + fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { + self.filter_insert(field, json!(value)); + } + fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { + self.filter_insert(field, json!(value)); + } + fn record_i64(&mut self, field: &tracing::field::Field, value: i64) { + self.filter_insert(field, json!(value)); + } + fn record_f64(&mut self, field: &tracing::field::Field, value: f64) { + self.filter_insert(field, json!(value)); + } + fn record_error( + &mut self, + field: &tracing::field::Field, + value: &(dyn std::error::Error + 'static), + ) { + self.filter_insert(field, json!(value.to_string())); + } + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + self.filter_insert(field, json!(format!("{value:?}"))); + } +} diff --git a/common/src/wasm.rs b/common/src/wasm.rs new file mode 100644 index 000000000..1508644dd --- /dev/null +++ b/common/src/wasm.rs @@ -0,0 +1,520 @@ +use std::{ + io::Write, + slice::IterMut, + sync::{Arc, Mutex}, +}; + +use chrono::{DateTime, NaiveDateTime, Utc}; +use http::{HeaderMap, Method, Request, Response, StatusCode, Uri, Version}; +use rmps::Serializer; +use serde::{Deserialize, Serialize}; +use tracing::Subscriber; +use tracing_subscriber::Layer; + +use crate::tracing::JsonVisitor; + +extern crate rmp_serde as rmps; + +#[derive(Serialize, Deserialize, Debug)] +pub struct RequestWrapper { + #[serde(with = "http_serde::method")] + pub method: Method, + + #[serde(with = "http_serde::uri")] + pub uri: Uri, + + #[serde(with = "http_serde::version")] + pub version: Version, + + #[serde(with = "http_serde::header_map")] + pub headers: HeaderMap, +} + +impl From for RequestWrapper { + fn from(parts: http::request::Parts) -> Self { + RequestWrapper { + method: parts.method, + uri: parts.uri, + version: parts.version, + headers: parts.headers, + } + } +} + +impl RequestWrapper { + /// Serialize a RequestWrapper to the Rust MessagePack data format + pub fn into_rmp(self) -> Result, rmps::encode::Error> { + let mut buf = Vec::new(); + self.serialize(&mut Serializer::new(&mut buf))?; + + Ok(buf) + } + + /// Consume the wrapper and return a request builder with `Parts` set + pub fn into_request_builder(self) -> http::request::Builder { + let mut request = Request::builder() + .method(self.method) + .version(self.version) + .uri(self.uri); + + request + .headers_mut() + .unwrap() // Safe to unwrap as we just made the builder + .extend(self.headers.into_iter()); + + request + } +} + +// todo: add http extensions field +#[derive(Serialize, Deserialize, Debug)] +pub struct ResponseWrapper { + #[serde(with = "http_serde::status_code")] + pub status: StatusCode, + + #[serde(with = "http_serde::version")] + pub version: Version, + + #[serde(with = "http_serde::header_map")] + pub headers: HeaderMap, +} + +impl From for ResponseWrapper { + fn from(parts: http::response::Parts) -> Self { + ResponseWrapper { + status: parts.status, + version: parts.version, + headers: parts.headers, + } + } +} + +impl ResponseWrapper { + /// Serialize a ResponseWrapper into the Rust MessagePack data format + pub fn into_rmp(self) -> Result, rmps::encode::Error> { + let mut buf = Vec::new(); + self.serialize(&mut Serializer::new(&mut buf))?; + + Ok(buf) + } + + /// Consume the wrapper and return a response builder with `Parts` set + pub fn into_response_builder(self) -> http::response::Builder { + let mut response = Response::builder() + .status(self.status) + .version(self.version); + + response + .headers_mut() + .unwrap() // Safe to unwrap since we just made the builder + .extend(self.headers.into_iter()); + + response + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Log { + pub level: Level, + pub timestamp: DateTime, + pub file: String, + pub line: u32, + pub target: String, + pub fields: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Level { + Trace, + Debug, + Info, + Warn, + Error, +} + +impl From<&tracing::Level> for Level { + fn from(level: &tracing::Level) -> Self { + match *level { + tracing::Level::TRACE => Self::Trace, + tracing::Level::DEBUG => Self::Debug, + tracing::Level::INFO => Self::Info, + tracing::Level::WARN => Self::Warn, + tracing::Level::ERROR => Self::Error, + } + } +} + +impl Log { + pub fn into_bytes(self) -> Vec { + let mut buf = Vec::new(); + + buf.add(self); + + buf + } +} + +/// Like [slice::fill_with] but allows unwrapping of [Option]s +trait TryFillWith: Sized { + fn try_fill_with>(self, iter: &mut I) -> Option<()>; +} + +impl<'a> TryFillWith for IterMut<'a, u8> { + fn try_fill_with>(self, iter: &mut I) -> Option<()> { + for el in self { + *el = iter.next()?; + } + + Some(()) + } +} + +/// Convert a structure to and from bytes (array of u8) +pub trait Bytesable: Sized { + /// Add self to bytes vec + fn append_bytes(self, buf: &mut Vec); + + /// Get self from bytes vec + fn from_bytes>(iter: &mut I) -> Option; +} + +macro_rules! impl_bytesable { + ($($int:ident),*) => { + $(impl Bytesable for $int { + fn append_bytes(self, buf: &mut Vec) { + buf.extend_from_slice(&self.to_le_bytes()); + } + + fn from_bytes>(iter: &mut I) -> Option { + let mut buf = [0; $int::BITS as usize / 8]; + buf.iter_mut().try_fill_with(iter)?; + + Some($int::from_le_bytes(buf)) + } + })* + }; +} + +// Never implement for usize because it could map to u64 in the runtime and a u32 inside wasm +// This will cause the deserialization step to fail +impl_bytesable!(u32, u64, i64); + +impl Bytesable for String { + fn append_bytes(self, buf: &mut Vec) { + buf.add(self.len() as u64); + buf.extend_from_slice(self.as_bytes()); + } + + fn from_bytes>(iter: &mut I) -> Option { + let length: u64 = iter.get()?; + + let mut vec = vec![0; length as usize]; + vec.iter_mut().try_fill_with(iter)?; + + String::from_utf8(vec).ok() + } +} + +impl Bytesable for Level { + fn append_bytes(self, buf: &mut Vec) { + buf.add(self as u32); + } + + fn from_bytes>(iter: &mut I) -> Option { + let i: u32 = iter.get()?; + + let res = match i { + 0 => Self::Trace, + 1 => Self::Debug, + 2 => Self::Info, + 3 => Self::Warn, + 4 => Self::Error, + _ => Self::Trace, + }; + + Some(res) + } +} + +impl Bytesable for DateTime { + fn append_bytes(self, buf: &mut Vec) { + buf.add(self.naive_utc().timestamp_millis()); + } + + fn from_bytes>(iter: &mut I) -> Option { + let millis: i64 = iter.get()?; + + let datetime = NaiveDateTime::from_timestamp_millis(millis)?; + + Some(Self::from_utc(datetime, Utc)) + } +} + +impl Bytesable for Vec { + fn append_bytes(self, buf: &mut Vec) { + buf.add(self.len() as u64); + buf.extend_from_slice(&self); + } + + fn from_bytes>(iter: &mut I) -> Option { + let length: u64 = iter.get()?; + + let mut vec = vec![0; length as usize]; + vec.iter_mut().try_fill_with(iter)?; + + Some(vec) + } +} + +impl Bytesable for Log { + fn append_bytes(self, buf: &mut Vec) { + buf.add(self.level); + buf.add(self.timestamp); + buf.add(self.file); + buf.add(self.line); + buf.add(self.target); + buf.add(self.fields); + } + + // These should be in the same order as they appear in [Self::append_bytes] + fn from_bytes>(iter: &mut I) -> Option { + Some(Self { + level: iter.get()?, + timestamp: iter.get()?, + file: iter.get()?, + line: iter.get()?, + target: iter.get()?, + fields: iter.get()?, + }) + } +} + +/// Trait to make it easier to add a bytable type to a data source +trait BytesableAppendExt { + fn add(&mut self, i: B); +} + +impl BytesableAppendExt for Vec { + fn add(&mut self, i: B) { + i.append_bytes(self); + } +} + +/// Trait to make it easier to get a bytable type from a data source +trait BytesableFromExt { + fn get(&mut self) -> Option; +} + +impl> BytesableFromExt for I { + fn get(&mut self) -> Option { + B::from_bytes(self) + } +} + +pub struct Logger { + writer: Arc>, +} + +impl Logger { + pub fn new(writer: W) -> Self { + Self { + writer: Arc::new(Mutex::new(writer)), + } + } +} + +impl Layer for Logger +where + S: Subscriber, + W: Write + 'static, +{ + fn on_event( + &self, + event: &tracing::Event<'_>, + _ctx: tracing_subscriber::layer::Context<'_, S>, + ) { + let datetime = Utc::now(); + + let item = { + let metadata = event.metadata(); + let mut visitor = JsonVisitor::default(); + + event.record(&mut visitor); + + Log { + level: metadata.level().into(), + timestamp: datetime, + file: visitor + .file + .or_else(|| metadata.file().map(str::to_string)) + .unwrap_or_default(), + line: visitor.line.or_else(|| metadata.line()).unwrap_or_default(), + target: visitor + .target + .unwrap_or_else(|| metadata.target().to_string()), + fields: serde_json::to_vec(&visitor.fields).unwrap(), + } + }; + + let _ = self + .writer + .lock() + .expect("to get lock on writer") + .write(&item.into_bytes()) + .expect("sending log should succeed"); + } +} + +#[cfg(test)] +mod tests { + use cap_std::os::unix::net::UnixStream; + use serde_json::json; + use std::io::{Read, Write}; + + use super::*; + use chrono::SubsecRound; + use http::HeaderValue; + use hyper::body::Body; + use tracing_subscriber::prelude::*; + + #[test] + fn request_roundtrip() { + let request: Request = Request::builder() + .method(Method::PUT) + .version(Version::HTTP_11) + .header("test", HeaderValue::from_static("request")) + .uri("https://axum-wasm.example/hello") + .body(Body::empty()) + .unwrap(); + + let (parts, _) = request.into_parts(); + let rmp = RequestWrapper::from(parts).into_rmp().unwrap(); + + let back: RequestWrapper = rmps::from_slice(&rmp).unwrap(); + + assert_eq!( + back.headers.get("test").unwrap(), + HeaderValue::from_static("request") + ); + assert_eq!(back.method, Method::PUT); + assert_eq!(back.version, Version::HTTP_11); + assert_eq!( + back.uri.to_string(), + "https://axum-wasm.example/hello".to_string() + ); + } + + #[test] + fn response_roundtrip() { + let response: Response = Response::builder() + .version(Version::HTTP_11) + .header("test", HeaderValue::from_static("response")) + .status(StatusCode::NOT_MODIFIED) + .body(Body::empty()) + .unwrap(); + + let (parts, _) = response.into_parts(); + let rmp = ResponseWrapper::from(parts).into_rmp().unwrap(); + + let back: ResponseWrapper = rmps::from_slice(&rmp).unwrap(); + + assert_eq!( + back.headers.get("test").unwrap(), + HeaderValue::from_static("response") + ); + assert_eq!(back.status, StatusCode::NOT_MODIFIED); + assert_eq!(back.version, Version::HTTP_11); + } + + #[test] + fn log_roundtrip() { + let log = Log { + level: Level::Debug, + timestamp: Utc::now().trunc_subsecs(3), + file: "main.rs".to_string(), + line: 5, + target: "crate::main".to_string(), + fields: serde_json::to_vec(&json!({"message": "Hello"})).unwrap(), + }; + + let mut buf = Vec::new(); + buf.add(log.clone()); + let mut iter = buf.into_iter(); + + let actual = iter.get::(); + + assert_eq!(log, actual.unwrap()); + assert_eq!(iter.next(), None); + } + + #[test] + fn logs_over_socket() { + let (mut tx, rx) = UnixStream::pair().unwrap(); + let log1 = Log { + level: Level::Debug, + timestamp: Utc::now().trunc_subsecs(3), + file: "lib.rs".to_string(), + line: 9, + target: "crate::lib".to_string(), + fields: serde_json::to_vec(&json!({"message": "starting"})).unwrap(), + }; + let log2 = Log { + level: Level::Debug, + timestamp: Utc::now().trunc_subsecs(3), + file: Default::default(), + line: Default::default(), + target: Default::default(), + fields: Default::default(), + }; + + let _ = tx.write(&log1.clone().into_bytes()).unwrap(); + let _ = tx.write(&log2.clone().into_bytes()).unwrap(); + + let mut rx = rx.bytes().filter_map(Result::ok); + + let actual = rx.get::().unwrap(); + assert_eq!(log1, actual); + + let actual = rx.get::().unwrap(); + assert_eq!(log2, actual); + + // Make sure the closed channel (end) is handled correctly + drop(tx); + assert_eq!(rx.get::(), None); + } + + #[test] + fn logging() { + let (tx, rx) = UnixStream::pair().unwrap(); + let mut rx = rx.bytes().filter_map(Result::ok); + + let logger = Logger::new(tx); + let to_tuple = |log: Option| { + let log = log.unwrap(); + let fields: serde_json::Map = + serde_json::from_slice(&log.fields).unwrap(); + + let message = fields["message"].as_str().unwrap().to_owned(); + + (message, log.level) + }; + + let _guard = tracing_subscriber::registry().with(logger).set_default(); + + tracing::debug!("this is"); + tracing::info!("hi"); + tracing::warn!("from"); + tracing::error!("logger"); + + assert_eq!( + to_tuple(rx.get::()), + ("this is".to_string(), Level::Debug) + ); + assert_eq!(to_tuple(rx.get::()), ("hi".to_string(), Level::Info)); + assert_eq!(to_tuple(rx.get::()), ("from".to_string(), Level::Warn)); + assert_eq!( + to_tuple(rx.get::()), + ("logger".to_string(), Level::Error) + ); + } +} diff --git a/deployer/Cargo.toml b/deployer/Cargo.toml index de6dfdc74..daa0eb193 100644 --- a/deployer/Cargo.toml +++ b/deployer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-deployer" -version = "0.11.1" +version = "0.12.0" edition.workspace = true license.workspace = true description = "Service with instances created per project for handling the compilation, loading, and execution of Shuttle services" @@ -8,7 +8,7 @@ description = "Service with instances created per project for handling the compi [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } -axum = { workspace = true, features = ["headers", "ws"] } +axum = { workspace = true, features = ["headers", "json", "query", "ws"] } bytes = "1.3.0" # TODO: debug the libgit2-sys conflict with cargo-edit when upgrading cargo to 0.66 cargo = "0.65.0" @@ -19,6 +19,7 @@ crossbeam-channel = "0.5.6" flate2 = "1.0.25" fqdn = "0.2.3" futures = "0.3.25" +home = "0.5.4" hyper = { workspace = true, features = ["client", "http1", "http2", "tcp"] } # not great, but waiting for WebSocket changes to be merged hyper-reverse-proxy = { git = "https://github.com/chesedo/hyper-reverse-proxy", branch = "master" } @@ -40,13 +41,14 @@ sqlx = { version = "0.6.2", features = [ strum = { workspace = true } tar = "0.4.38" thiserror = { workspace = true } -tokio = { version = "1.22.0", features = ["fs"] } +tokio = { version = "1.22.0", features = ["fs", "process"] } toml = "0.5.9" -tonic = "0.8.3" +tonic = { workspace = true } tower = { workspace = true, features = ["make"] } -tracing = { workspace = true } +tower-http = { version = "0.3.4", features = ["auth", "trace"] } +tracing = { workspace = true, features = ["default"] } tracing-opentelemetry = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing-subscriber = { workspace = true, features = ["default", "env-filter", "fmt"] } uuid = { workspace = true, features = ["v4"] } [dependencies.shuttle-common] @@ -58,7 +60,7 @@ workspace = true [dependencies.shuttle-service] workspace = true -features = ["loader"] +features = ["builder"] [dev-dependencies] ctor = "0.1.26" diff --git a/deployer/migrations/0001_next.sql b/deployer/migrations/0001_next.sql new file mode 100644 index 000000000..42b89c217 --- /dev/null +++ b/deployer/migrations/0001_next.sql @@ -0,0 +1 @@ +ALTER TABLE deployments ADD COLUMN is_next BOOLEAN DEFAULT 0 NOT NULL; diff --git a/deployer/prepare.sh b/deployer/prepare.sh index 38a848028..a7725531d 100755 --- a/deployer/prepare.sh +++ b/deployer/prepare.sh @@ -9,11 +9,32 @@ mkdir -p $CARGO_HOME; \ echo '[patch.crates-io] shuttle-service = { path = "/usr/src/shuttle/service" } +shuttle-runtime = { path = "/usr/src/shuttle/runtime" } + shuttle-aws-rds = { path = "/usr/src/shuttle/resources/aws-rds" } shuttle-persist = { path = "/usr/src/shuttle/resources/persist" } shuttle-shared-db = { path = "/usr/src/shuttle/resources/shared-db" } shuttle-secrets = { path = "/usr/src/shuttle/resources/secrets" } -shuttle-static-folder = { path = "/usr/src/shuttle/resources/static-folder" }' > $CARGO_HOME/config.toml +shuttle-static-folder = { path = "/usr/src/shuttle/resources/static-folder" } + +shuttle-axum = { path = "/usr/src/shuttle/services/shuttle-axum" } +shuttle-actix-web = { path = "/usr/src/shuttle/services/shuttle-actix-web" } +shuttle-next = { path = "/usr/src/shuttle/services/shuttle-next" } +shuttle-poem = { path = "/usr/src/shuttle/services/shuttle-poem" } +shuttle-poise = { path = "/usr/src/shuttle/services/shuttle-poise" } +shuttle-rocket = { path = "/usr/src/shuttle/services/shuttle-rocket" } +shuttle-salvo = { path = "/usr/src/shuttle/services/shuttle-salvo" } +shuttle-serenity = { path = "/usr/src/shuttle/services/shuttle-serenity" } +shuttle-thruster = { path = "/usr/src/shuttle/services/shuttle-thruster" } +shuttle-tide = { path = "/usr/src/shuttle/services/shuttle-tide" } +shuttle-tower = { path = "/usr/src/shuttle/services/shuttle-tower" } +shuttle-warp = { path = "/usr/src/shuttle/services/shuttle-warp" }' > $CARGO_HOME/config.toml + +# Add the wasm32-wasi target +rustup target add wasm32-wasi + +# Install the shuttle runtime +cargo install shuttle-runtime --path "/usr/src/shuttle/runtime" --bin shuttle-next --features next while getopts "p," o; do case $o in diff --git a/deployer/src/args.rs b/deployer/src/args.rs index 8f185761d..73211faa8 100644 --- a/deployer/src/args.rs +++ b/deployer/src/args.rs @@ -3,7 +3,8 @@ use std::{net::SocketAddr, path::PathBuf}; use clap::Parser; use fqdn::FQDN; use hyper::Uri; -use shuttle_common::{project::ProjectName, Port}; +use shuttle_common::project::ProjectName; +use tonic::transport::Endpoint; /// Program to handle the deploys for a single project /// Handling includes, building, testing, and running each service @@ -15,12 +16,8 @@ pub struct Args { pub state: String, /// Address to connect to the provisioning service - #[clap(long)] - pub provisioner_address: String, - - /// Port provisioner is running on - #[clap(long, default_value = "5000")] - pub provisioner_port: Port, + #[clap(long, default_value = "http://provisioner:5000")] + pub provisioner_address: Endpoint, /// FQDN where the proxy can be reached at #[clap(long)] diff --git a/deployer/src/deployment/deploy_layer.rs b/deployer/src/deployment/deploy_layer.rs index c49040768..f38823a13 100644 --- a/deployer/src/deployment/deploy_layer.rs +++ b/deployer/src/deployment/deploy_layer.rs @@ -21,9 +21,10 @@ use chrono::{DateTime, Utc}; use serde_json::json; -use shuttle_common::STATE_MESSAGE; -use std::{net::SocketAddr, str::FromStr}; -use tracing::{error, field::Visit, span, warn, Metadata, Subscriber}; +use shuttle_common::{tracing::JsonVisitor, ParseError, STATE_MESSAGE}; +use shuttle_proto::runtime; +use std::{convert::TryFrom, str::FromStr, time::SystemTime}; +use tracing::{field::Visit, span, warn, Metadata, Subscriber}; use tracing_subscriber::Layer; use uuid::Uuid; @@ -62,8 +63,6 @@ pub struct Log { pub fields: serde_json::Value, pub r#type: LogType, - - pub address: Option, } impl From for persistence::Log { @@ -105,23 +104,42 @@ impl From for shuttle_common::LogItem { impl From for DeploymentState { fn from(log: Log) -> Self { - let address = if let Some(address_str) = log.address { - match SocketAddr::from_str(&address_str) { - Ok(address) => Some(address), - Err(err) => { - error!(error = %err, "failed to convert to [SocketAddr]"); - None - } - } - } else { - None - }; - Self { id: log.id, state: log.state, last_update: log.timestamp, - address, + } + } +} + +impl TryFrom for Log { + type Error = ParseError; + + fn try_from(log: runtime::LogItem) -> Result { + Ok(Self { + id: Default::default(), + state: State::Running, + level: runtime::LogLevel::from_i32(log.level) + .unwrap_or_default() + .into(), + timestamp: DateTime::from(SystemTime::try_from(log.timestamp.unwrap_or_default())?), + file: log.file, + line: log.line, + target: log.target, + fields: serde_json::from_slice(&log.fields)?, + r#type: LogType::Event, + }) + } +} + +impl From for LogLevel { + fn from(level: runtime::LogLevel) -> Self { + match level { + runtime::LogLevel::Trace => Self::Trace, + runtime::LogLevel::Debug => Self::Debug, + runtime::LogLevel::Info => Self::Info, + runtime::LogLevel::Warn => Self::Warn, + runtime::LogLevel::Error => Self::Error, } } } @@ -172,38 +190,18 @@ where event.record(&mut visitor); let metadata = event.metadata(); - // Extract details from log::Log interface which is different from tracing - let target = if let Some(target) = visitor.0.remove("log.target") { - target.as_str().unwrap_or_default().to_string() - } else { - metadata.target().to_string() - }; - - let line = if let Some(line) = visitor.0.remove("log.line") { - line.as_u64().and_then(|u| u32::try_from(u).ok()) - } else { - metadata.line() - }; - - let file = if let Some(file) = visitor.0.remove("log.file") { - file.as_str().map(str::to_string) - } else { - metadata.file().map(str::to_string) - }; - - visitor.0.remove("log.module_path"); - self.recorder.record(Log { id: details.id, state: details.state, level: metadata.level().into(), timestamp: Utc::now(), - file, - line, - target, - fields: serde_json::Value::Object(visitor.0), + file: visitor.file.or_else(|| metadata.file().map(str::to_string)), + line: visitor.line.or_else(|| metadata.line()), + target: visitor + .target + .unwrap_or_else(|| metadata.target().to_string()), + fields: serde_json::Value::Object(visitor.fields), r#type: LogType::Event, - address: None, }); break; } @@ -247,7 +245,6 @@ where target: metadata.target().to_string(), fields: Default::default(), r#type: LogType::State, - address: details.address.clone(), }); extensions.insert::(details); @@ -259,7 +256,6 @@ where struct ScopeDetails { id: Uuid, state: State, - address: Option, } impl From<&tracing::Level> for LogLevel { @@ -287,9 +283,6 @@ impl NewStateVisitor { /// Field containing the deployment state identifier const STATE_IDENT: &'static str = "state"; - /// Field containing the deployment address identifier - const ADDRESS_IDENT: &'static str = "address"; - fn is_valid(metadata: &Metadata) -> bool { metadata.is_span() && metadata.fields().field(Self::ID_IDENT).is_some() @@ -303,71 +296,45 @@ impl Visit for NewStateVisitor { self.details.state = State::from_str(&format!("{value:?}")).unwrap_or_default(); } else if field.name() == Self::ID_IDENT { self.details.id = Uuid::try_parse(&format!("{value:?}")).unwrap_or_default(); - } else if field.name() == Self::ADDRESS_IDENT { - self.details.address = Some(format!("{value:?}")); } } } -#[derive(Default)] -struct JsonVisitor(serde_json::Map); - -impl Visit for JsonVisitor { - fn record_str(&mut self, field: &tracing::field::Field, value: &str) { - self.0.insert(field.name().to_string(), json!(value)); - } - fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { - self.0.insert(field.name().to_string(), json!(value)); - } - fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { - self.0.insert(field.name().to_string(), json!(value)); - } - fn record_i64(&mut self, field: &tracing::field::Field, value: i64) { - self.0.insert(field.name().to_string(), json!(value)); - } - fn record_f64(&mut self, field: &tracing::field::Field, value: f64) { - self.0.insert(field.name().to_string(), json!(value)); - } - fn record_error( - &mut self, - field: &tracing::field::Field, - value: &(dyn std::error::Error + 'static), - ) { - self.0 - .insert(field.name().to_string(), json!(value.to_string())); - } - fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { - self.0 - .insert(field.name().to_string(), json!(format!("{value:?}"))); - } -} - #[cfg(test)] mod tests { use std::{ - collections::BTreeMap, fs::read_dir, + net::{Ipv4Addr, SocketAddr}, path::PathBuf, sync::{Arc, Mutex}, time::Duration, }; + use crate::{ + persistence::{DeploymentUpdater, Resource, ResourceManager}, + RuntimeManager, + }; + use async_trait::async_trait; use axum::body::Bytes; use ctor::ctor; use flate2::{write::GzEncoder, Compression}; - use shuttle_common::backends::auth::Claim; - use shuttle_service::Logger; - use tokio::{select, sync::mpsc, time::sleep}; - use tracing_subscriber::prelude::*; + use portpicker::pick_unused_port; + use shuttle_proto::provisioner::{ + provisioner_server::{Provisioner, ProvisionerServer}, + DatabaseDeletionResponse, DatabaseRequest, DatabaseResponse, + }; + use tempfile::Builder; + use tokio::{select, time::sleep}; + use tonic::transport::Server; + use tracing_subscriber::{fmt, prelude::*, EnvFilter}; use uuid::Uuid; use crate::{ deployment::{ - deploy_layer::LogType, gateway_client::BuildQueueClient, provisioner_factory, - runtime_logger, storage_manager::StorageManager, ActiveDeploymentsGetter, Built, - DeploymentManager, Queued, + deploy_layer::LogType, gateway_client::BuildQueueClient, ActiveDeploymentsGetter, + Built, DeploymentManager, Queued, }, - persistence::{SecretRecorder, State}, + persistence::{Secret, SecretGetter, SecretRecorder, State}, }; use super::{DeployLayer, Log, LogRecorder}; @@ -375,8 +342,46 @@ mod tests { #[ctor] static RECORDER: Arc> = { let recorder = RecorderMock::new(); + + // Copied from the test-log crate + let event_filter = { + use ::tracing_subscriber::fmt::format::FmtSpan; + + match ::std::env::var("RUST_LOG_SPAN_EVENTS") { + Ok(value) => { + value + .to_ascii_lowercase() + .split(',') + .map(|filter| match filter.trim() { + "new" => FmtSpan::NEW, + "enter" => FmtSpan::ENTER, + "exit" => FmtSpan::EXIT, + "close" => FmtSpan::CLOSE, + "active" => FmtSpan::ACTIVE, + "full" => FmtSpan::FULL, + _ => panic!("test-log: RUST_LOG_SPAN_EVENTS must contain filters separated by `,`.\n\t\ + For example: `active` or `new,close`\n\t\ + Supported filters: new, enter, exit, close, active, full\n\t\ + Got: {}", value), + }) + .fold(FmtSpan::NONE, |acc, filter| filter | acc) + }, + Err(::std::env::VarError::NotUnicode(_)) => + panic!("test-log: RUST_LOG_SPAN_EVENTS must contain a valid UTF-8 string"), + Err(::std::env::VarError::NotPresent) => FmtSpan::NONE, + } + }; + let fmt_layer = fmt::layer() + .with_test_writer() + .with_span_events(event_filter); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("shuttle_deployer")) + .unwrap(); + tracing_subscriber::registry() .with(DeployLayer::new(Arc::clone(&recorder))) + .with(filter_layer) + .with(fmt_layer) .init(); recorder @@ -391,7 +396,6 @@ mod tests { struct StateLog { id: Uuid, state: State, - has_address: bool, } impl From for StateLog { @@ -399,7 +403,6 @@ mod tests { Self { id: log.id, state: log.state, - has_address: log.address.is_some(), } } } @@ -431,6 +434,45 @@ mod tests { } } + struct ProvisionerMock; + + #[async_trait] + impl Provisioner for ProvisionerMock { + async fn provision_database( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + panic!("no deploy layer tests should request a db"); + } + + async fn delete_database( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + panic!("no deploy layer tests should request delete a db"); + } + } + + fn get_runtime_manager() -> Arc> { + let provisioner_addr = + SocketAddr::new(Ipv4Addr::LOCALHOST.into(), pick_unused_port().unwrap()); + let mock = ProvisionerMock; + + tokio::spawn(async move { + Server::builder() + .add_service(ProvisionerServer::new(mock)) + .serve(provisioner_addr) + .await + .unwrap(); + }); + + let tmp_dir = Builder::new().prefix("shuttle_run_test").tempdir().unwrap(); + let path = tmp_dir.into_path(); + let (tx, _rx) = crossbeam_channel::unbounded(); + + RuntimeManager::new(path, format!("http://{}", provisioner_addr), None, tx) + } + #[async_trait::async_trait] impl SecretRecorder for Arc> { type Err = std::io::Error; @@ -451,72 +493,19 @@ mod tests { } } - struct StubAbstractProvisionerFactory; - - #[async_trait::async_trait] - impl provisioner_factory::AbstractFactory for StubAbstractProvisionerFactory { - type Output = StubProvisionerFactory; - type Error = std::io::Error; - - async fn get_factory( - &self, - _project_name: shuttle_common::project::ProjectName, - _service_id: Uuid, - _deployment_id: Uuid, - _storage_manager: StorageManager, - _claim: Option, - ) -> Result { - Ok(StubProvisionerFactory) - } - } - - struct StubProvisionerFactory; + #[derive(Clone)] + struct StubDeploymentUpdater; #[async_trait::async_trait] - impl shuttle_service::Factory for StubProvisionerFactory { - async fn get_db_connection_string( - &mut self, - _db_type: shuttle_common::database::Type, - ) -> Result { - panic!("did not expect any deploy_layer test to connect to the database") - } - - async fn get_secrets( - &mut self, - ) -> Result, shuttle_service::Error> { - panic!("did not expect any deploy_layer test to get secrets") - } - - fn get_service_name(&self) -> shuttle_service::ServiceName { - panic!("did not expect any deploy_layer test to get the service name") - } - - fn get_environment(&self) -> shuttle_service::Environment { - panic!("did not expect any deploy_layer test to get the environment") - } - - fn get_build_path(&self) -> Result { - panic!("did not expect any deploy_layer test to get the build path") - } + impl DeploymentUpdater for StubDeploymentUpdater { + type Err = std::io::Error; - fn get_storage_path(&self) -> Result { - panic!("did not expect any deploy_layer test to get the storage path") + async fn set_address(&self, _id: &Uuid, _address: &SocketAddr) -> Result<(), Self::Err> { + Ok(()) } - } - - struct StubRuntimeLoggerFactory; - impl runtime_logger::Factory for StubRuntimeLoggerFactory { - fn get_logger(&self, id: Uuid) -> Logger { - let (tx, mut rx) = mpsc::unbounded_channel(); - - tokio::spawn(async move { - while let Some(log) = rx.recv().await { - println!("{log}") - } - }); - - Logger::new(tx, id) + async fn set_is_next(&self, _id: &Uuid, _is_next: bool) -> Result<(), Self::Err> { + Ok(()) } } @@ -555,80 +544,93 @@ mod tests { } } - #[tokio::test(flavor = "multi_thread")] - async fn deployment_to_be_queued() { - let deployment_manager = get_deployment_manager(); + #[derive(Clone)] + struct StubSecretGetter; - let queued = get_queue("sleep-async"); - let id = queued.id; - deployment_manager.queue_push(queued).await; + #[async_trait::async_trait] + impl SecretGetter for StubSecretGetter { + type Err = std::io::Error; - let test = async { - loop { - let recorder = RECORDER.lock().unwrap(); - let states = recorder.get_deployment_states(&id); + async fn get_secrets(&self, _service_id: &Uuid) -> Result, Self::Err> { + Ok(Default::default()) + } + } - if states.len() < 5 { - drop(recorder); // Don't block - sleep(Duration::from_millis(350)).await; - continue; - } + #[derive(Clone)] + struct StubResourceManager; - assert_eq!( - states.len(), - 5, - "did not expect these states:\n\t{states:#?}" - ); - - assert_eq!( - *states, - vec![ - StateLog { - id, - state: State::Queued, - has_address: false, - }, - StateLog { - id, - state: State::Building, - has_address: false, - }, - StateLog { - id, - state: State::Built, - has_address: false, - }, - StateLog { - id, - state: State::Loading, - has_address: true, - }, - StateLog { - id, - state: State::Running, - has_address: true, - }, - ] - ); + #[async_trait::async_trait] + impl ResourceManager for StubResourceManager { + type Err = std::io::Error; + + async fn insert_resource(&self, _resource: &Resource) -> Result<(), Self::Err> { + Ok(()) + } + async fn get_resources(&self, _service_id: &Uuid) -> Result, Self::Err> { + Ok(Vec::new()) + } + } + + async fn test_states(id: &Uuid, expected_states: Vec) { + loop { + let states = RECORDER.lock().unwrap().get_deployment_states(id); + if *states == expected_states { break; } - }; + + sleep(Duration::from_millis(250)).await; + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn deployment_to_be_queued() { + let deployment_manager = get_deployment_manager().await; + + let queued = get_queue("sleep-async"); + let id = queued.id; + deployment_manager.queue_push(queued).await; + + let test = test_states( + &id, + vec![ + StateLog { + id, + state: State::Queued, + }, + StateLog { + id, + state: State::Building, + }, + StateLog { + id, + state: State::Built, + }, + StateLog { + id, + state: State::Loading, + }, + StateLog { + id, + state: State::Running, + }, + ], + ); select! { - _ = sleep(Duration::from_secs(180)) => { - panic!("states should go into 'Running' for a valid service"); - } + _ = sleep(Duration::from_secs(360)) => { + let states = RECORDER.lock().unwrap().get_deployment_states(&id); + panic!("states should go into 'Running' for a valid service: {:#?}", states); + }, _ = test => {} - } + }; // Send kill signal deployment_manager.kill(id).await; sleep(Duration::from_secs(1)).await; - let recorder = RECORDER.lock().unwrap(); - let states = recorder.get_deployment_states(&id); + let states = RECORDER.lock().unwrap().get_deployment_states(&id); assert_eq!( *states, @@ -636,32 +638,26 @@ mod tests { StateLog { id, state: State::Queued, - has_address: false, }, StateLog { id, state: State::Building, - has_address: false, }, StateLog { id, state: State::Built, - has_address: false, }, StateLog { id, state: State::Loading, - has_address: true, }, StateLog { id, state: State::Running, - has_address: true, }, StateLog { id, state: State::Stopped, - has_address: false, }, ] ); @@ -669,72 +665,46 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn deployment_self_stop() { - let deployment_manager = get_deployment_manager(); + let deployment_manager = get_deployment_manager().await; let queued = get_queue("self-stop"); let id = queued.id; deployment_manager.queue_push(queued).await; - let test = async { - loop { - let recorder = RECORDER.lock().unwrap(); - let states = recorder.get_deployment_states(&id); - - if states.len() < 6 { - drop(recorder); // Don't block - sleep(Duration::from_millis(350)).await; - continue; - } - - assert_eq!( - states.len(), - 6, - "did not expect these states:\n\t{states:#?}" - ); - - assert_eq!( - *states, - vec![ - StateLog { - id, - state: State::Queued, - has_address: false, - }, - StateLog { - id, - state: State::Building, - has_address: false, - }, - StateLog { - id, - state: State::Built, - has_address: false, - }, - StateLog { - id, - state: State::Loading, - has_address: true, - }, - StateLog { - id, - state: State::Running, - has_address: true, - }, - StateLog { - id, - state: State::Completed, - has_address: false, - }, - ] - ); - - break; - } - }; + let test = test_states( + &id, + vec![ + StateLog { + id, + state: State::Queued, + }, + StateLog { + id, + state: State::Building, + }, + StateLog { + id, + state: State::Built, + }, + StateLog { + id, + state: State::Loading, + }, + StateLog { + id, + state: State::Running, + }, + StateLog { + id, + state: State::Completed, + }, + ], + ); select! { - _ = sleep(Duration::from_secs(180)) => { - panic!("states should go into 'Completed' when a service stops by itself"); + _ = sleep(Duration::from_secs(360)) => { + let states = RECORDER.lock().unwrap().get_deployment_states(&id); + panic!("states should go into 'Completed' when a service stops by itself: {:#?}", states); } _ = test => {} } @@ -742,72 +712,46 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn deployment_bind_panic() { - let deployment_manager = get_deployment_manager(); + let deployment_manager = get_deployment_manager().await; let queued = get_queue("bind-panic"); let id = queued.id; deployment_manager.queue_push(queued).await; - let test = async { - loop { - let recorder = RECORDER.lock().unwrap(); - let states = recorder.get_deployment_states(&id); - - if states.len() < 6 { - drop(recorder); // Don't block - sleep(Duration::from_millis(350)).await; - continue; - } - - assert_eq!( - states.len(), - 6, - "did not expect these states:\n\t{states:#?}" - ); - - assert_eq!( - *states, - vec![ - StateLog { - id, - state: State::Queued, - has_address: false, - }, - StateLog { - id, - state: State::Building, - has_address: false, - }, - StateLog { - id, - state: State::Built, - has_address: false, - }, - StateLog { - id, - state: State::Loading, - has_address: true, - }, - StateLog { - id, - state: State::Running, - has_address: true, - }, - StateLog { - id, - state: State::Crashed, - has_address: false, - }, - ] - ); - - break; - } - }; + let test = test_states( + &id, + vec![ + StateLog { + id, + state: State::Queued, + }, + StateLog { + id, + state: State::Building, + }, + StateLog { + id, + state: State::Built, + }, + StateLog { + id, + state: State::Loading, + }, + StateLog { + id, + state: State::Running, + }, + StateLog { + id, + state: State::Crashed, + }, + ], + ); select! { - _ = sleep(Duration::from_secs(180)) => { - panic!("states should go into 'Crashed' panicing in bind"); + _ = sleep(Duration::from_secs(360)) => { + let states = RECORDER.lock().unwrap().get_deployment_states(&id); + panic!("states should go into 'Crashed' panicking in bind: {:#?}", states); } _ = test => {} } @@ -815,67 +759,42 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn deployment_main_panic() { - let deployment_manager = get_deployment_manager(); + let deployment_manager = get_deployment_manager().await; let queued = get_queue("main-panic"); let id = queued.id; deployment_manager.queue_push(queued).await; - let test = async { - loop { - let recorder = RECORDER.lock().unwrap(); - let states = recorder.get_deployment_states(&id); - - if states.len() < 5 { - drop(recorder); // Don't block - sleep(Duration::from_millis(350)).await; - continue; - } - - assert_eq!( - states.len(), - 5, - "did not expect these states:\n\t{states:#?}" - ); - - assert_eq!( - *states, - vec![ - StateLog { - id, - state: State::Queued, - has_address: false, - }, - StateLog { - id, - state: State::Building, - has_address: false, - }, - StateLog { - id, - state: State::Built, - has_address: false, - }, - StateLog { - id, - state: State::Loading, - has_address: true, - }, - StateLog { - id, - state: State::Crashed, - has_address: false, - }, - ] - ); - - break; - } - }; + let test = test_states( + &id, + vec![ + StateLog { + id, + state: State::Queued, + }, + StateLog { + id, + state: State::Building, + }, + StateLog { + id, + state: State::Built, + }, + StateLog { + id, + state: State::Loading, + }, + StateLog { + id, + state: State::Crashed, + }, + ], + ); select! { - _ = sleep(Duration::from_secs(180)) => { - panic!("states should go into 'Crashed' when panicing in main"); + _ = sleep(Duration::from_secs(360)) => { + let states = RECORDER.lock().unwrap().get_deployment_states(&id); + panic!("states should go into 'Crashed' when panicking in main: {:#?}", states); } _ = test => {} } @@ -883,7 +802,7 @@ mod tests { #[tokio::test] async fn deployment_from_run() { - let deployment_manager = get_deployment_manager(); + let deployment_manager = get_deployment_manager().await; let id = Uuid::new_v4(); deployment_manager @@ -892,47 +811,41 @@ mod tests { service_name: "run-test".to_string(), service_id: Uuid::new_v4(), tracing_context: Default::default(), + is_next: false, claim: None, }) .await; - // Give it a small time to start up - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - - let recorder = RECORDER.lock().unwrap(); - let states = recorder.get_deployment_states(&id); - - assert_eq!( - states.len(), - 3, - "did not expect these states:\n\t{states:#?}" - ); - - assert_eq!( - *states, + let test = test_states( + &id, vec![ StateLog { id, state: State::Built, - has_address: false, }, StateLog { id, state: State::Loading, - has_address: true, }, StateLog { id, state: State::Crashed, - has_address: false, }, - ] + ], ); + + select! { + _ = sleep(Duration::from_secs(50)) => { + let states = RECORDER.lock().unwrap().get_deployment_states(&id); + panic!("from running should start in built and end in crash for invalid: {:#?}", states) + }, + _ = test => {} + }; } #[tokio::test] async fn scope_with_nil_id() { - let deployment_manager = get_deployment_manager(); + let deployment_manager = get_deployment_manager().await; let id = Uuid::nil(); deployment_manager @@ -959,14 +872,16 @@ mod tests { ); } - fn get_deployment_manager() -> DeploymentManager { + async fn get_deployment_manager() -> DeploymentManager { DeploymentManager::builder() - .abstract_factory(StubAbstractProvisionerFactory) - .runtime_logger_factory(StubRuntimeLoggerFactory) .build_log_recorder(RECORDER.clone()) .secret_recorder(RECORDER.clone()) .active_deployment_getter(StubActiveDeploymentGetter) .artifacts_path(PathBuf::from("/tmp")) + .secret_getter(StubSecretGetter) + .resource_manager(StubResourceManager) + .runtime(get_runtime_manager()) + .deployment_updater(StubDeploymentUpdater) .queue_client(StubBuildQueueClient) .build() } diff --git a/deployer/src/deployment/mod.rs b/deployer/src/deployment/mod.rs index 74d706acd..fe5aca313 100644 --- a/deployer/src/deployment/mod.rs +++ b/deployer/src/deployment/mod.rs @@ -1,61 +1,50 @@ pub mod deploy_layer; pub mod gateway_client; -pub mod provisioner_factory; mod queue; mod run; -pub mod runtime_logger; -mod storage_manager; -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; pub use queue::Queued; pub use run::{ActiveDeploymentsGetter, Built}; +use shuttle_common::storage_manager::ArtifactsStorageManager; use tracing::{instrument, Span}; use tracing_opentelemetry::OpenTelemetrySpanExt; -use crate::persistence::{SecretRecorder, State}; -use tokio::sync::{broadcast, mpsc}; +use crate::{ + persistence::{DeploymentUpdater, ResourceManager, SecretGetter, SecretRecorder, State}, + RuntimeManager, +}; +use tokio::sync::{mpsc, Mutex}; use uuid::Uuid; -use self::{ - deploy_layer::LogRecorder, gateway_client::BuildQueueClient, storage_manager::StorageManager, -}; +use self::{deploy_layer::LogRecorder, gateway_client::BuildQueueClient}; const QUEUE_BUFFER_SIZE: usize = 100; const RUN_BUFFER_SIZE: usize = 100; -const KILL_BUFFER_SIZE: usize = 10; -pub struct DeploymentManagerBuilder { - abstract_factory: Option, - runtime_logger_factory: Option, +pub struct DeploymentManagerBuilder { build_log_recorder: Option, secret_recorder: Option, active_deployment_getter: Option, artifacts_path: Option, + runtime_manager: Option>>, + deployment_updater: Option, + secret_getter: Option, + resource_manager: Option, queue_client: Option, } -impl DeploymentManagerBuilder +impl DeploymentManagerBuilder where - AF: provisioner_factory::AbstractFactory, - RLF: runtime_logger::Factory, LR: LogRecorder, SR: SecretRecorder, ADG: ActiveDeploymentsGetter, + DU: DeploymentUpdater, + SG: SecretGetter, + RM: ResourceManager, QC: BuildQueueClient, { - pub fn abstract_factory(mut self, abstract_factory: AF) -> Self { - self.abstract_factory = Some(abstract_factory); - - self - } - - pub fn runtime_logger_factory(mut self, runtime_logger_factory: RLF) -> Self { - self.runtime_logger_factory = Some(runtime_logger_factory); - - self - } - pub fn build_log_recorder(mut self, build_log_recorder: LR) -> Self { self.build_log_recorder = Some(build_log_recorder); @@ -86,17 +75,35 @@ where self } + pub fn secret_getter(mut self, secret_getter: SG) -> Self { + self.secret_getter = Some(secret_getter); + + self + } + + pub fn resource_manager(mut self, resource_manager: RM) -> Self { + self.resource_manager = Some(resource_manager); + + self + } + + pub fn runtime(mut self, runtime_manager: Arc>) -> Self { + self.runtime_manager = Some(runtime_manager); + + self + } + + pub fn deployment_updater(mut self, deployment_updater: DU) -> Self { + self.deployment_updater = Some(deployment_updater); + + self + } + /// Creates two Tokio tasks, one for building queued services, the other for /// executing/deploying built services. Two multi-producer, single consumer /// channels are also created which are for moving on-going service /// deployments between the aforementioned tasks. pub fn build(self) -> DeploymentManager { - let abstract_factory = self - .abstract_factory - .expect("an abstract factory to be set"); - let runtime_logger_factory = self - .runtime_logger_factory - .expect("a runtime logger factory to be set"); let build_log_recorder = self .build_log_recorder .expect("a build log recorder to be set"); @@ -106,17 +113,23 @@ where .expect("an active deployment getter to be set"); let artifacts_path = self.artifacts_path.expect("artifacts path to be set"); let queue_client = self.queue_client.expect("a queue client to be set"); + let runtime_manager = self.runtime_manager.expect("a runtime manager to be set"); + let deployment_updater = self + .deployment_updater + .expect("a deployment updater to be set"); + let secret_getter = self.secret_getter.expect("a secret getter to be set"); + let resource_manager = self.resource_manager.expect("a resource manager to be set"); let (queue_send, queue_recv) = mpsc::channel(QUEUE_BUFFER_SIZE); let (run_send, run_recv) = mpsc::channel(RUN_BUFFER_SIZE); - let (kill_send, _) = broadcast::channel(KILL_BUFFER_SIZE); - let storage_manager = StorageManager::new(artifacts_path); + let storage_manager = ArtifactsStorageManager::new(artifacts_path); let run_send_clone = run_send.clone(); tokio::spawn(queue::task( queue_recv, run_send_clone, + deployment_updater.clone(), build_log_recorder, secret_recorder, storage_manager.clone(), @@ -124,17 +137,18 @@ where )); tokio::spawn(run::task( run_recv, - kill_send.clone(), - abstract_factory, - runtime_logger_factory, + runtime_manager.clone(), + deployment_updater, active_deployment_getter, + secret_getter, + resource_manager, storage_manager.clone(), )); DeploymentManager { queue_send, run_send, - kill_send, + runtime_manager, storage_manager, } } @@ -144,8 +158,8 @@ where pub struct DeploymentManager { queue_send: QueueSender, run_send: RunSender, - kill_send: KillSender, - storage_manager: StorageManager, + runtime_manager: Arc>, + storage_manager: ArtifactsStorageManager, } /// ```no-test @@ -165,15 +179,17 @@ pub struct DeploymentManager { impl DeploymentManager { /// Create a new deployment manager. Manages one or more 'pipelines' for /// processing service building, loading, and deployment. - pub fn builder() -> DeploymentManagerBuilder - { + pub fn builder( + ) -> DeploymentManagerBuilder { DeploymentManagerBuilder { - abstract_factory: None, - runtime_logger_factory: None, build_log_recorder: None, secret_recorder: None, active_deployment_getter: None, artifacts_path: None, + runtime_manager: None, + deployment_updater: None, + secret_getter: None, + resource_manager: None, queue_client: None, } } @@ -194,12 +210,10 @@ impl DeploymentManager { } pub async fn kill(&self, id: Uuid) { - if self.kill_send.receiver_count() > 0 { - self.kill_send.send(id).unwrap(); - } + self.runtime_manager.lock().await.kill(&id).await; } - pub fn storage_manager(&self) -> StorageManager { + pub fn storage_manager(&self) -> ArtifactsStorageManager { self.storage_manager.clone() } } @@ -209,6 +223,3 @@ type QueueReceiver = mpsc::Receiver; type RunSender = mpsc::Sender; type RunReceiver = mpsc::Receiver; - -type KillSender = broadcast::Sender; -type KillReceiver = broadcast::Receiver; diff --git a/deployer/src/deployment/provisioner_factory.rs b/deployer/src/deployment/provisioner_factory.rs deleted file mode 100644 index e2b7d167b..000000000 --- a/deployer/src/deployment/provisioner_factory.rs +++ /dev/null @@ -1,251 +0,0 @@ -use std::{collections::BTreeMap, path::PathBuf}; - -use async_trait::async_trait; -use shuttle_common::{ - backends::{ - auth::{Claim, ClaimLayer, ClaimService}, - tracing::{InjectPropagation, InjectPropagationLayer}, - }, - database, DatabaseReadyInfo, -}; -use shuttle_proto::provisioner::{ - database_request::DbType, provisioner_client::ProvisionerClient, DatabaseRequest, -}; -use shuttle_service::{Environment, Factory, ServiceName}; -use thiserror::Error; -use tonic::{ - transport::{Channel, Endpoint}, - Request, -}; -use tower::ServiceBuilder; -use tracing::{debug, info, trace}; -use uuid::Uuid; - -use crate::persistence::{Resource, ResourceManager, ResourceType, SecretGetter}; - -use super::storage_manager::StorageManager; - -/// Trait to make it easy to get a factory (service locator) for each service being started -#[async_trait] -pub trait AbstractFactory: Send + Sync + 'static { - type Output: Factory; - type Error: std::error::Error; - - /// Get a factory for a specific service - async fn get_factory( - &self, - service_name: ServiceName, - service_id: Uuid, - deployment_id: Uuid, - storage_manager: StorageManager, - claim: Option, - ) -> Result; -} - -/// An abstract factory that makes factories which uses provisioner -#[derive(Clone)] -pub struct AbstractProvisionerFactory { - provisioner_uri: Endpoint, - resource_manager: R, - secret_getter: S, -} - -#[async_trait] -impl AbstractFactory for AbstractProvisionerFactory { - type Output = ProvisionerFactory; - type Error = ProvisionerError; - - async fn get_factory( - &self, - service_name: ServiceName, - service_id: Uuid, - deployment_id: Uuid, - storage_manager: StorageManager, - claim: Option, - ) -> Result { - let channel = self.provisioner_uri.clone().connect().await?; - let channel = ServiceBuilder::new() - .layer(ClaimLayer) - .layer(InjectPropagationLayer) - .service(channel); - - let provisioner_client = ProvisionerClient::new(channel); - - Ok(ProvisionerFactory { - provisioner_client, - service_name, - service_id, - deployment_id, - storage_manager, - resource_manager: self.resource_manager.clone(), - secret_getter: self.secret_getter.clone(), - claim, - info: None, - secrets: None, - }) - } -} - -impl AbstractProvisionerFactory { - pub fn new(provisioner_uri: Endpoint, resource_manager: R, secret_getter: S) -> Self { - Self { - provisioner_uri, - resource_manager, - secret_getter, - } - } -} - -#[derive(Error, Debug)] -pub enum ProvisionerError { - #[error("failed to connect to provisioner: {0}")] - TonicClient(#[from] tonic::transport::Error), -} - -/// A factory (service locator) which goes through the provisioner crate -pub struct ProvisionerFactory { - service_name: ServiceName, - service_id: Uuid, - deployment_id: Uuid, - storage_manager: StorageManager, - provisioner_client: ProvisionerClient>>, - info: Option, - resource_manager: R, - secret_getter: S, - secrets: Option>, - claim: Option, -} - -#[async_trait] -impl Factory for ProvisionerFactory { - async fn get_db_connection_string( - &mut self, - db_type: database::Type, - ) -> Result { - if let Some(ref info) = self.info { - debug!("A database has already been provisioned for this deployment, so reusing it"); - return Ok(info.connection_string_private()); - } - - let r#type = ResourceType::Database(db_type.clone().into()); - - // Try to get the database info from provisioner if possible - let info = if let Some(claim) = self.claim.clone() { - info!("Provisioning a {db_type} on the shuttle servers. This can take a while..."); - - let db_type: DbType = db_type.into(); - - let mut request = Request::new(DatabaseRequest { - project_name: self.service_name.to_string(), - db_type: Some(db_type), - }); - - request.extensions_mut().insert(claim); - - let response = self - .provisioner_client - .provision_database(request) - .await - .map_err(shuttle_service::error::CustomError::new)? - .into_inner(); - - let info: DatabaseReadyInfo = response.into(); - - self.resource_manager - .insert_resource(&Resource { - service_id: self.service_id, - r#type, - data: serde_json::to_value(&info).map_err(|err| { - shuttle_service::Error::Database(format!( - "failed to convert DatabaseReadyInfo to json: {err}", - )) - })?, - }) - .await - .map_err(|err| { - shuttle_service::Error::Database(format!("failed to store resource: {err}")) - })?; - - info - } else { - info!("Getting a {db_type} from a previous provision"); - - let resources = self - .resource_manager - .get_resources(&self.service_id) - .await - .map_err(|err| { - shuttle_service::Error::Database(format!("failed to get resources: {err}")) - })?; - - let info = resources.into_iter().find_map(|resource| { - if resource.r#type == r#type { - Some(resource.data) - } else { - None - } - }); - - if let Some(info) = info { - serde_json::from_value(info).map_err(|err| { - shuttle_service::Error::Database(format!( - "failed to convert json to DatabaseReadyInfo: {err}", - )) - })? - } else { - return Err(shuttle_service::Error::Database( - "could not find resource from past resources".to_string(), - )); - } - }; - - let conn_str = info.connection_string_private(); - self.info = Some(info); - - info!("Done provisioning database"); - trace!("giving a DB connection string: {}", conn_str); - Ok(conn_str) - } - - async fn get_secrets(&mut self) -> Result, shuttle_service::Error> { - if let Some(ref secrets) = self.secrets { - debug!("Returning previously fetched secrets"); - Ok(secrets.clone()) - } else { - info!("Fetching secrets for deployment"); - let iter = self - .secret_getter - .get_secrets(&self.service_id) - .await - .map_err(shuttle_service::error::CustomError::new)? - .into_iter() - .map(|secret| (secret.key, secret.value)); - - let secrets = BTreeMap::from_iter(iter); - self.secrets = Some(secrets.clone()); - - info!("Done fetching secrets"); - Ok(secrets) - } - } - - fn get_service_name(&self) -> ServiceName { - self.service_name.clone() - } - - fn get_environment(&self) -> Environment { - Environment::Production - } - - fn get_build_path(&self) -> Result { - self.storage_manager - .service_build_path(self.service_name.as_str()) - .map_err(Into::into) - } - - fn get_storage_path(&self) -> Result { - self.storage_manager - .deployment_storage_path(self.service_name.as_str(), &self.deployment_id) - .map_err(Into::into) - } -} diff --git a/deployer/src/deployment/queue.rs b/deployer/src/deployment/queue.rs index 007382982..0087531be 100644 --- a/deployer/src/deployment/queue.rs +++ b/deployer/src/deployment/queue.rs @@ -1,9 +1,9 @@ use super::deploy_layer::{Log, LogRecorder, LogType}; use super::gateway_client::BuildQueueClient; -use super::storage_manager::StorageManager; use super::{Built, QueueReceiver, RunSender, State}; use crate::error::{Error, Result, TestError}; -use crate::persistence::{LogLevel, SecretRecorder}; +use crate::persistence::{DeploymentUpdater, LogLevel, SecretRecorder}; +use shuttle_common::storage_manager::{ArtifactsStorageManager, StorageManager}; use cargo::util::interning::InternedString; use cargo_metadata::Message; @@ -11,8 +11,8 @@ use chrono::Utc; use crossbeam_channel::Sender; use opentelemetry::global; use serde_json::json; -use shuttle_common::backends::auth::Claim; -use shuttle_service::loader::{build_crate, get_config}; +use shuttle_common::claims::Claim; +use shuttle_service::builder::{build_crate, get_config, Runtime}; use tokio::time::{sleep, timeout}; use tracing::{debug, debug_span, error, info, instrument, trace, warn, Instrument, Span}; use tracing_opentelemetry::OpenTelemetrySpanExt; @@ -35,9 +35,10 @@ use tokio::fs; pub async fn task( mut recv: QueueReceiver, run_send: RunSender, + deployment_updater: impl DeploymentUpdater, log_recorder: impl LogRecorder, secret_recorder: impl SecretRecorder, - storage_manager: StorageManager, + storage_manager: ArtifactsStorageManager, queue_client: impl BuildQueueClient, ) { info!("Queue task started"); @@ -47,6 +48,7 @@ pub async fn task( info!("Queued deployment at the front of the queue: {id}"); + let deployment_updater = deployment_updater.clone(); let run_send_cloned = run_send.clone(); let log_recorder = log_recorder.clone(); let secret_recorder = secret_recorder.clone(); @@ -72,7 +74,12 @@ pub async fn task( } match queued - .handle(storage_manager, log_recorder, secret_recorder) + .handle( + storage_manager, + deployment_updater, + log_recorder, + secret_recorder, + ) .await { Ok(built) => { @@ -127,7 +134,7 @@ async fn remove_from_queue(queue_client: impl BuildQueueClient, id: Uuid) { } } -#[instrument(fields(id = %built.id, state = %State::Built))] +#[instrument(skip(run_send), fields(id = %built.id, state = %State::Built))] async fn promote_to_run(mut built: Built, run_send: RunSender) { let cx = Span::current().context(); @@ -151,10 +158,11 @@ pub struct Queued { } impl Queued { - #[instrument(skip(self, storage_manager, log_recorder, secret_recorder), fields(id = %self.id, state = %State::Building))] + #[instrument(skip(self, storage_manager, deployment_updater, log_recorder, secret_recorder), fields(id = %self.id, state = %State::Building))] async fn handle( self, - storage_manager: StorageManager, + storage_manager: ArtifactsStorageManager, + deployment_updater: impl DeploymentUpdater, log_recorder: impl LogRecorder, secret_recorder: impl SecretRecorder, ) -> Result { @@ -187,7 +195,6 @@ impl Queued { target: String::new(), fields: json!({ "build_line": line }), r#type: LogType::Event, - address: None, }, message => Log { id, @@ -199,7 +206,6 @@ impl Queued { target: String::new(), fields: serde_json::to_value(message).unwrap(), r#type: LogType::Event, - address: None, }, }; log_recorder.record(log); @@ -207,7 +213,7 @@ impl Queued { }); let project_path = project_path.canonicalize()?; - let so_path = build_deployment(self.id, &project_path, tx.clone()).await?; + let runtime = build_deployment(&project_path, tx.clone()).await?; if self.will_run_tests { info!( @@ -218,15 +224,23 @@ impl Queued { run_pre_deploy_tests(&project_path, tx).await?; } - info!("Moving built library"); + info!("Moving built executable"); - store_lib(&storage_manager, so_path, &self.id).await?; + store_executable(&storage_manager, &runtime, &self.id).await?; + + let is_next = matches!(runtime, Runtime::Next(_)); + + deployment_updater + .set_is_next(&id, is_next) + .await + .map_err(|e| Error::Build(Box::new(e)))?; let built = Built { id: self.id, service_name: self.service_name, service_id: self.service_id, tracing_context: Default::default(), + is_next, claim: self.claim, }; @@ -315,17 +329,12 @@ async fn extract_tar_gz_data(data: impl Read, dest: impl AsRef) -> Result< #[instrument(skip(project_path, tx))] async fn build_deployment( - deployment_id: Uuid, project_path: &Path, tx: crossbeam_channel::Sender, -) -> Result { - let so_path = build_crate(deployment_id, project_path, true, tx) +) -> Result { + build_crate(project_path, true, tx) .await - .map_err(|e| Error::Build(e.into()))?; - - trace!(?so_path, "got so path"); - - Ok(so_path) + .map_err(|e| Error::Build(e.into())) } #[instrument(skip(project_path, tx))] @@ -387,16 +396,22 @@ async fn run_pre_deploy_tests( } } -/// Store 'so' file in the libs folder -#[instrument(skip(storage_manager, so_path, id))] -async fn store_lib( - storage_manager: &StorageManager, - so_path: impl AsRef, +/// This will store the path to the executable for each runtime, which will be the users project with +/// an embedded runtime for alpha, and a .wasm file for shuttle-next. +#[instrument(skip(storage_manager, runtime, id))] +async fn store_executable( + storage_manager: &ArtifactsStorageManager, + runtime: &Runtime, id: &Uuid, ) -> Result<()> { - let new_so_path = storage_manager.deployment_library_path(id)?; + let executable_path = match runtime { + Runtime::Next(path) => path, + Runtime::Alpha(path) => path, + }; + + let new_executable_path = storage_manager.deployment_executable_path(id)?; - fs::rename(so_path, new_so_path).await?; + fs::rename(executable_path, new_executable_path).await?; Ok(()) } @@ -405,11 +420,13 @@ async fn store_lib( mod tests { use std::{collections::BTreeMap, fs::File, io::Write, path::Path}; + use shuttle_common::storage_manager::ArtifactsStorageManager; + use shuttle_service::builder::Runtime; use tempfile::Builder; use tokio::fs; use uuid::Uuid; - use crate::{deployment::storage_manager::StorageManager, error::TestError}; + use crate::error::TestError; #[tokio::test] async fn extract_tar_gz_data() { @@ -534,29 +551,34 @@ ff0e55bda1ff01000000000000000000e0079c01ff12a55500280000", } #[tokio::test] - async fn store_lib() { - let libs_dir = Builder::new().prefix("lib-store").tempdir().unwrap(); - let libs_p = libs_dir.path(); - let storage_manager = StorageManager::new(libs_p.to_path_buf()); + async fn store_executable() { + let executables_dir = Builder::new().prefix("executable-store").tempdir().unwrap(); + let executables_p = executables_dir.path(); + let storage_manager = ArtifactsStorageManager::new(executables_p.to_path_buf()); let build_p = storage_manager.builds_path().unwrap(); - let so_path = build_p.join("xyz.so"); + let executable_path = build_p.join("xyz"); + let runtime = Runtime::Alpha(executable_path.clone()); let id = Uuid::new_v4(); - fs::write(&so_path, "barfoo").await.unwrap(); + fs::write(&executable_path, "barfoo").await.unwrap(); - super::store_lib(&storage_manager, &so_path, &id) + super::store_executable(&storage_manager, &runtime, &id) .await .unwrap(); - // Old '.so' file gone? - assert!(!so_path.exists()); + // Old executable file gone? + assert!(!executable_path.exists()); assert_eq!( - fs::read_to_string(libs_p.join("shuttle-libs").join(id.to_string())) - .await - .unwrap(), + fs::read_to_string( + executables_p + .join("shuttle-executables") + .join(id.to_string()) + ) + .await + .unwrap(), "barfoo" ); } diff --git a/deployer/src/deployment/run.rs b/deployer/src/deployment/run.rs index c104783cb..4bd26191d 100644 --- a/deployer/src/deployment/run.rs +++ b/deployer/src/deployment/run.rs @@ -2,38 +2,45 @@ use std::{ collections::HashMap, net::{Ipv4Addr, SocketAddr}, path::PathBuf, - str::FromStr, + sync::Arc, }; use async_trait::async_trait; use opentelemetry::global; use portpicker::pick_unused_port; -use shuttle_common::{backends::auth::Claim, project::ProjectName as ServiceName}; -use shuttle_service::{ - loader::{LoadedService, Loader}, - Factory, Logger, +use shuttle_common::{ + claims::{Claim, ClaimService, InjectPropagation}, + resource, + storage_manager::ArtifactsStorageManager, }; -use tokio::task::JoinError; -use tracing::{debug, debug_span, error, info, instrument, trace, Instrument}; + +use shuttle_proto::runtime::{ + runtime_client::RuntimeClient, LoadRequest, StartRequest, StopReason, SubscribeStopRequest, + SubscribeStopResponse, +}; +use tokio::sync::Mutex; +use tonic::{transport::Channel, Code}; +use tracing::{debug, debug_span, error, info, instrument, trace, warn, Instrument}; use tracing_opentelemetry::OpenTelemetrySpanExt; use uuid::Uuid; -use super::{ - provisioner_factory, runtime_logger, storage_manager::StorageManager, KillReceiver, KillSender, - RunReceiver, State, +use super::{RunReceiver, State}; +use crate::{ + error::{Error, Result}, + persistence::{DeploymentUpdater, Resource, ResourceManager, SecretGetter}, + RuntimeManager, }; -use crate::error::{Error, Result}; -/// Run a task which takes runnable deploys from a channel and starts them up with a factory provided by the -/// abstract factory and a runtime logger provided by the logger factory +/// Run a task which takes runnable deploys from a channel and starts them up on our runtime /// A deploy is killed when it receives a signal from the kill channel pub async fn task( mut recv: RunReceiver, - kill_send: KillSender, - abstract_factory: impl provisioner_factory::AbstractFactory, - logger_factory: impl runtime_logger::Factory, + runtime_manager: Arc>, + deployment_updater: impl DeploymentUpdater, active_deployment_getter: impl ActiveDeploymentsGetter, - storage_manager: StorageManager, + secret_getter: impl SecretGetter, + resource_manager: impl ResourceManager, + storage_manager: ArtifactsStorageManager, ) { info!("Run task started"); @@ -42,65 +49,39 @@ pub async fn task( info!("Built deployment at the front of run queue: {id}"); - let kill_send = kill_send.clone(); - let kill_recv = kill_send.subscribe(); + let deployment_updater = deployment_updater.clone(); + let secret_getter = secret_getter.clone(); + let resource_manager = resource_manager.clone(); let storage_manager = storage_manager.clone(); - let port = match pick_unused_port() { - Some(port) => port, - None => { - start_crashed_cleanup( - &id, - Error::PrepareLoad( - "could not find a free port to deploy service on".to_string(), - ), - ); - continue; - } - }; - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port); - let service_name = match ServiceName::from_str(&built.service_name) { - Ok(name) => name, - Err(err) => { - start_crashed_cleanup(&id, err); - continue; - } - }; - let mut factory = match abstract_factory - .get_factory( - service_name, - built.service_id, - built.id, - storage_manager.clone(), - built.claim.clone(), - ) - .await - { - Ok(factory) => factory, - Err(err) => { - start_crashed_cleanup(&id, err); - continue; - } - }; - let logger = logger_factory.get_logger(id); - let old_deployments_killer = kill_old_deployments( built.service_id, id, active_deployment_getter.clone(), - kill_send, + runtime_manager.clone(), ); - let cleanup = move |result: std::result::Result< - std::result::Result<(), shuttle_service::Error>, - JoinError, - >| match result { - Ok(inner) => match inner { - Ok(()) => completed_cleanup(&id), - Err(err) => crashed_cleanup(&id, err), - }, - Err(err) if err.is_cancelled() => stopped_cleanup(&id), - Err(err) => start_crashed_cleanup(&id, err), + let cleanup = move |response: Option| { + debug!(response = ?response, "stop client response: "); + + if let Some(response) = response { + match StopReason::from_i32(response.reason).unwrap_or_default() { + StopReason::Request => stopped_cleanup(&id), + StopReason::End => completed_cleanup(&id), + StopReason::Crash => crashed_cleanup( + &id, + Error::Run(anyhow::Error::msg(response.message).into()), + ), + } + } else { + crashed_cleanup( + &id, + Error::Runtime(anyhow::anyhow!( + "stop subscribe channel stopped unexpectedly" + )), + ) + } }; + let runtime_manager = runtime_manager.clone(); tokio::spawn(async move { let parent_cx = global::get_text_map_propagator(|propagator| { @@ -112,11 +93,11 @@ pub async fn task( async move { if let Err(err) = built .handle( - addr, storage_manager, - &mut factory, - logger, - kill_recv, + secret_getter, + resource_manager, + runtime_manager, + deployment_updater, old_deployments_killer, cleanup, ) @@ -133,13 +114,15 @@ pub async fn task( } } -#[instrument(skip(active_deployment_getter, kill_send))] +#[instrument(skip(active_deployment_getter, runtime_manager))] async fn kill_old_deployments( service_id: Uuid, deployment_id: Uuid, active_deployment_getter: impl ActiveDeploymentsGetter, - kill_send: KillSender, + runtime_manager: Arc>, ) -> Result<()> { + let mut guard = runtime_manager.lock().await; + for old_id in active_deployment_getter .clone() .get_active_deployments(&service_id) @@ -149,9 +132,10 @@ async fn kill_old_deployments( .filter(|old_id| old_id != &deployment_id) { trace!(%old_id, "stopping old deployment"); - kill_send - .send(old_id) - .map_err(|e| Error::OldCleanup(Box::new(e)))?; + + if !guard.kill(&old_id).await { + warn!(id = %old_id, "failed to kill old deployment"); + } } Ok(()) @@ -199,199 +183,385 @@ pub struct Built { pub service_name: String, pub service_id: Uuid, pub tracing_context: HashMap, + pub is_next: bool, pub claim: Option, } impl Built { - #[instrument(skip(self, storage_manager, factory, logger, kill_recv, kill_old_deployments, cleanup), fields(id = %self.id, state = %State::Loading))] + #[instrument(skip(self, storage_manager, secret_getter, resource_manager, runtime_manager, deployment_updater, kill_old_deployments, cleanup), fields(id = %self.id, state = %State::Loading))] #[allow(clippy::too_many_arguments)] async fn handle( self, - address: SocketAddr, - storage_manager: StorageManager, - factory: &mut dyn Factory, - logger: Logger, - kill_recv: KillReceiver, + storage_manager: ArtifactsStorageManager, + secret_getter: impl SecretGetter, + resource_manager: impl ResourceManager, + runtime_manager: Arc>, + deployment_updater: impl DeploymentUpdater, kill_old_deployments: impl futures::Future>, - cleanup: impl FnOnce(std::result::Result, JoinError>) - + Send - + 'static, + cleanup: impl FnOnce(Option) + Send + 'static, ) -> Result<()> { - let so_path = storage_manager.deployment_library_path(&self.id)?; - let service = load_deployment(address, so_path, factory, logger).await?; + // For alpha this is the path to the users project with an embedded runtime. + // For shuttle-next this is the path to the compiled .wasm file, which will be + // used in the load request. + let executable_path = storage_manager.deployment_executable_path(&self.id)?; + + let port = match pick_unused_port() { + Some(port) => port, + None => { + return Err(Error::PrepareRun( + "could not find a free port to deploy service on".to_string(), + )) + } + }; + + let address = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port); + + let alpha_runtime_path = if self.is_next { + // The runtime client for next is the installed shuttle-next bin + None + } else { + Some(executable_path.clone()) + }; + + let runtime_client = runtime_manager + .lock() + .await + .get_runtime_client(self.id, alpha_runtime_path.clone()) + .await + .map_err(Error::Runtime)?; kill_old_deployments.await?; - info!("got handle for deployment"); // Execute loaded service - tokio::spawn(run(self.id, service, address, kill_recv, cleanup)); + load( + self.service_name.clone(), + self.service_id, + executable_path.clone(), + secret_getter, + resource_manager, + runtime_client.clone(), + self.claim, + ) + .await?; + + tokio::spawn(run( + self.id, + self.service_name, + runtime_client, + address, + deployment_updater, + cleanup, + )); Ok(()) } } -#[instrument(skip(service, kill_recv, cleanup), fields(address = %_address, state = %State::Running))] -async fn run( - id: Uuid, - service: LoadedService, - _address: SocketAddr, - mut kill_recv: KillReceiver, - cleanup: impl FnOnce(std::result::Result, JoinError>) - + Send - + 'static, -) { - info!("starting up service"); - let (mut handle, library) = service; - let result; - loop { - tokio::select! { - Ok(kill_id) = kill_recv.recv() => { - if kill_id == id { - debug!("deployment '{id}' killed"); - handle.abort(); - result = handle.await; - break; - } - } - rsl = &mut handle => { - result = rsl; - break; - } - } - } +async fn load( + service_name: String, + service_id: Uuid, + executable_path: PathBuf, + secret_getter: impl SecretGetter, + resource_manager: impl ResourceManager, + mut runtime_client: RuntimeClient>>, + claim: Option, +) -> Result<()> { + info!( + "loading project from: {}", + executable_path + .clone() + .into_os_string() + .into_string() + .unwrap_or_default() + ); - if let Err(err) = library.close() { - crashed_cleanup(&id, err); + // Get resources from cache when a claim is not set (ie an idl project is started) + let resources = if claim.is_none() { + resource_manager + .get_resources(&service_id) + .await + .unwrap() + .into_iter() + .map(resource::Response::from) + .map(resource::Response::into_bytes) + .collect() } else { - cleanup(result); + Default::default() + }; + + let secrets = secret_getter + .get_secrets(&service_id) + .await + .map_err(|e| Error::SecretsGet(Box::new(e)))? + .into_iter() + .map(|secret| (secret.key, secret.value)); + let secrets = HashMap::from_iter(secrets); + + let mut load_request = tonic::Request::new(LoadRequest { + path: executable_path + .into_os_string() + .into_string() + .unwrap_or_default(), + service_name: service_name.clone(), + resources, + secrets, + }); + + if let Some(claim) = claim { + load_request.extensions_mut().insert(claim); + } + + debug!("loading service"); + let response = runtime_client.load(load_request).await; + + match response { + Ok(response) => { + let response = response.into_inner(); + info!(?response, "loading response"); + + for resource in response.resources { + let resource: resource::Response = serde_json::from_slice(&resource).unwrap(); + let resource = Resource { + service_id, + r#type: resource.r#type.into(), + data: resource.data, + }; + resource_manager + .insert_resource(&resource) + .await + .expect("to add resource to persistence"); + } + + if response.success { + Ok(()) + } else { + error!(error = %response.message, "failed to load service"); + Err(Error::Load(response.message)) + } + } + Err(error) => { + error!(%error, "failed to load service"); + Err(Error::Load(error.to_string())) + } } } -#[instrument(skip(addr, so_path, factory, logger))] -async fn load_deployment( - addr: SocketAddr, - so_path: PathBuf, - factory: &mut dyn Factory, - logger: Logger, -) -> Result { - let loader = Loader::from_so_file(so_path)?; +#[instrument(skip(runtime_client, deployment_updater, cleanup), fields(state = %State::Running))] +async fn run( + id: Uuid, + service_name: String, + mut runtime_client: RuntimeClient>>, + address: SocketAddr, + deployment_updater: impl DeploymentUpdater, + cleanup: impl FnOnce(Option) + Send + 'static, +) { + deployment_updater + .set_address(&id, &address) + .await + .expect("to set deployment address"); + + let start_request = tonic::Request::new(StartRequest { + ip: address.to_string(), + }); + + // Subscribe to stop before starting to catch immediate errors + let mut stream = runtime_client + .subscribe_stop(tonic::Request::new(SubscribeStopRequest {})) + .await + .unwrap() + .into_inner(); + + info!("starting service"); + let response = runtime_client.start(start_request).await; + + match response { + Ok(response) => { + info!(response = ?response.into_inner(), "start client response: "); + + // Wait for stop reason + let reason = stream.message().await.expect("message from tonic stream"); + + cleanup(reason); + } + Err(ref status) if status.code() == Code::InvalidArgument => { + cleanup(Some(SubscribeStopResponse { + reason: StopReason::Crash as i32, + message: status.to_string(), + })); + } + Err(ref status) => { + start_crashed_cleanup( + &id, + Error::Start("runtime failed to start deployment".to_string()), + ); - Ok(loader.load(factory, addr, logger).await?) + error!(%status, "failed to start service"); + } + } } #[cfg(test)] mod tests { use std::{ - collections::BTreeMap, net::{Ipv4Addr, SocketAddr}, path::PathBuf, process::Command, + sync::Arc, time::Duration, }; - use shuttle_common::database; - use shuttle_service::{Factory, Logger}; + use async_trait::async_trait; + use portpicker::pick_unused_port; + use shuttle_common::storage_manager::ArtifactsStorageManager; + use shuttle_proto::{ + provisioner::{ + provisioner_server::{Provisioner, ProvisionerServer}, + DatabaseDeletionResponse, DatabaseRequest, DatabaseResponse, + }, + runtime::{StopReason, SubscribeStopResponse}, + }; use tempfile::Builder; use tokio::{ - sync::{broadcast, mpsc, oneshot}, - task::JoinError, + sync::{oneshot, Mutex}, time::sleep, }; + use tonic::transport::Server; use uuid::Uuid; - use crate::{deployment::storage_manager::StorageManager, error::Error}; + use crate::{ + persistence::{DeploymentUpdater, Resource, ResourceManager, Secret, SecretGetter}, + RuntimeManager, + }; use super::Built; const RESOURCES_PATH: &str = "tests/resources"; - struct StubFactory; - - #[async_trait::async_trait] - impl Factory for StubFactory { - async fn get_db_connection_string( - &mut self, - _db_type: database::Type, - ) -> Result { - panic!("no run test should get an sql connection"); - } + fn get_storage_manager() -> ArtifactsStorageManager { + let tmp_dir = Builder::new().prefix("shuttle_run_test").tempdir().unwrap(); + let path = tmp_dir.into_path(); - async fn get_secrets( - &mut self, - ) -> Result, shuttle_service::Error> { - panic!("no test should get any secrets"); - } + ArtifactsStorageManager::new(path) + } - fn get_service_name(&self) -> shuttle_service::ServiceName { - panic!("no test should get the service name"); - } + async fn kill_old_deployments() -> crate::error::Result<()> { + Ok(()) + } - fn get_environment(&self) -> shuttle_service::Environment { - panic!("no test should get the environment"); - } + struct ProvisionerMock; - fn get_build_path(&self) -> Result { - panic!("no test should get the build path"); + #[async_trait] + impl Provisioner for ProvisionerMock { + async fn provision_database( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + panic!("no run tests should request a db"); } - fn get_storage_path(&self) -> Result { - panic!("no test should get the storage path"); + async fn delete_database( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + panic!("no run tests should delete a db"); } } - fn get_storage_manager() -> StorageManager { + fn get_runtime_manager() -> Arc> { + let provisioner_addr = + SocketAddr::new(Ipv4Addr::LOCALHOST.into(), pick_unused_port().unwrap()); + let mock = ProvisionerMock; + + tokio::spawn(async move { + Server::builder() + .add_service(ProvisionerServer::new(mock)) + .serve(provisioner_addr) + .await + .unwrap(); + }); + let tmp_dir = Builder::new().prefix("shuttle_run_test").tempdir().unwrap(); let path = tmp_dir.into_path(); + let (tx, rx) = crossbeam_channel::unbounded(); - StorageManager::new(path) + tokio::runtime::Handle::current().spawn_blocking(move || { + while let Ok(log) = rx.recv() { + println!("test log: {log:?}"); + } + }); + + RuntimeManager::new(path, format!("http://{}", provisioner_addr), None, tx) } - fn get_logger(id: Uuid) -> Logger { - let (tx, mut rx) = mpsc::unbounded_channel(); + #[derive(Clone)] + struct StubSecretGetter; - tokio::spawn(async move { - while let Some(log) = rx.recv().await { - println!("{log}"); - } - }); + #[async_trait] + impl SecretGetter for StubSecretGetter { + type Err = std::io::Error; - Logger::new(tx, id) + async fn get_secrets(&self, _service_id: &Uuid) -> Result, Self::Err> { + Ok(Default::default()) + } } - async fn kill_old_deployments() -> crate::error::Result<()> { - Ok(()) + #[derive(Clone)] + struct StubResourceManager; + + #[async_trait] + impl ResourceManager for StubResourceManager { + type Err = std::io::Error; + + async fn insert_resource(&self, _resource: &Resource) -> Result<(), Self::Err> { + Ok(()) + } + async fn get_resources(&self, _service_id: &Uuid) -> Result, Self::Err> { + Ok(Vec::new()) + } + } + + #[derive(Clone)] + struct StubDeploymentUpdater; + + #[async_trait] + impl DeploymentUpdater for StubDeploymentUpdater { + type Err = std::io::Error; + + async fn set_address(&self, _id: &Uuid, _address: &SocketAddr) -> Result<(), Self::Err> { + Ok(()) + } + + async fn set_is_next(&self, _id: &Uuid, _is_next: bool) -> Result<(), Self::Err> { + Ok(()) + } } // This test uses the kill signal to make sure a service does stop when asked to #[tokio::test] async fn can_be_killed() { - let (built, storage_manager) = make_so_and_built("sleep-async"); + let (built, storage_manager) = make_and_built("sleep-async"); let id = built.id; - let (kill_send, kill_recv) = broadcast::channel(1); + let runtime_manager = get_runtime_manager(); let (cleanup_send, cleanup_recv) = oneshot::channel(); - let handle_cleanup = |result: std::result::Result< - std::result::Result<(), shuttle_service::Error>, - JoinError, - >| { - assert!( - matches!(result, Err(ref join_error) if join_error.is_cancelled()), - "handle should have been cancelled: {:?}", - result - ); - cleanup_send.send(()).unwrap(); + let handle_cleanup = |response: Option| { + let response = response.unwrap(); + match ( + StopReason::from_i32(response.reason).unwrap(), + response.message, + ) { + (StopReason::Request, mes) if mes.is_empty() => cleanup_send.send(()).unwrap(), + _ => panic!("expected stop due to request"), + } }; - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let mut factory = StubFactory; - let logger = get_logger(built.id); built .handle( - addr, storage_manager, - &mut factory, - logger, - kill_recv, + StubSecretGetter, + StubResourceManager, + runtime_manager.clone(), + StubDeploymentUpdater, kill_old_deployments(), handle_cleanup, ) @@ -402,7 +572,7 @@ mod tests { sleep(Duration::from_secs(1)).await; // Send kill signal - kill_send.send(id).unwrap(); + assert!(runtime_manager.lock().await.kill(&id).await); tokio::select! { _ = sleep(Duration::from_secs(1)) => panic!("cleanup should have been called"), @@ -413,33 +583,28 @@ mod tests { // This test does not use a kill signal to stop the service. Rather the service decided to stop on its own without errors #[tokio::test] async fn self_stop() { - let (built, storage_manager) = make_so_and_built("sleep-async"); - let (_kill_send, kill_recv) = broadcast::channel(1); + let (built, storage_manager) = make_and_built("sleep-async"); + let runtime_manager = get_runtime_manager(); let (cleanup_send, cleanup_recv) = oneshot::channel(); - let handle_cleanup = |result: std::result::Result< - std::result::Result<(), shuttle_service::Error>, - JoinError, - >| { - let result = result.unwrap(); - assert!( - result.is_ok(), - "did not expect error from self stopping service: {}", - result.unwrap_err() - ); - cleanup_send.send(()).unwrap(); + let handle_cleanup = |response: Option| { + let response = response.unwrap(); + match ( + StopReason::from_i32(response.reason).unwrap(), + response.message, + ) { + (StopReason::End, mes) if mes.is_empty() => cleanup_send.send(()).unwrap(), + _ => panic!("expected stop due to self end"), + } }; - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let mut factory = StubFactory; - let logger = get_logger(built.id); built .handle( - addr, storage_manager, - &mut factory, - logger, - kill_recv, + StubSecretGetter, + StubResourceManager, + runtime_manager.clone(), + StubDeploymentUpdater, kill_old_deployments(), handle_cleanup, ) @@ -450,38 +615,38 @@ mod tests { _ = sleep(Duration::from_secs(5)) => panic!("cleanup should have been called as service stopped on its own"), Ok(()) = cleanup_recv => {}, } + + // Prevent the runtime manager from dropping earlier, which will kill the processes it manages + drop(runtime_manager); } // Test for panics in Service::bind #[tokio::test] async fn panic_in_bind() { - let (built, storage_manager) = make_so_and_built("bind-panic"); - let (_kill_send, kill_recv) = broadcast::channel(1); - let (cleanup_send, cleanup_recv): (oneshot::Sender<()>, _) = oneshot::channel(); - - let handle_cleanup = |result: std::result::Result< - std::result::Result<(), shuttle_service::Error>, - JoinError, - >| { - let result = result.unwrap(); - assert!( - matches!(result, Err(shuttle_service::Error::BindPanic(ref msg)) if msg == "panic in bind"), - "expected inner error from handle: {:?}", - result - ); - cleanup_send.send(()).unwrap(); + let (built, storage_manager) = make_and_built("bind-panic"); + let runtime_manager = get_runtime_manager(); + let (cleanup_send, cleanup_recv) = oneshot::channel(); + + let handle_cleanup = |response: Option| { + let response = response.unwrap(); + match ( + StopReason::from_i32(response.reason).unwrap(), + response.message, + ) { + (StopReason::Crash, mes) if mes.contains("panic in bind") => { + cleanup_send.send(()).unwrap() + } + (_, mes) => panic!("expected stop due to crash: {mes}"), + } }; - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let mut factory = StubFactory; - let logger = get_logger(built.id); built .handle( - addr, storage_manager, - &mut factory, - logger, - kill_recv, + StubSecretGetter, + StubResourceManager, + runtime_manager.clone(), + StubDeploymentUpdater, kill_old_deployments(), handle_cleanup, ) @@ -492,78 +657,35 @@ mod tests { _ = sleep(Duration::from_secs(5)) => panic!("cleanup should have been called as service handle stopped after panic"), Ok(()) = cleanup_recv => {} } + + // Prevent the runtime manager from dropping earlier, which will kill the processes it manages + drop(runtime_manager); } // Test for panics in the main function #[tokio::test] + #[should_panic(expected = "Load(\"main panic\")")] async fn panic_in_main() { - let (built, storage_manager) = make_so_and_built("main-panic"); - let (_kill_send, kill_recv) = broadcast::channel(1); - - let handle_cleanup = |_result| panic!("the service shouldn't even start"); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let mut factory = StubFactory; - let logger = get_logger(built.id); - - let result = built - .handle( - addr, - storage_manager, - &mut factory, - logger, - kill_recv, - kill_old_deployments(), - handle_cleanup, - ) - .await; - - assert!( - matches!(result, Err(Error::Run(shuttle_service::Error::BuildPanic(ref msg))) if msg == "main panic"), - "expected inner error from main: {:?}", - result - ); - } - - #[tokio::test] - async fn missing_so() { - let built = Built { - id: Uuid::new_v4(), - service_name: "test".to_string(), - service_id: Uuid::new_v4(), - tracing_context: Default::default(), - claim: None, - }; - let (_kill_send, kill_recv) = broadcast::channel(1); + let (built, storage_manager) = make_and_built("main-panic"); + let runtime_manager = get_runtime_manager(); - let handle_cleanup = |_result| panic!("no service means no cleanup"); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let storage_manager = get_storage_manager(); - let mut factory = StubFactory; - let logger = get_logger(built.id); + let handle_cleanup = |_result| panic!("service should never be started"); - let result = built + built .handle( - addr, storage_manager, - &mut factory, - logger, - kill_recv, + StubSecretGetter, + StubResourceManager, + runtime_manager.clone(), + StubDeploymentUpdater, kill_old_deployments(), handle_cleanup, ) - .await; - - assert!( - matches!( - result, - Err(Error::Load(shuttle_service::loader::LoaderError::Load(_))) - ), - "expected missing 'so' error: {:?}", - result - ); + .await + .unwrap(); } - fn make_so_and_built(crate_name: &str) -> (Built, StorageManager) { + fn make_and_built(crate_name: &str) -> (Built, ArtifactsStorageManager) { let crate_dir: PathBuf = [RESOURCES_PATH, crate_name].iter().collect(); Command::new("cargo") @@ -574,27 +696,25 @@ mod tests { .wait() .unwrap(); - let dashes_replaced = crate_name.replace('-', "_"); - let lib_name = if cfg!(target_os = "windows") { - format!("{}.dll", dashes_replaced) + format!("{}.exe", crate_name) } else { - format!("lib{}.so", dashes_replaced) + crate_name.to_string() }; let id = Uuid::new_v4(); let so_path = crate_dir.join("target/release").join(lib_name); let storage_manager = get_storage_manager(); - let new_so_path = storage_manager.deployment_library_path(&id).unwrap(); + let new_so_path = storage_manager.deployment_executable_path(&id).unwrap(); std::fs::copy(so_path, new_so_path).unwrap(); - ( Built { id, service_name: crate_name.to_string(), service_id: Uuid::new_v4(), tracing_context: Default::default(), + is_next: false, claim: None, }, storage_manager, diff --git a/deployer/src/deployment/runtime_logger.rs b/deployer/src/deployment/runtime_logger.rs deleted file mode 100644 index 266805fed..000000000 --- a/deployer/src/deployment/runtime_logger.rs +++ /dev/null @@ -1,54 +0,0 @@ -use shuttle_common::LogItem; -use shuttle_service::Logger; -use tokio::sync::mpsc::{self, UnboundedSender}; -use uuid::Uuid; - -use super::deploy_layer::{self, LogType}; - -pub trait Factory: Send + 'static { - fn get_logger(&self, id: Uuid) -> Logger; -} - -/// Factory to create runtime loggers for deployments -pub struct RuntimeLoggerFactory { - log_send: crossbeam_channel::Sender, -} - -impl RuntimeLoggerFactory { - pub fn new(log_send: crossbeam_channel::Sender) -> Self { - Self { log_send } - } -} - -impl Factory for RuntimeLoggerFactory { - fn get_logger(&self, id: Uuid) -> Logger { - let (tx, mut rx): (UnboundedSender, _) = mpsc::unbounded_channel(); - - let sender = self.log_send.clone(); - - tokio::spawn(async move { - while let Some(log) = rx.recv().await { - sender.send(log.into()).expect("to send log to persistence"); - } - }); - - Logger::new(tx, id) - } -} - -impl From for deploy_layer::Log { - fn from(log: LogItem) -> Self { - Self { - id: log.id, - state: log.state.into(), - level: log.level.into(), - timestamp: log.timestamp, - file: log.file, - line: log.line, - target: log.target, - fields: serde_json::from_slice(&log.fields).unwrap(), - r#type: LogType::Event, - address: None, - } - } -} diff --git a/deployer/src/deployment/storage_manager.rs b/deployer/src/deployment/storage_manager.rs deleted file mode 100644 index 5a5fa1300..000000000 --- a/deployer/src/deployment/storage_manager.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::{fs, io, path::PathBuf}; - -use uuid::Uuid; - -/// Manager to take care of directories for storing project, services and deployment files -#[derive(Clone)] -pub struct StorageManager { - artifacts_path: PathBuf, -} - -impl StorageManager { - pub fn new(artifacts_path: PathBuf) -> Self { - Self { artifacts_path } - } - - /// Path of the directory that contains extracted service Cargo projects. - pub fn builds_path(&self) -> Result { - let builds_path = self.artifacts_path.join("shuttle-builds"); - fs::create_dir_all(&builds_path)?; - - Ok(builds_path) - } - - /// Path for a specific service - pub fn service_build_path>(&self, service_name: S) -> Result { - let builds_path = self.builds_path()?.join(service_name.as_ref()); - fs::create_dir_all(&builds_path)?; - - Ok(builds_path) - } - - /// The directory in which compiled '.so' files are stored. - pub fn libraries_path(&self) -> Result { - let libs_path = self.artifacts_path.join("shuttle-libs"); - fs::create_dir_all(&libs_path)?; - - Ok(libs_path) - } - - /// Path to `.so` for a service - pub fn deployment_library_path(&self, deployment_id: &Uuid) -> Result { - let library_path = self.libraries_path()?.join(deployment_id.to_string()); - - Ok(library_path) - } - - /// Path of the directory to store user files - pub fn storage_path(&self) -> Result { - let storage_path = self.artifacts_path.join("shuttle-storage"); - fs::create_dir_all(&storage_path)?; - - Ok(storage_path) - } - - /// Path to folder for storing deployment files - pub fn deployment_storage_path>( - &self, - service_name: S, - deployment_id: &Uuid, - ) -> Result { - let storage_path = self - .storage_path()? - .join(service_name.as_ref()) - .join(deployment_id.to_string()); - fs::create_dir_all(&storage_path)?; - - Ok(storage_path) - } -} diff --git a/deployer/src/error.rs b/deployer/src/error.rs index 452062b90..e5e407276 100644 --- a/deployer/src/error.rs +++ b/deployer/src/error.rs @@ -2,8 +2,6 @@ use std::error::Error as StdError; use std::io; use thiserror::Error; -use shuttle_service::loader::LoaderError; - use cargo::util::errors::CargoTestError; use crate::deployment::gateway_client; @@ -14,10 +12,10 @@ pub enum Error { InputOutput(#[from] io::Error), #[error("Build error: {0}")] Build(#[source] Box), - #[error("Prepare to load error: {0}")] - PrepareLoad(String), #[error("Load error: {0}")] - Load(#[from] LoaderError), + Load(String), + #[error("Prepare to run error: {0}")] + PrepareRun(String), #[error("Run error: {0}")] Run(#[from] shuttle_service::Error), #[error("Pre-deployment test failure: {0}")] @@ -26,10 +24,16 @@ pub enum Error { SecretsParse(#[from] toml::de::Error), #[error("Failed to set secrets: {0}")] SecretsSet(#[source] Box), + #[error("Failed to get secrets: {0}")] + SecretsGet(#[source] Box), #[error("Failed to cleanup old deployments: {0}")] OldCleanup(#[source] Box), #[error("Gateway client error: {0}")] GatewayClient(#[from] gateway_client::Error), + #[error("Failed to get runtime: {0}")] + Runtime(#[source] anyhow::Error), + #[error("Failed to call start on runtime: {0}")] + Start(String), } #[derive(Error, Debug)] diff --git a/deployer/src/handlers/mod.rs b/deployer/src/handlers/mod.rs index 25710e236..f0185f02f 100644 --- a/deployer/src/handlers/mod.rs +++ b/deployer/src/handlers/mod.rs @@ -13,14 +13,16 @@ use fqdn::FQDN; use futures::StreamExt; use hyper::Uri; use shuttle_common::backends::auth::{ - AdminSecretLayer, AuthPublicKey, Claim, JwtAuthenticationLayer, Scope, ScopedLayer, + AdminSecretLayer, AuthPublicKey, JwtAuthenticationLayer, ScopedLayer, }; use shuttle_common::backends::headers::XShuttleAccountName; use shuttle_common::backends::metrics::{Metrics, TraceLayer}; +use shuttle_common::claims::{Claim, Scope}; use shuttle_common::models::secret; use shuttle_common::project::ProjectName; +use shuttle_common::storage_manager::StorageManager; use shuttle_common::{request_span, LogItem}; -use shuttle_service::loader::clean_crate; +use shuttle_service::builder::clean_crate; use tracing::{debug, error, field, instrument, trace}; use uuid::Uuid; @@ -208,6 +210,7 @@ async fn post_service( state: State::Queued, last_update: Utc::now(), address: None, + is_next: false, }; let mut data = Vec::new(); @@ -403,7 +406,7 @@ async fn post_clean( ) -> Result>> { let project_path = deployment_manager .storage_manager() - .service_build_path(project_name) + .service_build_path(&project_name) .map_err(anyhow::Error::new)?; let lines = clean_crate(&project_path, true)?; diff --git a/deployer/src/lib.rs b/deployer/src/lib.rs index 86849e955..73912cb76 100644 --- a/deployer/src/lib.rs +++ b/deployer/src/lib.rs @@ -1,11 +1,8 @@ -use std::{convert::Infallible, net::SocketAddr}; +use std::{convert::Infallible, net::SocketAddr, sync::Arc}; pub use args::Args; -pub use deployment::{ - deploy_layer::DeployLayer, provisioner_factory::AbstractProvisionerFactory, - runtime_logger::RuntimeLoggerFactory, -}; -use deployment::{provisioner_factory, runtime_logger, Built, DeploymentManager}; +pub use deployment::deploy_layer::DeployLayer; +use deployment::{Built, DeploymentManager}; use fqdn::FQDN; use hyper::{ server::conn::AddrStream, @@ -13,6 +10,8 @@ use hyper::{ }; pub use persistence::Persistence; use proxy::AddressGetter; +pub use runtime_manager::RuntimeManager; +use tokio::sync::Mutex; use tracing::{error, info}; use crate::deployment::gateway_client::GatewayClient; @@ -23,20 +22,22 @@ mod error; mod handlers; mod persistence; mod proxy; +mod runtime_manager; pub async fn start( - abstract_factory: impl provisioner_factory::AbstractFactory, - runtime_logger_factory: impl runtime_logger::Factory, persistence: Persistence, + runtime_manager: Arc>, args: Args, ) { let deployment_manager = DeploymentManager::builder() - .abstract_factory(abstract_factory) - .runtime_logger_factory(runtime_logger_factory) .build_log_recorder(persistence.clone()) .secret_recorder(persistence.clone()) .active_deployment_getter(persistence.clone()) .artifacts_path(args.artifacts_path) + .runtime(runtime_manager) + .deployment_updater(persistence.clone()) + .secret_getter(persistence.clone()) + .resource_manager(persistence.clone()) .queue_client(GatewayClient::new(args.gateway_uri)) .build(); @@ -50,6 +51,7 @@ pub async fn start( service_name: existing_deployment.service_name, service_id: existing_deployment.service_id, tracing_context: Default::default(), + is_next: existing_deployment.is_next, claim: None, // This will cause us to read the resource info from past provisions }; deployment_manager.run_push(built).await; @@ -64,12 +66,11 @@ pub async fn start( args.project, ) .await; - let make_service = router.into_make_service(); info!(address=%args.api_address, "Binding to and listening at address"); axum::Server::bind(&args.api_address) - .serve(make_service) + .serve(router.into_make_service()) .await .unwrap_or_else(|_| panic!("Failed to bind to address: {}", args.api_address)); } diff --git a/deployer/src/main.rs b/deployer/src/main.rs index f3cde259f..11d178b1a 100644 --- a/deployer/src/main.rs +++ b/deployer/src/main.rs @@ -1,12 +1,10 @@ +use std::process::exit; + use clap::Parser; use shuttle_common::backends::tracing::setup_tracing; -use shuttle_deployer::{ - start, start_proxy, AbstractProvisionerFactory, Args, DeployLayer, Persistence, - RuntimeLoggerFactory, -}; +use shuttle_deployer::{start, start_proxy, Args, DeployLayer, Persistence, RuntimeManager}; use tokio::select; -use tonic::transport::Endpoint; -use tracing::trace; +use tracing::{error, trace}; use tracing_subscriber::prelude::*; // The `multi_thread` is needed to prevent a deadlock in shuttle_service::loader::build_crate() which spawns two threads @@ -23,19 +21,21 @@ async fn main() { "deployer", ); - let provisioner_uri = Endpoint::try_from(format!( - "http://{}:{}", - args.provisioner_address, args.provisioner_port - )) - .expect("provisioner uri is not valid"); - - let abstract_factory = - AbstractProvisionerFactory::new(provisioner_uri, persistence.clone(), persistence.clone()); - - let runtime_logger_factory = RuntimeLoggerFactory::new(persistence.get_log_sender()); + let runtime_manager = RuntimeManager::new( + args.artifacts_path.clone(), + args.provisioner_address.uri().to_string(), + Some(args.auth_uri.to_string()), + persistence.get_log_sender(), + ); select! { - _ = start_proxy(args.proxy_address, args.proxy_fqdn.clone(), persistence.clone()) => {}, - _ = start(abstract_factory, runtime_logger_factory, persistence, args) => {}, + _ = start_proxy(args.proxy_address, args.proxy_fqdn.clone(), persistence.clone()) => { + error!("Proxy stopped.") + }, + _ = start(persistence, runtime_manager, args) => { + error!("Deployment service stopped.") + }, } + + exit(1); } diff --git a/deployer/src/persistence/deployment.rs b/deployer/src/persistence/deployment.rs index 03d210066..b7e50f8b8 100644 --- a/deployer/src/persistence/deployment.rs +++ b/deployer/src/persistence/deployment.rs @@ -1,5 +1,6 @@ use std::{net::SocketAddr, str::FromStr}; +use async_trait::async_trait; use chrono::{DateTime, Utc}; use sqlx::{sqlite::SqliteRow, FromRow, Row}; use tracing::error; @@ -14,6 +15,7 @@ pub struct Deployment { pub state: State, pub last_update: DateTime, pub address: Option, + pub is_next: bool, } impl FromRow<'_, SqliteRow> for Deployment { @@ -36,6 +38,7 @@ impl FromRow<'_, SqliteRow> for Deployment { state: row.try_get("state")?, last_update: row.try_get("last_update")?, address, + is_next: row.try_get("is_next")?, }) } } @@ -51,12 +54,23 @@ impl From for shuttle_common::models::deployment::Response { } } +/// Update the details of a deployment +#[async_trait] +pub trait DeploymentUpdater: Clone + Send + Sync + 'static { + type Err: std::error::Error + Send; + + /// Set the address for a deployment + async fn set_address(&self, id: &Uuid, address: &SocketAddr) -> Result<(), Self::Err>; + + /// Set if a deployment is build on shuttle-next + async fn set_is_next(&self, id: &Uuid, is_next: bool) -> Result<(), Self::Err>; +} + #[derive(Debug, PartialEq, Eq)] pub struct DeploymentState { pub id: Uuid, pub state: State, pub last_update: DateTime, - pub address: Option, } #[derive(sqlx::FromRow, Debug, PartialEq, Eq)] @@ -64,4 +78,5 @@ pub struct DeploymentRunnable { pub id: Uuid, pub service_name: String, pub service_id: Uuid, + pub is_next: bool, } diff --git a/deployer/src/persistence/mod.rs b/deployer/src/persistence/mod.rs index 91b64ef68..b726e8d64 100644 --- a/deployer/src/persistence/mod.rs +++ b/deployer/src/persistence/mod.rs @@ -27,12 +27,11 @@ use tracing::{error, info, instrument, trace}; use uuid::Uuid; use self::deployment::DeploymentRunnable; -pub use self::deployment::{Deployment, DeploymentState}; +pub use self::deployment::{Deployment, DeploymentState, DeploymentUpdater}; pub use self::error::Error as PersistenceError; pub use self::log::{Level as LogLevel, Log}; pub use self::resource::{Resource, ResourceManager, Type as ResourceType}; -use self::secret::Secret; -pub use self::secret::{SecretGetter, SecretRecorder}; +pub use self::secret::{Secret, SecretGetter, SecretRecorder}; pub use self::service::Service; pub use self::state::State; pub use self::user::User; @@ -172,13 +171,14 @@ impl Persistence { let deployment = deployment.into(); sqlx::query( - "INSERT INTO deployments (id, service_id, state, last_update, address) VALUES (?, ?, ?, ?, ?)", + "INSERT INTO deployments (id, service_id, state, last_update, address, is_next) VALUES (?, ?, ?, ?, ?, ?)", ) .bind(deployment.id) .bind(deployment.service_id) .bind(deployment.state) .bind(deployment.last_update) .bind(deployment.address.map(|socket| socket.to_string())) + .bind(deployment.is_next) .execute(&self.pool) .await .map(|_| ()) @@ -265,7 +265,7 @@ impl Persistence { pub async fn get_all_runnable_deployments(&self) -> Result> { sqlx::query_as( - r#"SELECT d.id, service_id, s.name AS service_name + r#"SELECT d.id, service_id, s.name AS service_name, d.is_next FROM deployments AS d JOIN services AS s ON s.id = d.service_id WHERE state = ? @@ -282,10 +282,12 @@ impl Persistence { get_deployment_logs(&self.pool, id).await } + /// Get a broadcast channel for listening to logs that are being stored into persistence pub fn get_log_subscriber(&self) -> Receiver { self.stream_log_send.subscribe() } + /// Returns a sender for sending logs to persistence storage pub fn get_log_sender(&self) -> crossbeam_channel::Sender { self.log_send.clone() } @@ -294,12 +296,9 @@ impl Persistence { async fn update_deployment(pool: &SqlitePool, state: impl Into) -> Result<()> { let state = state.into(); - // TODO: Handle moving to 'active_deployments' table for State::Running. - - sqlx::query("UPDATE deployments SET state = ?, last_update = ?, address = ? WHERE id = ?") + sqlx::query("UPDATE deployments SET state = ?, last_update = ? WHERE id = ?") .bind(state.state) .bind(state.last_update) - .bind(state.address.map(|socket| socket.to_string())) .bind(state.id) .execute(pool) .await @@ -440,6 +439,31 @@ impl AddressGetter for Persistence { } } +#[async_trait::async_trait] +impl DeploymentUpdater for Persistence { + type Err = Error; + + async fn set_address(&self, id: &Uuid, address: &SocketAddr) -> Result<()> { + sqlx::query("UPDATE deployments SET address = ? WHERE id = ?") + .bind(address.to_string()) + .bind(id) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(Error::from) + } + + async fn set_is_next(&self, id: &Uuid, is_next: bool) -> Result<()> { + sqlx::query("UPDATE deployments SET is_next = ? WHERE id = ?") + .bind(is_next) + .bind(id) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(Error::from) + } +} + #[async_trait::async_trait] impl ActiveDeploymentsGetter for Persistence { type Err = Error; @@ -491,7 +515,9 @@ mod tests { state: State::Queued, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 4, 43, 33).unwrap(), address: None, + is_next: false, }; + let address = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 12345); p.insert_deployment(deployment.clone()).await.unwrap(); assert_eq!(p.get_deployment(&id).await.unwrap().unwrap(), deployment); @@ -502,13 +528,18 @@ mod tests { id, state: State::Built, last_update: Utc::now(), - address: None, }, ) .await .unwrap(); + + p.set_address(&id, &address).await.unwrap(); + p.set_is_next(&id, true).await.unwrap(); + let update = p.get_deployment(&id).await.unwrap().unwrap(); assert_eq!(update.state, State::Built); + assert_eq!(update.address, Some(address)); + assert!(update.is_next); assert_ne!( update.last_update, Utc.with_ymd_and_hms(2022, 4, 25, 4, 43, 33).unwrap() @@ -528,6 +559,7 @@ mod tests { state: State::Crashed, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 7, 29, 35).unwrap(), address: None, + is_next: false, }; let deployment_stopped = Deployment { id: Uuid::new_v4(), @@ -535,6 +567,7 @@ mod tests { state: State::Stopped, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 7, 49, 35).unwrap(), address: None, + is_next: false, }; let deployment_other = Deployment { id: Uuid::new_v4(), @@ -542,6 +575,7 @@ mod tests { state: State::Running, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 7, 39, 39).unwrap(), address: None, + is_next: false, }; let deployment_running = Deployment { id: Uuid::new_v4(), @@ -549,6 +583,7 @@ mod tests { state: State::Running, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 7, 48, 29).unwrap(), address: Some(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 9876)), + is_next: true, }; for deployment in [ @@ -588,6 +623,7 @@ mod tests { state: State::Crashed, last_update: Utc::now(), address: None, + is_next: false, }; let deployment_stopped = Deployment { id: Uuid::new_v4(), @@ -595,6 +631,7 @@ mod tests { state: State::Stopped, last_update: Utc::now(), address: None, + is_next: false, }; let deployment_running = Deployment { id: Uuid::new_v4(), @@ -602,6 +639,7 @@ mod tests { state: State::Running, last_update: Utc::now(), address: Some(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 9876)), + is_next: false, }; let deployment_queued = Deployment { id: queued_id, @@ -609,6 +647,7 @@ mod tests { state: State::Queued, last_update: Utc::now(), address: None, + is_next: false, }; let deployment_building = Deployment { id: building_id, @@ -616,6 +655,7 @@ mod tests { state: State::Building, last_update: Utc::now(), address: None, + is_next: false, }; let deployment_built = Deployment { id: built_id, @@ -623,6 +663,7 @@ mod tests { state: State::Built, last_update: Utc::now(), address: None, + is_next: true, }; let deployment_loading = Deployment { id: loading_id, @@ -630,6 +671,7 @@ mod tests { state: State::Loading, last_update: Utc::now(), address: None, + is_next: false, }; for deployment in [ @@ -688,6 +730,7 @@ mod tests { state: State::Built, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 4, 29, 33).unwrap(), address: None, + is_next: false, }, Deployment { id: id_1, @@ -695,6 +738,7 @@ mod tests { state: State::Running, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 4, 29, 44).unwrap(), address: None, + is_next: false, }, Deployment { id: id_2, @@ -702,6 +746,7 @@ mod tests { state: State::Running, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 4, 33, 48).unwrap(), address: None, + is_next: true, }, Deployment { id: Uuid::new_v4(), @@ -709,6 +754,7 @@ mod tests { state: State::Crashed, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 4, 38, 52).unwrap(), address: None, + is_next: true, }, Deployment { id: id_3, @@ -716,6 +762,7 @@ mod tests { state: State::Running, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 4, 42, 32).unwrap(), address: None, + is_next: false, }, ] { p.insert_deployment(deployment).await.unwrap(); @@ -729,16 +776,19 @@ mod tests { id: id_1, service_name: "foo".to_string(), service_id: foo_id, + is_next: false, }, DeploymentRunnable { id: id_2, service_name: "bar".to_string(), service_id: bar_id, + is_next: true, }, DeploymentRunnable { id: id_3, service_name: "foo".to_string(), service_id: foo_id, + is_next: false, }, ] ); @@ -830,7 +880,6 @@ mod tests { target: "tests::log_recorder_event".to_string(), fields: json!({"message": "job queued"}), r#type: deploy_layer::LogType::Event, - address: None, }; p.record(event); @@ -865,6 +914,7 @@ mod tests { state: State::Queued, // Should be different from the state recorded below last_update: Utc.with_ymd_and_hms(2022, 4, 29, 2, 39, 39).unwrap(), address: None, + is_next: false, }) .await .unwrap(); @@ -878,7 +928,6 @@ mod tests { target: String::new(), fields: serde_json::Value::Null, r#type: deploy_layer::LogType::State, - address: Some("127.0.0.1:12345".to_string()), }; p.record(state); @@ -904,7 +953,8 @@ mod tests { service_id, state: State::Running, last_update: Utc.with_ymd_and_hms(2022, 4, 29, 2, 39, 59).unwrap(), - address: Some(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 12345)), + address: None, + is_next: false, } ); } @@ -1081,6 +1131,7 @@ mod tests { state: State::Built, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 4, 29, 33).unwrap(), address: None, + is_next: false, }, Deployment { id: Uuid::new_v4(), @@ -1088,6 +1139,7 @@ mod tests { state: State::Stopped, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 4, 29, 44).unwrap(), address: None, + is_next: false, }, Deployment { id: id_1, @@ -1095,6 +1147,7 @@ mod tests { state: State::Running, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 4, 33, 48).unwrap(), address: None, + is_next: false, }, Deployment { id: Uuid::new_v4(), @@ -1102,6 +1155,7 @@ mod tests { state: State::Crashed, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 4, 38, 52).unwrap(), address: None, + is_next: false, }, Deployment { id: id_2, @@ -1109,6 +1163,7 @@ mod tests { state: State::Running, last_update: Utc.with_ymd_and_hms(2022, 4, 25, 4, 42, 32).unwrap(), address: None, + is_next: true, }, ] { p.insert_deployment(deployment).await.unwrap(); diff --git a/deployer/src/persistence/resource/mod.rs b/deployer/src/persistence/resource/mod.rs index 20069563d..36fcff4e4 100644 --- a/deployer/src/persistence/resource/mod.rs +++ b/deployer/src/persistence/resource/mod.rs @@ -25,10 +25,9 @@ pub struct Resource { pub data: serde_json::Value, } -impl From for shuttle_common::models::resource::Response { +impl From for shuttle_common::resource::Response { fn from(resource: Resource) -> Self { - shuttle_common::models::resource::Response { - service_id: resource.service_id, + shuttle_common::resource::Response { r#type: resource.r#type.into(), data: resource.data, } @@ -40,7 +39,7 @@ pub enum Type { Database(DatabaseType), } -impl From for shuttle_common::models::resource::Type { +impl From for shuttle_common::resource::Type { fn from(r#type: Type) -> Self { match r#type { Type::Database(r#type) => Self::Database(r#type.into()), @@ -48,6 +47,14 @@ impl From for shuttle_common::models::resource::Type { } } +impl From for Type { + fn from(r#type: shuttle_common::resource::Type) -> Self { + match r#type { + shuttle_common::resource::Type::Database(r#type) => Self::Database(r#type.into()), + } + } +} + impl Display for Type { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/deployer/src/runtime_manager.rs b/deployer/src/runtime_manager.rs new file mode 100644 index 000000000..80e769ef7 --- /dev/null +++ b/deployer/src/runtime_manager.rs @@ -0,0 +1,178 @@ +use std::{collections::HashMap, path::PathBuf, sync::Arc}; + +use anyhow::Context; +use shuttle_common::claims::{ClaimService, InjectPropagation}; +use shuttle_proto::runtime::{ + self, runtime_client::RuntimeClient, StopRequest, SubscribeLogsRequest, +}; +use tokio::{process, sync::Mutex}; +use tonic::transport::Channel; +use tracing::{debug, info, trace}; +use uuid::Uuid; + +use crate::deployment::deploy_layer; + +const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); + +type Runtimes = Arc< + std::sync::Mutex< + HashMap< + Uuid, + ( + process::Child, + RuntimeClient>>, + ), + >, + >, +>; + +/// Manager that can start up mutliple runtimes. This is needed so that two runtimes can be up when a new deployment is made: +/// One runtime for the new deployment being loaded; another for the currently active deployment +#[derive(Clone)] +pub struct RuntimeManager { + runtimes: Runtimes, + artifacts_path: PathBuf, + provisioner_address: String, + auth_uri: Option, + log_sender: crossbeam_channel::Sender, +} + +impl RuntimeManager { + pub fn new( + artifacts_path: PathBuf, + provisioner_address: String, + auth_uri: Option, + log_sender: crossbeam_channel::Sender, + ) -> Arc> { + Arc::new(Mutex::new(Self { + runtimes: Default::default(), + artifacts_path, + provisioner_address, + auth_uri, + log_sender, + })) + } + + pub async fn get_runtime_client( + &mut self, + id: Uuid, + alpha_runtime_path: Option, + ) -> anyhow::Result>>> { + trace!("making new client"); + + let port = portpicker::pick_unused_port().context("failed to find available port")?; + let is_next = alpha_runtime_path.is_none(); + + let get_runtime_executable = || { + if let Some(alpha_runtime) = alpha_runtime_path { + debug!( + "Starting alpha runtime at: {}", + alpha_runtime + .clone() + .into_os_string() + .into_string() + .unwrap_or_default() + ); + alpha_runtime + } else { + if cfg!(debug_assertions) { + debug!("Installing shuttle-next runtime in debug mode from local source"); + // If we're running deployer natively, install shuttle-runtime using the + // version of runtime from the calling repo. + let path = std::fs::canonicalize(format!("{MANIFEST_DIR}/../runtime")); + + // The path will not be valid if we are in a deployer container, in which + // case we don't try to install and use the one installed in deploy.sh. + if let Ok(path) = path { + std::process::Command::new("cargo") + .arg("install") + .arg("shuttle-runtime") + .arg("--path") + .arg(path) + .arg("--bin") + .arg("shuttle-next") + .arg("--features") + .arg("next") + .output() + .expect("failed to install the local version of shuttle-runtime"); + } + } + + debug!("Returning path to shuttle-next runtime",); + // If we're in a deployer built with the containerfile, the runtime will have + // been installed in deploy.sh. + home::cargo_home() + .expect("failed to find path to cargo home") + .join("bin/shuttle-next") + } + }; + + let (process, runtime_client) = runtime::start( + is_next, + runtime::StorageManagerType::Artifacts(self.artifacts_path.clone()), + &self.provisioner_address, + self.auth_uri.as_ref(), + port, + get_runtime_executable, + ) + .await + .context("failed to start shuttle runtime")?; + + let sender = self.log_sender.clone(); + let mut stream = runtime_client + .clone() + .subscribe_logs(tonic::Request::new(SubscribeLogsRequest {})) + .await + .context("subscribing to runtime logs stream")? + .into_inner(); + + tokio::spawn(async move { + while let Ok(Some(log)) = stream.message().await { + if let Ok(mut log) = deploy_layer::Log::try_from(log) { + log.id = id; + + sender.send(log).expect("to send log to persistence"); + } + } + }); + + self.runtimes + .lock() + .unwrap() + .insert(id, (process, runtime_client.clone())); + + Ok(runtime_client) + } + + /// Send a kill / stop signal for a deployment to its running runtime + pub async fn kill(&mut self, id: &Uuid) -> bool { + let value = self.runtimes.lock().unwrap().remove(id); + + if let Some((mut process, mut runtime_client)) = value { + trace!(%id, "sending stop signal for deployment"); + + let stop_request = tonic::Request::new(StopRequest {}); + let response = runtime_client.stop(stop_request).await.unwrap(); + + trace!(?response, "stop deployment response"); + + let result = response.into_inner().success; + let _ = process.start_kill(); + + result + } else { + trace!("no client running"); + true + } + } +} + +impl Drop for RuntimeManager { + fn drop(&mut self) { + info!("runtime manager shutting down"); + + for (process, _runtime_client) in self.runtimes.lock().unwrap().values_mut() { + let _ = process.start_kill(); + } + } +} diff --git a/deployer/tests/deploy_layer/bind-panic/Cargo.toml b/deployer/tests/deploy_layer/bind-panic/Cargo.toml index 2a06bc95d..2db7c6427 100644 --- a/deployer/tests/deploy_layer/bind-panic/Cargo.toml +++ b/deployer/tests/deploy_layer/bind-panic/Cargo.toml @@ -3,12 +3,10 @@ name = "bind-panic" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] [dependencies] -shuttle-service = "0.11.0" +shuttle-runtime = "0.12.0" +tokio = "1.22" diff --git a/deployer/tests/deploy_layer/bind-panic/src/lib.rs b/deployer/tests/deploy_layer/bind-panic/src/lib.rs deleted file mode 100644 index 1ecd700bf..000000000 --- a/deployer/tests/deploy_layer/bind-panic/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use shuttle_service::Service; - -struct MyService; - -#[shuttle_service::async_trait] -impl Service for MyService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::Error> { - panic!("panic in bind"); - } -} - -#[shuttle_service::main] -async fn bind_panic() -> Result { - Ok(MyService) -} diff --git a/deployer/tests/deploy_layer/bind-panic/src/main.rs b/deployer/tests/deploy_layer/bind-panic/src/main.rs new file mode 100644 index 000000000..d7badcd69 --- /dev/null +++ b/deployer/tests/deploy_layer/bind-panic/src/main.rs @@ -0,0 +1,13 @@ +struct MyService; + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for MyService { + async fn bind(mut self, _: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { + panic!("panic in bind"); + } +} + +#[shuttle_runtime::main] +async fn bind_panic() -> Result { + Ok(MyService) +} diff --git a/deployer/tests/deploy_layer/main-panic/Cargo.toml b/deployer/tests/deploy_layer/main-panic/Cargo.toml index 047878b66..a5274cbb5 100644 --- a/deployer/tests/deploy_layer/main-panic/Cargo.toml +++ b/deployer/tests/deploy_layer/main-panic/Cargo.toml @@ -3,12 +3,10 @@ name = "main-panic" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] [dependencies] -shuttle-service = "0.11.0" +shuttle-runtime = "0.12.0" +tokio = "1.22" diff --git a/deployer/tests/deploy_layer/main-panic/src/lib.rs b/deployer/tests/deploy_layer/main-panic/src/lib.rs deleted file mode 100644 index 438c12540..000000000 --- a/deployer/tests/deploy_layer/main-panic/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use shuttle_service::Service; - -struct MyService; - -#[shuttle_service::async_trait] -impl Service for MyService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::Error> { - Ok(()) - } -} - -#[shuttle_service::main] -async fn main_panic() -> Result { - panic!("main panic") -} diff --git a/deployer/tests/deploy_layer/main-panic/src/main.rs b/deployer/tests/deploy_layer/main-panic/src/main.rs new file mode 100644 index 000000000..fcb8bce0b --- /dev/null +++ b/deployer/tests/deploy_layer/main-panic/src/main.rs @@ -0,0 +1,13 @@ +struct MyService; + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for MyService { + async fn bind(mut self, _: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { + Ok(()) + } +} + +#[shuttle_runtime::main] +async fn main_panic() -> Result { + panic!("main panic") +} diff --git a/deployer/tests/deploy_layer/self-stop/Cargo.toml b/deployer/tests/deploy_layer/self-stop/Cargo.toml index 1e7b037ae..8d97cf0b0 100644 --- a/deployer/tests/deploy_layer/self-stop/Cargo.toml +++ b/deployer/tests/deploy_layer/self-stop/Cargo.toml @@ -3,12 +3,10 @@ name = "self-stop" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] [dependencies] -shuttle-service = "0.11.0" +shuttle-runtime = "0.12.0" +tokio = "1.22" diff --git a/deployer/tests/deploy_layer/self-stop/src/lib.rs b/deployer/tests/deploy_layer/self-stop/src/lib.rs deleted file mode 100644 index 46558244a..000000000 --- a/deployer/tests/deploy_layer/self-stop/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use shuttle_service::Service; - -struct MyService; - -#[shuttle_service::async_trait] -impl Service for MyService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::error::Error> { - Ok(()) - } -} - -#[shuttle_service::main] -async fn self_stop() -> Result { - Ok(MyService) -} diff --git a/deployer/tests/deploy_layer/self-stop/src/main.rs b/deployer/tests/deploy_layer/self-stop/src/main.rs new file mode 100644 index 000000000..ddbba66d2 --- /dev/null +++ b/deployer/tests/deploy_layer/self-stop/src/main.rs @@ -0,0 +1,13 @@ +struct MyService; + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for MyService { + async fn bind(mut self, _: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { + Ok(()) + } +} + +#[shuttle_runtime::main] +async fn self_stop() -> Result { + Ok(MyService) +} diff --git a/deployer/tests/deploy_layer/sleep-async/Cargo.toml b/deployer/tests/deploy_layer/sleep-async/Cargo.toml index d1f5d1a50..f88136e11 100644 --- a/deployer/tests/deploy_layer/sleep-async/Cargo.toml +++ b/deployer/tests/deploy_layer/sleep-async/Cargo.toml @@ -3,13 +3,10 @@ name = "sleep-async" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] [dependencies] +shuttle-runtime = "0.12.0" tokio = { version = "1.0", features = ["time"]} -shuttle-service = "0.11.0" diff --git a/deployer/tests/resources/sleep-async/src/lib.rs b/deployer/tests/deploy_layer/sleep-async/src/main.rs similarity index 50% rename from deployer/tests/resources/sleep-async/src/lib.rs rename to deployer/tests/deploy_layer/sleep-async/src/main.rs index 542694709..aead71b2d 100644 --- a/deployer/tests/resources/sleep-async/src/lib.rs +++ b/deployer/tests/deploy_layer/sleep-async/src/main.rs @@ -1,23 +1,20 @@ use std::time::Duration; -use shuttle_service::Service; +use shuttle_runtime::Service; use tokio::time::sleep; struct SleepService { duration: u64, } -#[shuttle_service::main] -async fn simple() -> Result { +#[shuttle_runtime::main] +async fn simple() -> Result { Ok(SleepService { duration: 4 }) } -#[shuttle_service::async_trait] +#[shuttle_runtime::async_trait] impl Service for SleepService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::error::Error> { + async fn bind(mut self, _: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { let duration = Duration::from_secs(self.duration); sleep(duration).await; diff --git a/deployer/tests/resources/bind-panic/Cargo.toml b/deployer/tests/resources/bind-panic/Cargo.toml index 0a721bd4a..d20e3c071 100644 --- a/deployer/tests/resources/bind-panic/Cargo.toml +++ b/deployer/tests/resources/bind-panic/Cargo.toml @@ -3,12 +3,11 @@ name = "bind-panic" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] [dependencies] -shuttle-service = { path = "../../../../service" } +shuttle-runtime = { path = "../../../../runtime" } +tokio = "1.22" diff --git a/deployer/tests/resources/bind-panic/src/lib.rs b/deployer/tests/resources/bind-panic/src/lib.rs deleted file mode 100644 index 1ecd700bf..000000000 --- a/deployer/tests/resources/bind-panic/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use shuttle_service::Service; - -struct MyService; - -#[shuttle_service::async_trait] -impl Service for MyService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::Error> { - panic!("panic in bind"); - } -} - -#[shuttle_service::main] -async fn bind_panic() -> Result { - Ok(MyService) -} diff --git a/deployer/tests/resources/bind-panic/src/main.rs b/deployer/tests/resources/bind-panic/src/main.rs new file mode 100644 index 000000000..d7badcd69 --- /dev/null +++ b/deployer/tests/resources/bind-panic/src/main.rs @@ -0,0 +1,13 @@ +struct MyService; + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for MyService { + async fn bind(mut self, _: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { + panic!("panic in bind"); + } +} + +#[shuttle_runtime::main] +async fn bind_panic() -> Result { + Ok(MyService) +} diff --git a/deployer/tests/resources/main-panic/Cargo.toml b/deployer/tests/resources/main-panic/Cargo.toml index da7b5c841..9e068f31d 100644 --- a/deployer/tests/resources/main-panic/Cargo.toml +++ b/deployer/tests/resources/main-panic/Cargo.toml @@ -3,12 +3,10 @@ name = "main-panic" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] [dependencies] -shuttle-service = { path = "../../../../service" } +shuttle-runtime = { path = "../../../../runtime" } +tokio = "1.22" diff --git a/deployer/tests/resources/main-panic/src/lib.rs b/deployer/tests/resources/main-panic/src/lib.rs deleted file mode 100644 index 438c12540..000000000 --- a/deployer/tests/resources/main-panic/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use shuttle_service::Service; - -struct MyService; - -#[shuttle_service::async_trait] -impl Service for MyService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::Error> { - Ok(()) - } -} - -#[shuttle_service::main] -async fn main_panic() -> Result { - panic!("main panic") -} diff --git a/deployer/tests/resources/main-panic/src/main.rs b/deployer/tests/resources/main-panic/src/main.rs new file mode 100644 index 000000000..fcb8bce0b --- /dev/null +++ b/deployer/tests/resources/main-panic/src/main.rs @@ -0,0 +1,13 @@ +struct MyService; + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for MyService { + async fn bind(mut self, _: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { + Ok(()) + } +} + +#[shuttle_runtime::main] +async fn main_panic() -> Result { + panic!("main panic") +} diff --git a/deployer/tests/resources/sleep-async/Cargo.toml b/deployer/tests/resources/sleep-async/Cargo.toml index c0dc45479..edd2ea6bc 100644 --- a/deployer/tests/resources/sleep-async/Cargo.toml +++ b/deployer/tests/resources/sleep-async/Cargo.toml @@ -3,13 +3,10 @@ name = "sleep-async" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] [dependencies] +shuttle-runtime = { path = "../../../../runtime" } tokio = { version = "1.0", features = ["time"]} -shuttle-service = { path = "../../../../service" } diff --git a/deployer/tests/deploy_layer/sleep-async/src/lib.rs b/deployer/tests/resources/sleep-async/src/main.rs similarity index 50% rename from deployer/tests/deploy_layer/sleep-async/src/lib.rs rename to deployer/tests/resources/sleep-async/src/main.rs index 542694709..aead71b2d 100644 --- a/deployer/tests/deploy_layer/sleep-async/src/lib.rs +++ b/deployer/tests/resources/sleep-async/src/main.rs @@ -1,23 +1,20 @@ use std::time::Duration; -use shuttle_service::Service; +use shuttle_runtime::Service; use tokio::time::sleep; struct SleepService { duration: u64, } -#[shuttle_service::main] -async fn simple() -> Result { +#[shuttle_runtime::main] +async fn simple() -> Result { Ok(SleepService { duration: 4 }) } -#[shuttle_service::async_trait] +#[shuttle_runtime::async_trait] impl Service for SleepService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::error::Error> { + async fn bind(mut self, _: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { let duration = Duration::from_secs(self.duration); sleep(duration).await; diff --git a/deployer/tests/resources/tests-fail/Cargo.toml b/deployer/tests/resources/tests-fail/Cargo.toml index 43b6ff357..12a8c2a5d 100644 --- a/deployer/tests/resources/tests-fail/Cargo.toml +++ b/deployer/tests/resources/tests-fail/Cargo.toml @@ -3,11 +3,10 @@ name = "tests-fail" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] - [workspace] [dependencies] rocket = "0.5.0-rc.2" -shuttle-service = { path = "../../../../service", features = ["web-rocket"] } +shuttle-rocket = { path = "../../../../services/shuttle-rocket"} +shuttle-runtime = { path = "../../../../runtime" } +tokio = "1.22" diff --git a/deployer/tests/resources/tests-fail/src/lib.rs b/deployer/tests/resources/tests-fail/src/main.rs similarity index 72% rename from deployer/tests/resources/tests-fail/src/lib.rs rename to deployer/tests/resources/tests-fail/src/main.rs index 85efb0bd3..ccb32383c 100644 --- a/deployer/tests/resources/tests-fail/src/lib.rs +++ b/deployer/tests/resources/tests-fail/src/main.rs @@ -6,10 +6,10 @@ fn index() -> &'static str { "Hello, world!" } -#[shuttle_service::main] -async fn rocket() -> shuttle_service::ShuttleRocket { +#[shuttle_runtime::main] +async fn rocket() -> shuttle_rocket::ShuttleRocket { let rocket = rocket::build().mount("/hello", routes![index]); - Ok(rocket) + Ok(rocket.into()) } #[cfg(test)] diff --git a/deployer/tests/resources/tests-pass/Cargo.toml b/deployer/tests/resources/tests-pass/Cargo.toml index 43e898a65..e9d19b127 100644 --- a/deployer/tests/resources/tests-pass/Cargo.toml +++ b/deployer/tests/resources/tests-pass/Cargo.toml @@ -3,11 +3,10 @@ name = "tests-pass" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] - [workspace] [dependencies] rocket = "0.5.0-rc.2" -shuttle-service = { path = "../../../../service", features = ["web-rocket"] } +shuttle-rocket = { path = "../../../../services/shuttle-rocket"} +shuttle-runtime = { path = "../../../../runtime" } +tokio = "1.22" diff --git a/deployer/tests/resources/tests-pass/src/lib.rs b/deployer/tests/resources/tests-pass/src/main.rs similarity index 74% rename from deployer/tests/resources/tests-pass/src/lib.rs rename to deployer/tests/resources/tests-pass/src/main.rs index 2f4525283..08e124e91 100644 --- a/deployer/tests/resources/tests-pass/src/lib.rs +++ b/deployer/tests/resources/tests-pass/src/main.rs @@ -6,10 +6,10 @@ fn index() -> &'static str { "Hello, world!" } -#[shuttle_service::main] -async fn rocket() -> shuttle_service::ShuttleRocket { +#[shuttle_runtime::main] +async fn rocket() -> shuttle_rocket::ShuttleRocket { let rocket = rocket::build().mount("/hello", routes![index]); - Ok(rocket) + Ok(rocket.into()) } #[cfg(test)] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index ea1ac7eb2..c1f95de6b 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -23,3 +23,9 @@ services: exec /usr/local/bin/service "$${@:0}" ports: - 5000:8000 + panamax: + profiles: + - panamax + deck-chores: + profiles: + - panamax diff --git a/docker-compose.yml b/docker-compose.yml index 6fffc6dec..85ef0e204 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -173,8 +173,6 @@ services: constraints: - node.hostname==controller panamax: - profiles: - - panamax image: "${CONTAINER_REGISTRY}/panamax:${PANAMAX_TAG}" restart: always networks: @@ -191,8 +189,6 @@ services: constraints: - node.hostname==controller deck-chores: - profiles: - - panamax image: funkyfuture/deck-chores:1 restart: unless-stopped environment: diff --git a/e2e/tests/integration/helpers/mod.rs b/e2e/tests/integration/helpers/mod.rs index 45a4e1dab..fe6e1e6c1 100644 --- a/e2e/tests/integration/helpers/mod.rs +++ b/e2e/tests/integration/helpers/mod.rs @@ -38,13 +38,29 @@ impl TempCargoHome { write!( config, r#"[patch.crates-io] -shuttle-service = {{ path = "{}" }} -shuttle-aws-rds = {{ path = "{}" }} -shuttle-persist = {{ path = "{}" }} -shuttle-shared-db = {{ path = "{}" }} -shuttle-secrets = {{ path = "{}" }} -shuttle-static-folder = {{ path = "{}" }}"#, +shuttle-service = { path = "{}" } +shuttle-runtime = { path = "{}" } + +shuttle-aws-rds = { path = "{}" } +shuttle-persist = { path = "{}" } +shuttle-shared-db = { path = "{}" } +shuttle-secrets = { path = "{}" } +shuttle-static-folder = { path = "{}" } + +shuttle-axum = { path = "{}" } +shuttle-actix-web = { path = "{}" } +shuttle-next = { path = "{}" } +shuttle-poem = { path = "{}" } +shuttle-poise = { path = "{}" } +shuttle-rocket = { path = "{}" } +shuttle-salvo = { path = "{}" } +shuttle-serenity = { path = "{}" } +shuttle-thruster = { path = "{}" } +shuttle-tide = { path = "{}" } +shuttle-tower = { path = "{}" } +shuttle-warp = { path = "{}" }"#, WORKSPACE_ROOT.join("service").display(), + WORKSPACE_ROOT.join("runtime").display(), WORKSPACE_ROOT.join("resources").join("aws-rds").display(), WORKSPACE_ROOT.join("resources").join("persist").display(), WORKSPACE_ROOT.join("resources").join("shared-db").display(), @@ -53,6 +69,54 @@ shuttle-static-folder = {{ path = "{}" }}"#, .join("resources") .join("static-folder") .display(), + WORKSPACE_ROOT + .join("services") + .join("shuttle-axum") + .display(), + WORKSPACE_ROOT + .join("services") + .join("shuttle-actix-web") + .display(), + WORKSPACE_ROOT + .join("services") + .join("shuttle-next") + .display(), + WORKSPACE_ROOT + .join("services") + .join("shuttle-poem") + .display(), + WORKSPACE_ROOT + .join("services") + .join("shuttle-poise") + .display(), + WORKSPACE_ROOT + .join("services") + .join("shuttle-rocket") + .display(), + WORKSPACE_ROOT + .join("services") + .join("shuttle-salvo") + .display(), + WORKSPACE_ROOT + .join("services") + .join("shuttle-serenity") + .display(), + WORKSPACE_ROOT + .join("services") + .join("shuttle-thruster") + .display(), + WORKSPACE_ROOT + .join("services") + .join("shuttle-tide") + .display(), + WORKSPACE_ROOT + .join("services") + .join("shuttle-tower") + .display(), + WORKSPACE_ROOT + .join("services") + .join("shuttle-warp") + .display(), ) .unwrap(); diff --git a/examples b/examples index a5c78703a..1f6cbf93c 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit a5c78703ab676bf7ed1649ef19cb4bfe43c5cc29 +Subproject commit 1f6cbf93ca99676a54e937cb9e2ff88b7e7f9d4e diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index de6267b5a..a6a1ec7e9 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-gateway" -version = "0.11.2" +version = "0.12.0" edition.workspace = true license.workspace = true publish = false @@ -8,7 +8,7 @@ publish = false [dependencies] acme2 = "0.5.1" async-trait = { workspace = true } -axum = { workspace = true, features = [ "headers" ] } +axum = { workspace = true, features = [ "default", "headers" ] } axum-server = { version = "0.4.4", features = [ "tls-rustls" ] } base64 = "0.13.1" bollard = "0.13.0" @@ -38,9 +38,10 @@ sqlx = { version = "0.6.2", features = [ "sqlite", "json", "runtime-tokio-native strum = { workspace = true } tokio = { version = "1.22.0", features = [ "full" ] } tower = { workspace = true, features = [ "steer" ] } -tracing = { workspace = true } +tower-http = { version = "0.3.4", features = ["trace"] } +tracing = { workspace = true, features = ["default"] } tracing-opentelemetry = { workspace = true } -tracing-subscriber = { workspace = true } +tracing-subscriber = { workspace = true, features = ["default", "env-filter"] } ttl_cache = { workspace = true } uuid = { workspace = true, features = [ "v4" ] } diff --git a/gateway/src/api/auth_layer.rs b/gateway/src/api/auth_layer.rs index 0e08d2587..b96b3a1a3 100644 --- a/gateway/src/api/auth_layer.rs +++ b/gateway/src/api/auth_layer.rs @@ -5,7 +5,6 @@ use axum::{ headers::{authorization::Bearer, Authorization, Cookie, Header, HeaderMapExt}, response::Response, }; -use chrono::{TimeZone, Utc}; use futures::future::BoxFuture; use http::{Request, StatusCode, Uri}; use hyper::{ @@ -24,6 +23,11 @@ use tracing_opentelemetry::OpenTelemetrySpanExt; static PROXY_CLIENT: Lazy>> = Lazy::new(|| ReverseProxy::new(Client::new())); +/// Time to cache tokens for. Currently tokens take 15 minutes to expire (see [EXP_MINUTES]) which leaves a 10 minutes +/// buffer (EXP_MINUTES - CACHE_MINUTES). We want the buffer to be atleast as long as the longest builds which has +/// been observed to be around 5 minutes. +const CACHE_MINUTES: u64 = 5; + /// The idea of this layer is to do two things: /// 1. Forward all user related routes (`/login`, `/logout`, `/users/*`, etc) to our auth service /// 2. Upgrade all Authorization Bearer keys and session cookies to JWT tokens for internal @@ -247,25 +251,11 @@ where } }; - match extract_token_expiration(response.token.clone()) { - Ok(expiration) => { - // Cache the token. - this.cache_manager.insert( - key.as_str(), - response.token.clone(), - expiration, - ); - } - Err(status) => { - error!( - "failed to extract token expiration before inserting into cache" - ); - return Ok(Response::builder() - .status(status) - .body(boxed(Body::empty())) - .unwrap()); - } - }; + this.cache_manager.insert( + key.as_str(), + response.token.clone(), + Duration::from_secs(CACHE_MINUTES * 60), + ); trace!("token inserted in cache, request proceeding"); req.headers_mut() @@ -290,46 +280,6 @@ where } } -fn extract_token_expiration(token: String) -> Result { - let (_header, rest) = token - .split_once('.') - .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; - - let (claim, _sig) = rest - .split_once('.') - .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; - - let claim = base64::decode_config(claim, base64::URL_SAFE_NO_PAD) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let claim: serde_json::Map = - serde_json::from_slice(&claim).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let exp = claim["exp"] - .as_i64() - .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; - - let expiration_timestamp = Utc - .timestamp_opt(exp, 0) - .single() - .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; - - let duration = expiration_timestamp - Utc::now(); - - // We will use this duration to set the TTL for the JWT in the cache. We subtract 180 seconds - // to make sure a token from the cache will still be valid in cases where it will be used to - // authorize some operation, the operation takes some time, and then the token needs to be - // used again. - // - // This number should never be negative since the JWT has just been created, and so should be - // safe to cast to u64. However, if the number *is* negative it would wrap and the TTL duration - // would be near u64::MAX, so we use try_from to ensure that can't happen. - let duration_minus_buffer = u64::try_from(duration.num_seconds() - 180) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - Ok(std::time::Duration::from_secs(duration_minus_buffer)) -} - fn make_token_request(uri: &str, header: impl Header) -> Request { let mut token_request = Request::builder().uri(uri); token_request diff --git a/gateway/src/api/latest.rs b/gateway/src/api/latest.rs index 2b64bc4fb..42f1ff3a6 100644 --- a/gateway/src/api/latest.rs +++ b/gateway/src/api/latest.rs @@ -16,11 +16,10 @@ use futures::Future; use http::{StatusCode, Uri}; use instant_acme::{AccountCredentials, ChallengeType}; use serde::{Deserialize, Serialize}; -use shuttle_common::backends::auth::{ - AuthPublicKey, JwtAuthenticationLayer, Scope, ScopedLayer, EXP_MINUTES, -}; +use shuttle_common::backends::auth::{AuthPublicKey, JwtAuthenticationLayer, ScopedLayer}; use shuttle_common::backends::cache::CacheManager; use shuttle_common::backends::metrics::{Metrics, TraceLayer}; +use shuttle_common::claims::{Scope, EXP_MINUTES}; use shuttle_common::models::error::ErrorKind; use shuttle_common::models::{project, stats}; use shuttle_common::request_span; @@ -593,7 +592,8 @@ pub mod tests { Request::builder() .method("POST") .uri(format!("/projects/{project}")) - .body(Body::empty()) + .header("Content-Type", "application/json") + .body("{\"idle_minutes\": 3}".into()) .unwrap() }; @@ -762,7 +762,8 @@ pub mod tests { let create_project = Request::builder() .method("POST") .uri(format!("/projects/{matrix}")) - .body(Body::empty()) + .header("Content-Type", "application/json") + .body("{\"idle_minutes\": 3}".into()) .unwrap() .with_header(&authorization); diff --git a/gateway/src/auth.rs b/gateway/src/auth.rs index 679890b0c..0c9910aa9 100644 --- a/gateway/src/auth.rs +++ b/gateway/src/auth.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use axum::extract::{FromRef, FromRequestParts, Path}; use axum::http::request::Parts; use serde::{Deserialize, Serialize}; -use shuttle_common::backends::auth::{Claim, Scope}; +use shuttle_common::claims::{Claim, Scope}; use tracing::{trace, Span}; use crate::api::latest::RouterState; diff --git a/gateway/src/lib.rs b/gateway/src/lib.rs index b1a6e6175..000700142 100644 --- a/gateway/src/lib.rs +++ b/gateway/src/lib.rs @@ -344,7 +344,8 @@ pub mod tests { use jsonwebtoken::EncodingKey; use rand::distributions::{Alphanumeric, DistString, Distribution, Uniform}; use ring::signature::{self, Ed25519KeyPair, KeyPair}; - use shuttle_common::backends::auth::{Claim, ConvertResponse, Scope}; + use shuttle_common::backends::auth::ConvertResponse; + use shuttle_common::claims::{Claim, Scope}; use shuttle_common::models::project; use sqlx::SqlitePool; use tokio::sync::mpsc::channel; @@ -801,7 +802,8 @@ pub mod tests { .request( Request::post("/projects/matrix") .with_header(&authorization) - .body(Body::empty()) + .header("Content-Type", "application/json") + .body("{\"idle_minutes\": 3}".into()) .unwrap(), ) .map_ok(|resp| { diff --git a/gateway/src/project.rs b/gateway/src/project.rs index da6dc12c7..54e955935 100644 --- a/gateway/src/project.rs +++ b/gateway/src/project.rs @@ -704,9 +704,7 @@ impl ProjectCreating { "--api-address", format!("0.0.0.0:{RUNTIME_API_PORT}"), "--provisioner-address", - provisioner_host, - "--provisioner-port", - "8000", + format!("http://{provisioner_host}:8000"), "--proxy-address", "0.0.0.0:8000", "--proxy-fqdn", @@ -719,7 +717,7 @@ impl ProjectCreating { auth_uri, ], "Env": [ - "RUST_LOG=debug,shuttle=trace", + "RUST_LOG=debug,shuttle=trace,h2=warn", ] }) }); @@ -1321,7 +1319,7 @@ where // Stopping a docker containers sends a SIGTERM which will stop the tokio runtime that deployer starts up. // Killing this runtime causes the deployment to enter the `completed` state and it therefore does not // start up again when starting up the project's container. Luckily the kill command allows us to change the - // signal to prevent this from happenning. + // signal to prevent this from happening. // // In some future state when all deployers hadle `SIGTERM` correctly, this can be changed to docker stop // safely. diff --git a/gateway/tests/hello_world.crate b/gateway/tests/hello_world.crate index ab7233965..a07f34d97 100644 Binary files a/gateway/tests/hello_world.crate and b/gateway/tests/hello_world.crate differ diff --git a/proto/Cargo.toml b/proto/Cargo.toml index e0474ee67..2a03c17b6 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -3,16 +3,24 @@ name = "shuttle-proto" version.workspace = true edition.workspace = true license.workspace = true -publish = false +description = "Library for all the gRPC definitions used by shuttle" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = { workspace = true } +chrono = { workspace = true } +home = "0.5.4" prost = "0.11.2" -tonic = "0.8.3" +prost-types = { workspace = true } +tokio = { version = "1.22.0", features = ["process"] } +tonic = { workspace = true } +tower = { workspace = true } +tracing = { workspace = true } [dependencies.shuttle-common] workspace = true -features = ["models"] +features = ["claims", "error", "service", "wasm"] [build-dependencies] tonic-build = "0.8.3" diff --git a/proto/build.rs b/proto/build.rs index 1502854e9..685a01573 100644 --- a/proto/build.rs +++ b/proto/build.rs @@ -1,5 +1,5 @@ fn main() -> Result<(), Box> { - tonic_build::compile_protos("../proto/provisioner.proto")?; + tonic_build::configure().compile(&["./provisioner.proto", "./runtime.proto"], &["./"])?; Ok(()) } diff --git a/proto/runtime.proto b/proto/runtime.proto new file mode 100644 index 000000000..2dee8806a --- /dev/null +++ b/proto/runtime.proto @@ -0,0 +1,101 @@ +syntax = "proto3"; +package runtime; + +import "google/protobuf/timestamp.proto"; + +service Runtime { + // Load a service file to be ready to start it + rpc Load(LoadRequest) returns (LoadResponse); + + // Start a loaded service file + rpc Start(StartRequest) returns (StartResponse); + + // Stop a started service + rpc Stop(StopRequest) returns (StopResponse); + + // Channel to notify a service has been stopped + rpc SubscribeStop(SubscribeStopRequest) returns (stream SubscribeStopResponse); + + // Subscribe to runtime logs + rpc SubscribeLogs(SubscribeLogsRequest) returns (stream LogItem); +} + +message LoadRequest { + // Name of service to load + string service_name = 1; + + // Path to compiled file to load for service + string path = 2; + + // A cache of resource details to use instead when asked + repeated bytes resources = 10; + + // Secrets that belong to this deployment + map secrets = 20; +} + +message LoadResponse { + // Could the service be loaded + bool success = 1; + // Error message if not successful + string message = 2; + // Which resources where requested + repeated bytes resources = 10; +} + +message StartRequest { + // Address and port to start the service on + string ip = 1; +} + +message StartResponse { + // Was the start successful + bool success = 1; +} + +message StopRequest {} + +message StopResponse { + // Was the stop successful + bool success = 1; +} + +message SubscribeStopRequest {} + +message SubscribeStopResponse { + // Reason the service has stopped + StopReason reason = 1; + + // Any extra message to go with the reason. If there are any + string message = 2; +} + +enum StopReason { + // User requested this stop + Request = 0; + + // Service stopped by itself + End = 1; + + // Service crashed + Crash = 2; +} + +message SubscribeLogsRequest {} + +message LogItem { + google.protobuf.Timestamp timestamp = 2; + LogLevel level = 4; + optional string file = 5; + optional uint32 line = 6; + string target = 7; + bytes fields = 8; +} + +enum LogLevel { + Trace = 0; + Debug = 1; + Info = 2; + Warn = 3; + Error = 4; +} diff --git a/proto/src/lib.rs b/proto/src/lib.rs index 39e7c501a..6f8602591 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -1,8 +1,8 @@ -pub mod provisioner { - // This clippy is disabled as per this prost comment - // https://github.com/tokio-rs/prost/issues/661#issuecomment-1156606409 - #![allow(clippy::derive_partial_eq_without_eq)] +// This clippy is disabled as per this prost comment +// https://github.com/tokio-rs/prost/issues/661#issuecomment-1156606409 +#![allow(clippy::derive_partial_eq_without_eq)] +pub mod provisioner { use std::fmt::Display; use shuttle_common::{ @@ -53,6 +53,36 @@ pub mod provisioner { } } + impl From for Option { + fn from(db_type: database_request::DbType) -> Self { + match db_type { + database_request::DbType::Shared(Shared { + engine: Some(engine), + }) => match engine { + shared::Engine::Postgres(_) => { + Some(database::Type::Shared(SharedEngine::Postgres)) + } + shared::Engine::Mongodb(_) => { + Some(database::Type::Shared(SharedEngine::MongoDb)) + } + }, + database_request::DbType::AwsRds(AwsRds { + engine: Some(engine), + }) => match engine { + aws_rds::Engine::Postgres(_) => { + Some(database::Type::AwsRds(AwsRdsEngine::Postgres)) + } + aws_rds::Engine::Mysql(_) => Some(database::Type::AwsRds(AwsRdsEngine::MySql)), + aws_rds::Engine::Mariadb(_) => { + Some(database::Type::AwsRds(AwsRdsEngine::MariaDB)) + } + }, + database_request::DbType::Shared(Shared { engine: None }) + | database_request::DbType::AwsRds(AwsRds { engine: None }) => None, + } + } + } + impl Display for aws_rds::Engine { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -63,3 +93,181 @@ pub mod provisioner { } } } + +pub mod runtime { + use std::{ + convert::TryFrom, + path::PathBuf, + time::{Duration, SystemTime}, + }; + + use anyhow::Context; + use chrono::DateTime; + use prost_types::Timestamp; + use shuttle_common::{ + claims::{ClaimLayer, ClaimService, InjectPropagation, InjectPropagationLayer}, + ParseError, + }; + use tokio::process; + use tonic::transport::{Channel, Endpoint}; + use tower::ServiceBuilder; + use tracing::info; + + pub enum StorageManagerType { + Artifacts(PathBuf), + WorkingDir(PathBuf), + } + + tonic::include_proto!("runtime"); + + impl From for LogLevel { + fn from(level: shuttle_common::log::Level) -> Self { + match level { + shuttle_common::log::Level::Trace => Self::Trace, + shuttle_common::log::Level::Debug => Self::Debug, + shuttle_common::log::Level::Info => Self::Info, + shuttle_common::log::Level::Warn => Self::Warn, + shuttle_common::log::Level::Error => Self::Error, + } + } + } + + impl TryFrom for shuttle_common::LogItem { + type Error = ParseError; + + fn try_from(log: LogItem) -> Result { + Ok(Self { + id: Default::default(), + timestamp: DateTime::from(SystemTime::try_from(log.timestamp.unwrap_or_default())?), + state: shuttle_common::deployment::State::Running, + level: LogLevel::from_i32(log.level).unwrap_or_default().into(), + file: log.file, + line: log.line, + target: log.target, + fields: log.fields, + }) + } + } + + impl From for shuttle_common::log::Level { + fn from(level: LogLevel) -> Self { + match level { + LogLevel::Trace => Self::Trace, + LogLevel::Debug => Self::Debug, + LogLevel::Info => Self::Info, + LogLevel::Warn => Self::Warn, + LogLevel::Error => Self::Error, + } + } + } + + impl From for LogItem { + fn from(log: shuttle_common::wasm::Log) -> Self { + let file = if log.file.is_empty() { + None + } else { + Some(log.file) + }; + + let line = if log.line == 0 { None } else { Some(log.line) }; + + Self { + timestamp: Some(Timestamp::from(SystemTime::from(log.timestamp))), + level: LogLevel::from(log.level) as i32, + file, + line, + target: log.target, + fields: log.fields, + } + } + } + + impl From for LogLevel { + fn from(level: shuttle_common::wasm::Level) -> Self { + match level { + shuttle_common::wasm::Level::Trace => Self::Trace, + shuttle_common::wasm::Level::Debug => Self::Debug, + shuttle_common::wasm::Level::Info => Self::Info, + shuttle_common::wasm::Level::Warn => Self::Warn, + shuttle_common::wasm::Level::Error => Self::Error, + } + } + } + + impl From<&tracing::Level> for LogLevel { + fn from(level: &tracing::Level) -> Self { + match *level { + tracing::Level::TRACE => Self::Trace, + tracing::Level::DEBUG => Self::Debug, + tracing::Level::INFO => Self::Info, + tracing::Level::WARN => Self::Warn, + tracing::Level::ERROR => Self::Error, + } + } + } + + pub async fn start( + wasm: bool, + storage_manager_type: StorageManagerType, + provisioner_address: &str, + auth_uri: Option<&String>, + port: u16, + get_runtime_executable: impl FnOnce() -> PathBuf, + ) -> anyhow::Result<( + process::Child, + runtime_client::RuntimeClient>>, + )> { + let (storage_manager_type, storage_manager_path) = match storage_manager_type { + StorageManagerType::Artifacts(path) => ("artifacts", path), + StorageManagerType::WorkingDir(path) => ("working-dir", path), + }; + + let port = &port.to_string(); + let storage_manager_path = &storage_manager_path.display().to_string(); + let runtime_executable_path = get_runtime_executable(); + + let args = if wasm { + vec!["--port", port] + } else { + let mut args = vec![ + "--port", + port, + "--provisioner-address", + provisioner_address, + "--storage-manager-type", + storage_manager_type, + "--storage-manager-path", + storage_manager_path, + ]; + + if let Some(auth_uri) = auth_uri { + args.append(&mut vec!["--auth-uri", auth_uri]); + } + + args + }; + + let runtime = process::Command::new(runtime_executable_path) + .args(&args) + .spawn() + .context("spawning runtime process")?; + + // Sleep because the timeout below does not seem to work + // TODO: investigate why + tokio::time::sleep(Duration::from_secs(2)).await; + + info!("connecting runtime client"); + let conn = Endpoint::new(format!("http://127.0.0.1:{port}")) + .context("creating runtime client endpoint")? + .connect_timeout(Duration::from_secs(5)); + + let channel = conn.connect().await.context("connecting runtime client")?; + let channel = ServiceBuilder::new() + .layer(ClaimLayer) + .layer(InjectPropagationLayer) + .service(channel); + let runtime_client = runtime_client::RuntimeClient::new(channel); + + Ok((runtime, runtime_client)) + } +} diff --git a/provisioner/Cargo.toml b/provisioner/Cargo.toml index 794a19f02..f183d283c 100644 --- a/provisioner/Cargo.toml +++ b/provisioner/Cargo.toml @@ -21,9 +21,9 @@ sqlx = { version = "0.6.2", features = [ ] } thiserror = { workspace = true } tokio = { version = "1.22.0", features = ["macros", "rt-multi-thread"] } -tonic = "0.8.3" -tracing = { workspace = true } -tracing-subscriber = { workspace = true } +tonic = { workspace = true } +tracing = { workspace = true, features = ["default"] } +tracing-subscriber = { workspace = true, features = ["default", "fmt"] } [dependencies.shuttle-common] workspace = true diff --git a/provisioner/src/lib.rs b/provisioner/src/lib.rs index 7c732149a..cfa9ebdc8 100644 --- a/provisioner/src/lib.rs +++ b/provisioner/src/lib.rs @@ -6,7 +6,7 @@ use aws_sdk_rds::{error::ModifyDBInstanceErrorKind, model::DbInstance, types::Sd pub use error::Error; use mongodb::{bson::doc, options::ClientOptions}; use rand::Rng; -use shuttle_common::backends::auth::{Claim, Scope}; +use shuttle_common::claims::{Claim, Scope}; pub use shuttle_proto::provisioner::provisioner_server::ProvisionerServer; use shuttle_proto::provisioner::{ aws_rds, database_request::DbType, shared, AwsRds, DatabaseRequest, DatabaseResponse, Shared, diff --git a/provisioner/tests/provisioner.rs b/provisioner/tests/provisioner.rs index 5b2a7a912..3c9d103d1 100644 --- a/provisioner/tests/provisioner.rs +++ b/provisioner/tests/provisioner.rs @@ -168,7 +168,7 @@ async fn shared_mongodb_role_does_not_exist() { .unwrap(); let user = exec_mongosh("db.getUser(\"user-not_exist\")", Some("mongodb-not_exist")); - assert_eq!(user, "null"); + assert_eq!(user, ""); provisioner .request_shared_db("not_exist", shared::Engine::Mongodb(String::new())) diff --git a/resources/aws-rds/Cargo.toml b/resources/aws-rds/Cargo.toml index 692b7823b..0a5ab226d 100644 --- a/resources/aws-rds/Cargo.toml +++ b/resources/aws-rds/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-aws-rds" -version = "0.11.0" +version = "0.12.0" edition = "2021" license = "Apache-2.0" description = "Plugin to provision AWS RDS resources" @@ -10,9 +10,8 @@ keywords = ["shuttle-service", "rds"] [dependencies] async-trait = "0.1.56" paste = "1.0.7" -shuttle-service = { path = "../../service", version = "0.11.0", default-features = false } +shuttle-service = { path = "../../service", version = "0.12.0", default-features = false } sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls"] } -tokio = { version = "1.19.2", features = ["rt"] } [features] postgres = ["sqlx/postgres"] diff --git a/resources/aws-rds/src/lib.rs b/resources/aws-rds/src/lib.rs index de7090798..16b00212a 100644 --- a/resources/aws-rds/src/lib.rs +++ b/resources/aws-rds/src/lib.rs @@ -7,7 +7,6 @@ use shuttle_service::{ error::CustomError, Factory, ResourceBuilder, }; -use tokio::runtime::Runtime; macro_rules! aws_engine { ($feature:expr, $pool_path:path, $options_path:path, $struct_ident:ident) => { @@ -26,7 +25,7 @@ macro_rules! aws_engine { Self { local_uri: None } } - async fn build(self, factory: &mut dyn Factory, runtime: &Runtime) -> Result<$pool_path, shuttle_service::Error> { + async fn build(self, factory: &mut dyn Factory) -> Result<$pool_path, shuttle_service::Error> { let connection_string = match factory.get_environment() { shuttle_service::Environment::Production => { factory @@ -44,17 +43,11 @@ macro_rules! aws_engine { } }; - // A sqlx Pool cannot cross runtime boundaries, so make sure to create the Pool on the service end - let pool = runtime - .spawn(async move { - $options_path::new() - .min_connections(1) - .max_connections(5) - .connect(&connection_string) - .await - }) + let pool = $options_path::new() + .min_connections(1) + .max_connections(5) + .connect(&connection_string) .await - .map_err(CustomError::new)? .map_err(CustomError::new)?; Ok(pool) diff --git a/resources/persist/Cargo.toml b/resources/persist/Cargo.toml index 87d799e78..158e3f1f8 100644 --- a/resources/persist/Cargo.toml +++ b/resources/persist/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-persist" -version = "0.11.0" +version = "0.12.0" edition = "2021" license = "Apache-2.0" description = "Plugin for persist objects" @@ -11,7 +11,6 @@ keywords = ["shuttle-service", "persistence"] async-trait = "0.1.56" bincode = "1.2.1" serde = { version = "1.0.0", features = ["derive"] } -shuttle-common = { path = "../../common", version = "0.11.0", default-features = false } -shuttle-service = { path = "../../service", version = "0.11.0", default-features = false } +shuttle-common = { path = "../../common", version = "0.12.0", default-features = false } +shuttle-service = { path = "../../service", version = "0.12.0", default-features = false } thiserror = "1.0.32" -tokio = { version = "1.19.2", features = ["rt"] } diff --git a/resources/persist/src/lib.rs b/resources/persist/src/lib.rs index f6fd8e8a4..84d3ce458 100644 --- a/resources/persist/src/lib.rs +++ b/resources/persist/src/lib.rs @@ -10,7 +10,6 @@ use std::io::BufReader; use std::io::BufWriter; use std::path::PathBuf; use thiserror::Error; -use tokio::runtime::Runtime; #[derive(Error, Debug)] pub enum PersistError { @@ -74,7 +73,6 @@ impl ResourceBuilder for Persist { async fn build( self, factory: &mut dyn Factory, - _runtime: &Runtime, ) -> Result { Ok(PersistInstance { service_name: factory.get_service_name(), diff --git a/resources/secrets/Cargo.toml b/resources/secrets/Cargo.toml index 69116b75b..f12f74ccc 100644 --- a/resources/secrets/Cargo.toml +++ b/resources/secrets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-secrets" -version = "0.11.0" +version = "0.12.0" edition = "2021" license = "Apache-2.0" description = "Plugin to for managing secrets on shuttle" @@ -9,5 +9,4 @@ keywords = ["shuttle-service", "secrets"] [dependencies] async-trait = "0.1.56" -shuttle-service = { path = "../../service", version = "0.11.0", default-features = false } -tokio = { version = "1.19.2", features = ["rt"] } +shuttle-service = { path = "../../service", version = "0.12.0", default-features = false } diff --git a/resources/secrets/src/lib.rs b/resources/secrets/src/lib.rs index 27ab12ff8..856ea0593 100644 --- a/resources/secrets/src/lib.rs +++ b/resources/secrets/src/lib.rs @@ -3,7 +3,6 @@ use std::collections::BTreeMap; use async_trait::async_trait; use shuttle_service::{Error, Factory, ResourceBuilder}; -use tokio::runtime::Runtime; pub struct Secrets; @@ -14,11 +13,7 @@ impl ResourceBuilder for Secrets { Self {} } - async fn build( - self, - factory: &mut dyn Factory, - _runtime: &Runtime, - ) -> Result { + async fn build(self, factory: &mut dyn Factory) -> Result { let secrets = factory.get_secrets().await?; Ok(SecretStore { secrets }) diff --git a/resources/shared-db/Cargo.toml b/resources/shared-db/Cargo.toml index 99ca6c99f..c7e3388f4 100644 --- a/resources/shared-db/Cargo.toml +++ b/resources/shared-db/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-shared-db" -version = "0.11.0" +version = "0.12.0" edition = "2021" license = "Apache-2.0" description = "Plugin for managing shared databases on shuttle" @@ -10,9 +10,8 @@ keywords = ["shuttle-service", "database"] [dependencies] async-trait = "0.1.56" mongodb = { version = "2.3.0", optional = true } -shuttle-service = { path = "../../service", version = "0.11.0", default-features = false } +shuttle-service = { path = "../../service", version = "0.12.0", default-features = false } sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls"], optional = true } -tokio = { version = "1.19.2", features = ["rt"] } [features] postgres = ["sqlx/postgres"] diff --git a/resources/shared-db/src/lib.rs b/resources/shared-db/src/lib.rs index d9c0d36c9..b44eab3a4 100644 --- a/resources/shared-db/src/lib.rs +++ b/resources/shared-db/src/lib.rs @@ -1,7 +1,5 @@ #![doc = include_str!("../README.md")] -use tokio::runtime::Runtime; - use async_trait::async_trait; use shuttle_service::{database, error::CustomError, Error, Factory, ResourceBuilder}; @@ -18,11 +16,7 @@ impl ResourceBuilder for Postgres { Self { local_uri: None } } - async fn build( - self, - factory: &mut dyn Factory, - runtime: &Runtime, - ) -> Result { + async fn build(self, factory: &mut dyn Factory) -> Result { let connection_string = match factory.get_environment() { shuttle_service::Environment::Production => { factory @@ -44,17 +38,11 @@ impl ResourceBuilder for Postgres { } }; - // A sqlx Pool cannot cross runtime boundaries, so make sure to create the Pool on the service end - let pool = runtime - .spawn(async move { - sqlx::postgres::PgPoolOptions::new() - .min_connections(1) - .max_connections(5) - .connect(&connection_string) - .await - }) + let pool = sqlx::postgres::PgPoolOptions::new() + .min_connections(1) + .max_connections(5) + .connect(&connection_string) .await - .map_err(CustomError::new)? .map_err(CustomError::new)?; Ok(pool) @@ -84,11 +72,7 @@ impl ResourceBuilder for MongoDb { Self { local_uri: None } } - async fn build( - self, - factory: &mut dyn Factory, - runtime: &Runtime, - ) -> Result { + async fn build(self, factory: &mut dyn Factory) -> Result { let connection_string = match factory.get_environment() { shuttle_service::Environment::Production => factory .get_db_connection_string(database::Type::Shared(database::SharedEngine::MongoDb)) @@ -114,12 +98,7 @@ impl ResourceBuilder for MongoDb { client_options.min_pool_size = Some(1); client_options.max_pool_size = Some(5); - // A mongodb client cannot cross runtime boundaries, so make sure to create the client on the service end - let client = runtime - .spawn(async move { mongodb::Client::with_options(client_options) }) - .await - .map_err(CustomError::new)? - .map_err(CustomError::new)?; + let client = mongodb::Client::with_options(client_options).map_err(CustomError::new)?; // Return a handle to the database defined at the end of the connection string, which is the users provisioned // database diff --git a/resources/static-folder/Cargo.toml b/resources/static-folder/Cargo.toml index d42d2a032..fd5ff3b23 100644 --- a/resources/static-folder/Cargo.toml +++ b/resources/static-folder/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-static-folder" -version = "0.11.0" +version = "0.12.0" edition = "2021" license = "Apache-2.0" description = "Plugin to get a static folder at runtime on shuttle" @@ -9,9 +9,10 @@ keywords = ["shuttle-service", "static-folder"] [dependencies] async-trait = "0.1.56" -shuttle-service = { path = "../../service", version = "0.11.0", default-features = false } -tokio = { version = "1.19.2", features = ["rt"] } +fs_extra = "1.3.0" +shuttle-service = { path = "../../service", version = "0.12.0", default-features = false } +tracing = "0.1.37" [dev-dependencies] tempfile = "3.3.0" -tokio = { version = "1.19.2", features = ["macros"] } +tokio = { version = "1.19.2", features = ["macros", "rt"] } diff --git a/resources/static-folder/src/lib.rs b/resources/static-folder/src/lib.rs index 8b08e7e72..4a3f3c775 100644 --- a/resources/static-folder/src/lib.rs +++ b/resources/static-folder/src/lib.rs @@ -1,13 +1,11 @@ use async_trait::async_trait; +use fs_extra::dir::{copy, CopyOptions}; use shuttle_service::{ error::{CustomError, Error as ShuttleError}, Factory, ResourceBuilder, }; -use std::{ - fs::rename, - path::{Path, PathBuf}, -}; -use tokio::runtime::Runtime; +use std::path::{Path, PathBuf}; +use tracing::{error, trace}; pub struct StaticFolder<'a> { /// The folder to reach at runtime. Defaults to `static` @@ -17,6 +15,7 @@ pub struct StaticFolder<'a> { pub enum Error { AbsolutePath, TransversedUp, + Copy(fs_extra::error::Error), } impl<'a> StaticFolder<'a> { @@ -33,41 +32,66 @@ impl<'a> ResourceBuilder for StaticFolder<'a> { Self { folder: "static" } } - async fn build( - self, - factory: &mut dyn Factory, - _runtime: &Runtime, - ) -> Result { + async fn build(self, factory: &mut dyn Factory) -> Result { let folder = Path::new(self.folder); + trace!(?folder, "building static folder"); + // Prevent users from users from reading anything outside of their crate's build folder if folder.is_absolute() { + error!("the static folder cannot be an absolute path"); return Err(Error::AbsolutePath)?; } let input_dir = factory.get_build_path()?.join(self.folder); + trace!(input_directory = ?input_dir, "got input directory"); + match input_dir.canonicalize() { Ok(canonical_path) if canonical_path != input_dir => return Err(Error::TransversedUp)?, Ok(_) => { // The path did not change to outside the crate's build folder } - Err(err) => return Err(err)?, + Err(err) => { + error!( + error = &err as &dyn std::error::Error, + "failed to get static folder" + ); + return Err(err)?; + } } - let output_dir = factory.get_storage_path()?.join(self.folder); + let output_dir = factory.get_storage_path()?; + + trace!(output_directory = ?output_dir, "got output directory"); + + if output_dir.join(self.folder) == input_dir { + return Ok(output_dir.join(self.folder)); + } - rename(input_dir, output_dir.clone())?; + let copy_options = CopyOptions::new().overwrite(true); + match copy(&input_dir, &output_dir, ©_options) { + Ok(_) => Ok(output_dir.join(self.folder)), + Err(error) => { + error!( + error = &error as &dyn std::error::Error, + "failed to copy static folder" + ); - Ok(output_dir) + Err(Error::Copy(error))? + } + } } } impl From for shuttle_service::Error { fn from(error: Error) -> Self { let msg = match error { - Error::AbsolutePath => "Cannot use an absolute path for a static folder", - Error::TransversedUp => "Cannot transverse out of crate for a static folder", + Error::AbsolutePath => "Cannot use an absolute path for a static folder".to_string(), + Error::TransversedUp => { + "Cannot transverse out of crate for a static folder".to_string() + } + Error::Copy(error) => format!("Cannot copy static folder: {}", error), }; ShuttleError::Custom(CustomError::msg(msg)) @@ -175,8 +199,7 @@ mod tests { // Call plugin let static_folder = StaticFolder::new(); - let runtime = tokio::runtime::Runtime::new().unwrap(); - let actual_folder = static_folder.build(&mut factory, &runtime).await.unwrap(); + let actual_folder = static_folder.build(&mut factory).await.unwrap(); assert_eq!( actual_folder, @@ -189,8 +212,6 @@ mod tests { "Hello, test!", "expected file content to match" ); - - runtime.shutdown_background(); } #[tokio::test] @@ -198,15 +219,12 @@ mod tests { async fn cannot_use_absolute_path() { let mut factory = MockFactory::new(); let static_folder = StaticFolder::new(); - let runtime = tokio::runtime::Runtime::new().unwrap(); let _ = static_folder .folder("/etc") - .build(&mut factory, &runtime) + .build(&mut factory) .await .unwrap(); - - runtime.shutdown_background(); } #[tokio::test] @@ -221,13 +239,10 @@ mod tests { // Call plugin let static_folder = StaticFolder::new(); - let runtime = tokio::runtime::Runtime::new().unwrap(); let _ = static_folder .folder("../escape") - .build(&mut factory, &runtime) + .build(&mut factory) .await .unwrap(); - - runtime.shutdown_background(); } } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml new file mode 100644 index 000000000..9faaaeec0 --- /dev/null +++ b/runtime/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "shuttle-runtime" +version = "0.12.0" +edition.workspace = true +license.workspace = true +description = "Runtime to start and manage any service that runs on shuttle" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "shuttle-next" +required-features = ["next"] + +[lib] +doctest = false + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +chrono = { workspace = true } +clap = { workspace = true } +prost-types = { workspace = true } +serde_json = { workspace = true } +strfmt = "0.2.2" +thiserror = { workspace = true } +tokio = { version = "1.22.0", features = ["full"] } +tokio-stream = "0.1.11" +tonic = { workspace = true } +tower = { workspace = true } +tracing = { workspace = true, features = ["default"] } +tracing-subscriber = { workspace = true, features = ["default", "env-filter", "fmt"] } + +# TODO: bump these crates to 6.0 when we bump rust to >= 1.66 +cap-std = { version = "1.0.2", optional = true } +futures = { version = "0.3.25", optional = true } +hyper = { version = "0.14.23", optional = true } +rmp-serde = { version = "1.1.1", optional = true } +wasi-common = { version = "4.0.0", optional = true } +wasmtime = { version = "4.0.0", optional = true } +wasmtime-wasi = { version = "4.0.0", optional = true } + +[dependencies.shuttle-common] +workspace = true +features = ["claims", "backend"] + +[dependencies.shuttle-proto] +workspace = true + +[dependencies.shuttle-service] +workspace = true + +[dev-dependencies] +crossbeam-channel = "0.5.6" +portpicker = "0.1.1" +futures = { version = "0.3.25" } +shuttle-service = { workspace = true, features = ["builder"] } + +[features] +default = [] +next = [ + "cap-std", + "futures", + "hyper/server", + "rmp-serde", + "futures", + "wasi-common", + "wasmtime", + "wasmtime-wasi", + "shuttle-common/wasm" +] diff --git a/runtime/Makefile b/runtime/Makefile new file mode 100644 index 000000000..fd4fccbd0 --- /dev/null +++ b/runtime/Makefile @@ -0,0 +1,13 @@ +.PHONY: axum + +all: axum + +axum: + cd tests/resources/axum-wasm-expanded; cargo build --target wasm32-wasi + cp tests/resources/axum-wasm-expanded/target/wasm32-wasi/debug/shuttle_axum_expanded.wasm axum.wasm + +test: axum + cargo test --all-features -- --nocapture + +runtime: + cargo build diff --git a/runtime/README.md b/runtime/README.md new file mode 100644 index 000000000..919f96089 --- /dev/null +++ b/runtime/README.md @@ -0,0 +1,16 @@ +# shuttle-runtime + +[Shuttle](https://www.shuttle.rs/) is a Rust-native cloud development platform that lets you deploy your Rust apps for free. + +Shuttle is built for productivity, reliability and performance: + +- Zero-Configuration support for Rust using annotations +- Automatic resource provisioning (databases, caches, subdomains, etc.) via [Infrastructure-From-Code](https://www.shuttle.rs/blog/2022/05/09/ifc) +- First-class support for popular Rust frameworks ([Actix](https://docs.shuttle.rs/examples/actix), [Rocket](https://docs.shuttle.rs/examples/rocket), [Axum](https://docs.shuttle.rs/examples/axum), + [Tide](https://docs.shuttle.rs/examples/tide), [Poem](https://docs.shuttle.rs/examples/poem) and [Tower](https://docs.shuttle.rs/examples/tower)) +- Support for deploying Discord bots using [Serenity](https://docs.shuttle.rs/examples/serenity) +- Scalable hosting (with optional self-hosting) + +📖 Check out our documentation to get started quickly: [docs.shuttle.rs](https://docs.shuttle.rs) + +🙋‍♂️ If you have any questions, join our [Discord](https://discord.gg/shuttle) server. diff --git a/runtime/developing.md b/runtime/developing.md new file mode 100644 index 000000000..1939551ca --- /dev/null +++ b/runtime/developing.md @@ -0,0 +1,105 @@ +# How to run + +## The easy way +Both the alpha and next examples can be run using the local client: + +``` bash +cd path/to/example +cargo run --manifest-path ../../../Cargo.toml --bin cargo-shuttle -- run +``` + +When a more fine controlled testing is needed, use the instructions below. + +## axum-wasm + +Compile the wasm axum router: + +```bash +make axum +``` + +Run the test: + +```bash +cargo test --features next axum -- --nocapture + +# or, run tests +make test +``` + +Load and run: + +```bash +cargo run --features next --bin next -- --port 6001 +``` + +In another terminal: + +``` bash +# load +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "/home//runtime/axum.wasm"}' localhost:6001 runtime.Runtime/Load + +# start +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"deployment_id": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAw", "ip": "127.0.0.1:8000"}' localhost:6001 runtime.Runtime/Start + +# subscribe to logs +grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:6001 runtime.Runtime/SubscribeLogs + +# stop +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{}' localhost:6001 runtime.Runtime/Stop +``` + +Curl the service: +```bash +curl localhost:8000/hello + +curl localhost:8000/goodbye +``` + +## shuttle-alpha + +This will no longer load a `.so` file, the code to start the runtime will be +codegened for all services. + +An example can be found in `src/bin/rocket.rs` which contains the secrets rocket example at the bottom and the codegen at the top. + +To test, first start a provisioner from the root directory using: + +```bash +docker-compose -f docker-compose.rendered.yml up provisioner +``` + +Then in another shell, start the wrapped runtime using the clap CLI: + +```bash +cargo run --bin rocket -- --port 6001 --storage-manager-type working-dir --storage-manager-path ./ +``` + +Or directly (this is the path hardcoded in `deployer::start`): +```bash +# first, make sure the shuttle-runtime binary is built +cargo build +# then +/home//target/debug/shuttle-runtime --port 6001 --storage-manager-type working-dir --storage-manager-path ./ +``` + +Then in another shell, load the service and start it up: + +``` bash +# load the service +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "/home//examples/rocket/hello-world/target/debug/libhello_world.so", "secrets": {"MY_API_KEY": "test"}}' localhost:6001 runtime.Runtime/Load + +# start the service +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"deployment_id": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAw", "ip": "127.0.0.1:8000"}' localhost:6001 runtime.Runtime/Start + +# subscribe to logs +grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:6001 runtime.Runtime/SubscribeLogs + +# stop the service +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{}' localhost:6001 runtime.Runtime/Stop +``` + +## Running the tests +```bash +$ cd ..; make test +``` diff --git a/runtime/src/alpha/args.rs b/runtime/src/alpha/args.rs new file mode 100644 index 000000000..21afa20f6 --- /dev/null +++ b/runtime/src/alpha/args.rs @@ -0,0 +1,37 @@ +use std::path::PathBuf; + +use clap::{Parser, ValueEnum}; +use tonic::transport::{Endpoint, Uri}; + +#[derive(Parser, Debug)] +#[command(version)] +pub struct Args { + /// Port to start runtime on + #[arg(long)] + pub port: u16, + + /// Address to reach provisioner at + #[arg(long, default_value = "http://localhost:5000")] + pub provisioner_address: Endpoint, + + /// Type of storage manager to start + #[arg(long, value_enum)] + pub storage_manager_type: StorageManagerType, + + /// Path to use for storage manager + #[arg(long)] + pub storage_manager_path: PathBuf, + + /// Address to reach the authentication service at + #[arg(long, default_value = "http://127.0.0.1:8008")] + pub auth_uri: Uri, +} + +#[derive(Clone, Debug, ValueEnum)] +pub enum StorageManagerType { + /// Use a deloyer artifacts directory + Artifacts, + + /// Use a local working directory + WorkingDir, +} diff --git a/runtime/src/alpha/mod.rs b/runtime/src/alpha/mod.rs new file mode 100644 index 000000000..1c188c894 --- /dev/null +++ b/runtime/src/alpha/mod.rs @@ -0,0 +1,434 @@ +use std::{ + collections::BTreeMap, + iter::FromIterator, + net::{Ipv4Addr, SocketAddr}, + ops::DerefMut, + str::FromStr, + sync::{Arc, Mutex}, + time::Duration, +}; + +use anyhow::Context; +use async_trait::async_trait; +use clap::Parser; +use core::future::Future; +use shuttle_common::{ + backends::{ + auth::{AuthPublicKey, JwtAuthenticationLayer}, + tracing::ExtractPropagationLayer, + }, + claims::{Claim, ClaimLayer, InjectPropagationLayer}, + resource, + storage_manager::{ArtifactsStorageManager, StorageManager, WorkingDirStorageManager}, +}; +use shuttle_proto::{ + provisioner::provisioner_client::ProvisionerClient, + runtime::{ + self, + runtime_server::{Runtime, RuntimeServer}, + LoadRequest, LoadResponse, LogItem, StartRequest, StartResponse, StopReason, StopRequest, + StopResponse, SubscribeLogsRequest, SubscribeStopRequest, SubscribeStopResponse, + }, +}; +use shuttle_service::{Environment, Factory, Service, ServiceName}; +use tokio::sync::{broadcast, oneshot}; +use tokio::sync::{ + broadcast::Sender, + mpsc::{self, UnboundedReceiver, UnboundedSender}, +}; +use tokio_stream::wrappers::ReceiverStream; +use tonic::{ + transport::{Endpoint, Server}, + Request, Response, Status, +}; +use tower::ServiceBuilder; +use tracing::{error, info, trace, warn}; + +use crate::{provisioner_factory::ProvisionerFactory, Logger}; + +use self::args::Args; + +mod args; + +pub async fn start(loader: impl Loader + Send + 'static) { + let args = Args::parse(); + let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), args.port); + + let provisioner_address = args.provisioner_address; + let mut server_builder = Server::builder() + .http2_keepalive_interval(Some(Duration::from_secs(60))) + .layer(JwtAuthenticationLayer::new(AuthPublicKey::new( + args.auth_uri, + ))) + .layer(ExtractPropagationLayer); + + // We wrap the StorageManager trait object in an Arc rather than a Box, since we need + // to clone it in the `ProvisionerFactory::new` call in the alpha runtime `load` method. + // We might be able to optimize this by implementing clone for a Box + // or by using static dispatch instead. + let storage_manager: Arc = match args.storage_manager_type { + args::StorageManagerType::Artifacts => { + Arc::new(ArtifactsStorageManager::new(args.storage_manager_path)) + } + args::StorageManagerType::WorkingDir => { + Arc::new(WorkingDirStorageManager::new(args.storage_manager_path)) + } + }; + + let router = { + let alpha = Alpha::new( + provisioner_address, + loader, + storage_manager, + Environment::Local, + ); + + let svc = RuntimeServer::new(alpha); + server_builder.add_service(svc) + }; + + router.serve(addr).await.unwrap(); +} + +pub struct Alpha { + // Mutexes are for interior mutability + logs_rx: Mutex>>, + logs_tx: UnboundedSender, + stopped_tx: Sender<(StopReason, String)>, + provisioner_address: Endpoint, + kill_tx: Mutex>>, + storage_manager: Arc, + loader: Mutex>, + service: Mutex>, + env: Environment, +} + +impl Alpha { + pub fn new( + provisioner_address: Endpoint, + loader: L, + storage_manager: Arc, + env: Environment, + ) -> Self { + let (tx, rx) = mpsc::unbounded_channel(); + let (stopped_tx, _stopped_rx) = broadcast::channel(10); + + Self { + logs_rx: Mutex::new(Some(rx)), + logs_tx: tx, + stopped_tx, + kill_tx: Mutex::new(None), + provisioner_address, + storage_manager, + loader: Mutex::new(Some(loader)), + service: Mutex::new(None), + env, + } + } +} + +#[async_trait] +pub trait Loader +where + Fac: Factory, +{ + type Service: Service; + + async fn load( + self, + factory: Fac, + logger: Logger, + ) -> Result; +} + +#[async_trait] +impl Loader for F +where + F: FnOnce(Fac, Logger) -> O + Send, + O: Future> + Send, + Fac: Factory + 'static, + S: Service, +{ + type Service = S; + + async fn load( + self, + factory: Fac, + logger: Logger, + ) -> Result { + (self)(factory, logger).await + } +} + +#[async_trait] +impl Runtime for Alpha +where + L: Loader + Send + 'static, + S: Service + Send + 'static, +{ + async fn load(&self, request: Request) -> Result, Status> { + let claim = request.extensions().get::().map(Clone::clone); + + let LoadRequest { + path, + resources, + secrets, + service_name, + } = request.into_inner(); + trace!(path, "loading alpha project"); + + let secrets = BTreeMap::from_iter(secrets.into_iter()); + + let channel = self + .provisioner_address + .clone() + .connect() + .await + .context("failed to connect to provisioner") + .map_err(|err| Status::internal(err.to_string()))?; + let channel = ServiceBuilder::new() + .layer(ClaimLayer) + .layer(InjectPropagationLayer) + .service(channel); + + let provisioner_client = ProvisionerClient::new(channel); + + let service_name = ServiceName::from_str(service_name.as_str()) + .map_err(|err| Status::from_error(Box::new(err)))?; + + let resources = resources + .into_iter() + .map(resource::Response::from_bytes) + .collect(); + let resources: Arc>> = + Arc::new(tokio::sync::Mutex::new(resources)); + + let factory = ProvisionerFactory::new( + provisioner_client, + service_name, + secrets, + self.storage_manager.clone(), + self.env, + claim, + resources.clone(), + ); + trace!("got factory"); + + let logs_tx = self.logs_tx.clone(); + let logger = Logger::new(logs_tx); + + let loader = self.loader.lock().unwrap().deref_mut().take().unwrap(); + + let service = match tokio::spawn(loader.load(factory, logger)).await { + Ok(res) => match res { + Ok(service) => service, + Err(error) => { + error!(%error, "loading service failed"); + + let message = LoadResponse { + success: false, + message: error.to_string(), + resources: resources + .lock() + .await + .clone() + .into_iter() + .map(resource::Response::into_bytes) + .collect(), + }; + return Ok(Response::new(message)); + } + }, + Err(error) => { + let resources = resources + .lock() + .await + .clone() + .into_iter() + .map(resource::Response::into_bytes) + .collect(); + + if error.is_panic() { + let panic = error.into_panic(); + let msg = panic + .downcast_ref::<&str>() + .map(|x| x.to_string()) + .unwrap_or_else(|| "".to_string()); + + error!(error = msg, "loading service panicked"); + + let message = LoadResponse { + success: false, + message: msg, + resources, + }; + return Ok(Response::new(message)); + } else { + error!(%error, "loading service crashed"); + let message = LoadResponse { + success: false, + message: error.to_string(), + resources, + }; + return Ok(Response::new(message)); + } + } + }; + + *self.service.lock().unwrap() = Some(service); + + let message = LoadResponse { + success: true, + message: String::new(), + resources: resources + .lock() + .await + .clone() + .into_iter() + .map(resource::Response::into_bytes) + .collect(), + }; + Ok(Response::new(message)) + } + + async fn start( + &self, + request: Request, + ) -> Result, Status> { + trace!("alpha starting"); + let service = self.service.lock().unwrap().deref_mut().take(); + let service = service.unwrap(); + + let StartRequest { ip, .. } = request.into_inner(); + let service_address = SocketAddr::from_str(&ip) + .context("invalid socket address") + .map_err(|err| Status::invalid_argument(err.to_string()))?; + + let _logs_tx = self.logs_tx.clone(); + + trace!(%service_address, "starting"); + + let (kill_tx, kill_rx) = tokio::sync::oneshot::channel(); + *self.kill_tx.lock().unwrap() = Some(kill_tx); + + let stopped_tx = self.stopped_tx.clone(); + + let handle = tokio::runtime::Handle::current(); + + // start service as a background task with a kill receiver + tokio::spawn(async move { + let mut background = handle.spawn(service.bind(service_address)); + + tokio::select! { + res = &mut background => { + match res { + Ok(_) => { + info!("service stopped all on its own"); + stopped_tx.send((StopReason::End, String::new())).unwrap(); + }, + Err(error) => { + if error.is_panic() { + let panic = error.into_panic(); + let msg = panic.downcast_ref::<&str>() + .map(|x| x.to_string()) + .unwrap_or_else(|| "".to_string()); + + error!(error = msg, "service panicked"); + + stopped_tx + .send((StopReason::Crash, msg)) + .unwrap(); + } else { + error!(%error, "service crashed"); + stopped_tx + .send((StopReason::Crash, error.to_string())) + .unwrap(); + } + }, + } + }, + message = kill_rx => { + match message { + Ok(_) => { + stopped_tx.send((StopReason::Request, String::new())).unwrap(); + } + Err(_) => trace!("the sender dropped") + }; + + info!("will now abort the service"); + background.abort(); + background.await.unwrap().expect("to stop service"); + } + } + }); + + let message = StartResponse { success: true }; + + Ok(Response::new(message)) + } + + async fn stop(&self, _request: Request) -> Result, Status> { + let kill_tx = self.kill_tx.lock().unwrap().deref_mut().take(); + + if let Some(kill_tx) = kill_tx { + if kill_tx.send("stopping deployment".to_owned()).is_err() { + error!("the receiver dropped"); + return Err(Status::internal("failed to stop deployment")); + } + + Ok(Response::new(StopResponse { success: true })) + } else { + warn!("failed to stop deployment"); + + Ok(tonic::Response::new(StopResponse { success: false })) + } + } + + type SubscribeStopStream = ReceiverStream>; + + async fn subscribe_stop( + &self, + _request: Request, + ) -> Result, Status> { + let mut stopped_rx = self.stopped_tx.subscribe(); + let (tx, rx) = mpsc::channel(1); + + // Move the stop channel into a stream to be returned + tokio::spawn(async move { + while let Ok((reason, message)) = stopped_rx.recv().await { + tx.send(Ok(SubscribeStopResponse { + reason: reason as i32, + message, + })) + .await + .unwrap(); + } + }); + + Ok(Response::new(ReceiverStream::new(rx))) + } + + type SubscribeLogsStream = ReceiverStream>; + + async fn subscribe_logs( + &self, + _request: Request, + ) -> Result, Status> { + let logs_rx = self.logs_rx.lock().unwrap().deref_mut().take(); + + if let Some(mut logs_rx) = logs_rx { + let (tx, rx) = mpsc::channel(1); + + // Move logger items into stream to be returned + tokio::spawn(async move { + while let Some(log) = logs_rx.recv().await { + tx.send(Ok(log)).await.expect("to send log"); + } + }); + + Ok(Response::new(ReceiverStream::new(rx))) + } else { + Err(Status::internal("logs have already been subscribed to")) + } + } +} diff --git a/runtime/src/bin/shuttle-next.rs b/runtime/src/bin/shuttle-next.rs new file mode 100644 index 000000000..d375ab17f --- /dev/null +++ b/runtime/src/bin/shuttle-next.rs @@ -0,0 +1,32 @@ +use std::{ + net::{Ipv4Addr, SocketAddr}, + time::Duration, +}; + +use clap::Parser; +use shuttle_common::backends::tracing::{setup_tracing, ExtractPropagationLayer}; +use shuttle_proto::runtime::runtime_server::RuntimeServer; +use shuttle_runtime::{AxumWasm, NextArgs}; +use tonic::transport::Server; +use tracing::trace; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + let args = NextArgs::parse(); + + setup_tracing(tracing_subscriber::registry(), "shuttle-next"); + + trace!(args = ?args, "parsed args"); + + let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), args.port); + + let mut server_builder = Server::builder() + .http2_keepalive_interval(Some(Duration::from_secs(60))) + .layer(ExtractPropagationLayer); + + let axum = AxumWasm::default(); + let svc = RuntimeServer::new(axum); + let router = server_builder.add_service(svc); + + router.serve(addr).await.unwrap(); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs new file mode 100644 index 000000000..8e1cf8e28 --- /dev/null +++ b/runtime/src/lib.rs @@ -0,0 +1,236 @@ +#![doc( + html_logo_url = "https://raw.githubusercontent.com/shuttle-hq/shuttle/main/assets/logo-square-transparent.png", + html_favicon_url = "https://raw.githubusercontent.com/shuttle-hq/shuttle/main/assets/favicon.ico" +)] +//! # Shuttle - Deploy Rust apps with a single Cargo subcommand +//!

+//! +//!
+//! +//! Hello, and welcome to the shuttle API documentation! +//! +//! Shuttle is an open-source app platform that uses traits and annotations to configure your backend deployments. +//! +//! ## Usage +//! Start by installing the [`cargo shuttle`](https://docs.rs/crate/cargo-shuttle/latest) subcommand by running the following in a terminal: +//! +//! ```bash +//! $ cargo install cargo-shuttle +//! ``` +//! +//! Now that shuttle is installed, you can initialize a project with Axum boilerplate: +//! ```bash +//! $ cargo shuttle init --axum my-axum-app +//! ``` +//! +//! By looking at the `Cargo.toml` file of the generated `my-axum-app` project you will see it has been made to +//! be a binary crate with a few dependencies including `shuttle-runtime` and `shuttle-axum`. +//! +//! ```toml +//! shuttle-runtime = "0.12.0" +//! axum = "0.6.10" +//! shuttle-axum = "0.12.0" +//! tokio = "1.26" +//! ``` +//! +//! A boilerplate code for your axum project can also be found in `src/main.rs`: +//! +//! ```rust,no_run +//! use axum::{routing::get, Router}; +//! +//! async fn hello_world() -> &'static str { +//! "Hello, world!" +//! } +//! +//! #[shuttle_runtime::main] +//! async fn axum() -> shuttle_axum::ShuttleAxum { +//! let router = Router::new().route("/hello", get(hello_world)); +//! +//! Ok(router.into()) +//! } +//! ``` +//! +//! Check out [our docs](https://docs.shuttle.rs/introduction/welcome) to see all the frameworks we support, or +//! our [examples](https://github.com/shuttle-hq/examples) if you prefer that format. +//! +//! ## Running locally +//! To test your app locally before deploying, use: +//! +//! ```bash +//! $ cargo shuttle run +//! ``` +//! +//! You should see your app build and start on the default port 8000. You can test this using; +//! +//! ```bash +//! $ curl http://localhost:8000/hello +//! +//! Hello, world! +//! ``` +//! +//! ## Deploying +//! +//! You can deploy your service with the [`cargo shuttle`](https://docs.rs/crate/cargo-shuttle/latest) subcommand too. +//! But, you will need to authenticate with the shuttle service first using: +//! +//! ```bash +//! $ cargo shuttle login +//! ``` +//! +//! This will open a browser window and prompt you to connect using your GitHub account. +//! +//! Before you can deploy, you have to create a project. This will start a deployer container for your +//! project under the hood, ensuring isolation from other users' projects. PS. you don't have to do this +//! now if you did in in the `cargo shuttle init` flow. +//! +//! ```bash +//! $ cargo shuttle project new +//! ``` +//! +//! Then, deploy the service with: +//! +//! ```bash +//! $ cargo shuttle deploy +//! ``` +//! +//! Your service will immediately be available at `{crate_name}.shuttleapp.rs`. For example: +//! +//! ```bash +//! $ curl https://my-axum-app.shuttleapp.rs/hello +//! Hello, world! +//! ``` +//! +//! ## Using `sqlx` +//! +//! Here is a quick example to deploy a rocket service that uses a postgres database and [sqlx](http://docs.rs/sqlx): +//! +//! Initialize a project with Rocket boilerplate: +//! ```bash +//! $ cargo shuttle init --rocket my-rocket-app +//! ``` +//! +//! Add `shuttle-shared-db` as a dependency with the `postgres` feature, and add `sqlx` as a dependency with the +//! `runtime-tokio-native-tls` and `postgres` features inside `Cargo.toml`: +//! +//! ```toml +//! shuttle-shared-db = { version = "0.12.0", features = ["postgres"] } +//! sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres"] } +//! ``` +//! +//! Now update the `#[shuttle_runtime::main]` function to take in a `PgPool`: +//! +//! ```rust,no_run +//! #[macro_use] +//! extern crate rocket; +//! +//! use rocket::State; +//! use sqlx::PgPool; +//! use shuttle_rocket::ShuttleRocket; +//! +//! struct MyState(PgPool); +//! +//! #[get("/hello")] +//! fn hello(state: &State) -> &'static str { +//! // Do things with `state.0`... +//! "Hello, Postgres!" +//! } +//! +//! #[shuttle_runtime::main] +//! async fn rocket(#[shuttle_shared_db::Postgres] pool: PgPool) -> ShuttleRocket { +//! let state = MyState(pool); +//! let rocket = rocket::build().manage(state).mount("/", routes![hello]); +//! +//! Ok(rocket.into()) +//! } +//! ``` +//! +//! For a local run, shuttle will automatically provision a Postgres instance inside a [Docker](https://www.docker.com/) container on your machine and connect it to the `PgPool`. +//! +//! For deploys, shuttle will provision a database for your application and connect it to the `PgPool` on your behalf. +//! +//! To learn more about shuttle managed resources, see our [resource docs](https://docs.shuttle.rs/resources/shuttle-shared-db). +//! +//! ## Configuration +//! +//! The `cargo shuttle` command can be customized by creating a `Shuttle.toml` in the same location as your `Cargo.toml`. +//! +//! ##### Change the name of your service +//! +//! To have your service deployed with a different name, add a `name` entry in the `Shuttle.toml`: +//! +//! ```toml +//! name = "hello-world" +//! ``` +//! +//! If the `name` key is not specified, the service's name will be the same as the crate's name. +//! +//! Alternatively, you can override the project name on the command-line, by passing the --name argument to any subcommand like so: +//! +//! ```bash +//! $ cargo shuttle deploy --name=$PROJECT_NAME +//! ``` +//! +//! ##### Using Podman instead of Docker +//! If you are using [Podman](https://podman.io/) instead of Docker, then `cargo shuttle run` will give +//! `got unexpected error while inspecting docker container: error trying to connect: No such file or directory` error. +//! +//! To fix this error you will need to expose a rootless socket for Podman first. This can be done using: +//! +//! ```bash +//! podman system service --time=0 unix:///tmp/podman.sock +//! ``` +//! +//! Now set the `DOCKER_HOST` environment variable to point to this socket using: +//! +//! ```bash +//! export DOCKER_HOST=unix:///tmp/podman.sock +//! ``` +//! +//! Now all `cargo shuttle run` commands will work against Podman. +//! +//! ## Getting API keys +//! +//! After you've installed the [cargo-shuttle](https://docs.rs/crate/cargo-shuttle/latest) command, run: +//! +//! ```bash +//! $ cargo shuttle login +//! ``` +//! +//! this will open a browser window and prompt you to connect using your GitHub account. +//! +//! ## We're in alpha 🤗 +//! +//! Thanks for using shuttle! We're very happy to have you with us! +//! +//! During our alpha period, API keys are completely free and you can deploy as many services as you want. +//! +//! Just keep in mind that there may be some kinks that require us to take all deployments down once in a while. In certain circumstances we may also have to delete all the data associated with those deployments. +//! +//! To stay updated with the release status of shuttle, [join our Discord](https://discord.gg/shuttle)! +//! +//! ## Join Discord +//! +//! If you have any questions, [join our Discord server](https://discord.gg/shuttle). There's always someone on there that can help! +//! +//! You can also [open an issue or a discussion on GitHub](https://github.com/shuttle-hq/shuttle). +//! +mod alpha; +mod logger; +#[cfg(feature = "next")] +mod next; +mod provisioner_factory; + +pub use alpha::{start, Alpha}; +pub use async_trait::async_trait; +pub use logger::Logger; +#[cfg(feature = "next")] +pub use next::{AxumWasm, NextArgs}; +pub use provisioner_factory::ProvisionerFactory; +pub use shuttle_common::storage_manager::StorageManager; +pub use shuttle_service::{main, CustomError, Error, Factory, ResourceBuilder, Service}; + +// Dependencies required by the codegen +pub use anyhow::Context; +pub use strfmt::strfmt; +pub use tracing; +pub use tracing_subscriber; diff --git a/runtime/src/logger.rs b/runtime/src/logger.rs new file mode 100644 index 000000000..b3608e301 --- /dev/null +++ b/runtime/src/logger.rs @@ -0,0 +1,100 @@ +use std::time::SystemTime; + +use chrono::Utc; +use prost_types::Timestamp; +use shuttle_common::tracing::JsonVisitor; +use shuttle_proto::runtime::{LogItem, LogLevel}; +use tokio::sync::mpsc::UnboundedSender; +use tracing::Subscriber; +use tracing_subscriber::Layer; + +pub struct Logger { + tx: UnboundedSender, +} + +impl Logger { + pub fn new(tx: UnboundedSender) -> Self { + Self { tx } + } +} + +impl Layer for Logger +where + S: Subscriber, +{ + fn on_event( + &self, + event: &tracing::Event<'_>, + _ctx: tracing_subscriber::layer::Context<'_, S>, + ) { + let datetime = Utc::now(); + + let item = { + let metadata = event.metadata(); + let mut visitor = JsonVisitor::default(); + + event.record(&mut visitor); + + LogItem { + level: LogLevel::from(metadata.level()) as i32, + timestamp: Some(Timestamp::from(SystemTime::from(datetime))), + file: visitor.file.or_else(|| metadata.file().map(str::to_string)), + line: visitor.line.or_else(|| metadata.line()), + target: visitor + .target + .unwrap_or_else(|| metadata.target().to_string()), + fields: serde_json::to_vec(&visitor.fields).unwrap(), + } + }; + + self.tx.send(item).expect("sending log should succeed"); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use tokio::sync::mpsc; + use tracing_subscriber::prelude::*; + + #[test] + fn logging() { + let (s, mut r) = mpsc::unbounded_channel(); + + let logger = Logger::new(s); + + let _guard = tracing_subscriber::registry().with(logger).set_default(); + + tracing::debug!("this is"); + tracing::info!("hi"); + tracing::warn!("from"); + tracing::error!("logger"); + + assert_eq!( + r.blocking_recv().map(to_tuple), + Some(("this is".to_string(), LogLevel::Debug as i32)) + ); + assert_eq!( + r.blocking_recv().map(to_tuple), + Some(("hi".to_string(), LogLevel::Info as i32)) + ); + assert_eq!( + r.blocking_recv().map(to_tuple), + Some(("from".to_string(), LogLevel::Warn as i32)) + ); + assert_eq!( + r.blocking_recv().map(to_tuple), + Some(("logger".to_string(), LogLevel::Error as i32)) + ); + } + + fn to_tuple(log: LogItem) -> (String, i32) { + let fields: serde_json::Map = + serde_json::from_slice(&log.fields).unwrap(); + + let message = fields["message"].as_str().unwrap().to_owned(); + + (message, log.level) + } +} diff --git a/runtime/src/next/args.rs b/runtime/src/next/args.rs new file mode 100644 index 000000000..ccbb3ac6e --- /dev/null +++ b/runtime/src/next/args.rs @@ -0,0 +1,9 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version)] +pub struct NextArgs { + /// Port to start runtime on + #[arg(long)] + pub port: u16, +} diff --git a/runtime/src/next/mod.rs b/runtime/src/next/mod.rs new file mode 100644 index 000000000..bc6a0ac5c --- /dev/null +++ b/runtime/src/next/mod.rs @@ -0,0 +1,559 @@ +use std::convert::Infallible; +use std::io::{BufReader, Read, Write}; +use std::net::{Shutdown, SocketAddr}; +use std::ops::DerefMut; +use std::os::unix::prelude::RawFd; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::Mutex; + +use anyhow::Context; +use async_trait::async_trait; +use cap_std::os::unix::net::UnixStream; +use futures::TryStreamExt; +use hyper::body::HttpBody; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Request, Response}; +use shuttle_common::wasm::{Bytesable, Log, RequestWrapper, ResponseWrapper}; +use shuttle_proto::runtime::runtime_server::Runtime; +use shuttle_proto::runtime::{ + self, LoadRequest, LoadResponse, StartRequest, StartResponse, StopReason, StopRequest, + StopResponse, SubscribeLogsRequest, SubscribeStopRequest, SubscribeStopResponse, +}; +use tokio::sync::mpsc::{Receiver, Sender}; +use tokio::sync::{broadcast, mpsc, oneshot}; +use tokio_stream::wrappers::ReceiverStream; +use tonic::Status; +use tracing::{error, trace, warn}; +use wasi_common::file::FileCaps; +use wasmtime::{Engine, Linker, Module, Store}; +use wasmtime_wasi::sync::net::UnixStream as WasiUnixStream; +use wasmtime_wasi::{WasiCtx, WasiCtxBuilder}; + +mod args; + +pub use self::args::NextArgs; + +extern crate rmp_serde as rmps; + +const LOGS_FD: u32 = 20; +const PARTS_FD: u32 = 3; +const BODY_FD: u32 = 4; + +pub struct AxumWasm { + router: Mutex>, + logs_rx: Mutex>>>, + logs_tx: Sender>, + kill_tx: Mutex>>, + stopped_tx: broadcast::Sender<(StopReason, String)>, +} + +impl AxumWasm { + pub fn new() -> Self { + // Allow about 2^15 = 32k logs of backpressure + // We know the wasm currently handles about 16k requests per second (req / sec) so 16k seems to be a safe number + // As we make performance gains elsewhere this might eventually become the new bottleneck to increase :D + // + // Testing has shown that a number half the req / sec yields poor performance. A number the same as the req / sec + // seems acceptable so going with double the number for some headroom + let (tx, rx) = mpsc::channel(1 << 15); + + let (stopped_tx, _stopped_rx) = broadcast::channel(10); + + Self { + router: Mutex::new(None), + logs_rx: Mutex::new(Some(rx)), + logs_tx: tx, + kill_tx: Mutex::new(None), + stopped_tx, + } + } +} + +impl Default for AxumWasm { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl Runtime for AxumWasm { + async fn load( + &self, + request: tonic::Request, + ) -> Result, Status> { + let wasm_path = request.into_inner().path; + trace!(wasm_path, "loading shuttle-next project"); + + let router = RouterBuilder::new() + .map_err(|err| Status::from_error(err.into()))? + .src(wasm_path) + .build() + .map_err(|err| Status::from_error(err.into()))?; + + *self.router.lock().unwrap() = Some(router); + + let message = LoadResponse { + success: true, + message: String::new(), + resources: Vec::new(), + }; + + Ok(tonic::Response::new(message)) + } + + async fn start( + &self, + request: tonic::Request, + ) -> Result, Status> { + let StartRequest { ip } = request.into_inner(); + + let address = SocketAddr::from_str(&ip) + .context("invalid socket address") + .map_err(|err| Status::invalid_argument(err.to_string()))?; + + let logs_tx = self.logs_tx.clone(); + + let (kill_tx, kill_rx) = tokio::sync::oneshot::channel(); + + *self.kill_tx.lock().unwrap() = Some(kill_tx); + + let router = self + .router + .lock() + .unwrap() + .take() + .context("tried to start a service that was not loaded") + .map_err(|err| Status::internal(err.to_string()))?; + + let stopped_tx = self.stopped_tx.clone(); + + tokio::spawn(run_until_stopped( + router, address, logs_tx, kill_rx, stopped_tx, + )); + + let message = StartResponse { success: true }; + + Ok(tonic::Response::new(message)) + } + + type SubscribeLogsStream = ReceiverStream>; + + async fn subscribe_logs( + &self, + _request: tonic::Request, + ) -> Result, Status> { + let logs_rx = self.logs_rx.lock().unwrap().deref_mut().take(); + + if let Some(logs_rx) = logs_rx { + Ok(tonic::Response::new(ReceiverStream::new(logs_rx))) + } else { + Err(Status::internal("logs have already been subscribed to")) + } + } + + async fn stop( + &self, + request: tonic::Request, + ) -> Result, Status> { + let _request = request.into_inner(); + + let kill_tx = self.kill_tx.lock().unwrap().deref_mut().take(); + + if let Some(kill_tx) = kill_tx { + if kill_tx.send("stopping deployment".to_owned()).is_err() { + error!("the receiver dropped"); + return Err(Status::internal("failed to stop deployment")); + } + + Ok(tonic::Response::new(StopResponse { success: true })) + } else { + warn!("trying to stop a service that was not started"); + + Ok(tonic::Response::new(StopResponse { success: false })) + } + } + + type SubscribeStopStream = ReceiverStream>; + + async fn subscribe_stop( + &self, + _request: tonic::Request, + ) -> Result, Status> { + let mut stopped_rx = self.stopped_tx.subscribe(); + let (tx, rx) = mpsc::channel(1); + + // Move the stop channel into a stream to be returned + tokio::spawn(async move { + trace!("moved stop channel into thread"); + while let Ok((reason, message)) = stopped_rx.recv().await { + tx.send(Ok(SubscribeStopResponse { + reason: reason as i32, + message, + })) + .await + .unwrap(); + } + }); + + Ok(tonic::Response::new(ReceiverStream::new(rx))) + } +} +struct RouterBuilder { + engine: Engine, + linker: Linker, + src: Option, +} + +impl RouterBuilder { + fn new() -> anyhow::Result { + let engine = Engine::default(); + + let mut linker: Linker = Linker::new(&engine); + wasmtime_wasi::add_to_linker(&mut linker, |s| s)?; + + Ok(Self { + engine, + linker, + src: None, + }) + } + + fn src>(mut self, src: P) -> Self { + self.src = Some(src.as_ref().to_path_buf()); + self + } + + fn build(self) -> anyhow::Result { + let file = self.src.context("module path should be set")?; + let module = Module::from_file(&self.engine, file)?; + + for export in module.exports() { + println!("export: {}", export.name()); + } + + Ok(Router { + linker: self.linker, + engine: self.engine, + module, + }) + } +} + +#[derive(Clone)] +struct Router { + linker: Linker, + engine: Engine, + module: Module, +} + +impl Router { + /// Send a HTTP request with body to given endpoint on the axum-wasm router and return the response + async fn handle_request( + &mut self, + req: hyper::Request, + logs_tx: Sender>, + ) -> anyhow::Result> { + let wasi = WasiCtxBuilder::new() + .inherit_stdio() + .inherit_args() + .context("failed to read args")? + .build(); + + let mut store = Store::new(&self.engine, wasi); + self.linker.module(&mut store, "axum", &self.module)?; + + let (logs_stream, logs_client) = + UnixStream::pair().context("failed to open logs unixstream")?; + let (mut parts_stream, parts_client) = + UnixStream::pair().context("failed to open parts unixstream")?; + let (mut body_stream, body_client) = + UnixStream::pair().context("failed to open body write unixstream")?; + + let logs_client = WasiUnixStream::from_cap_std(logs_client); + let parts_client = WasiUnixStream::from_cap_std(parts_client); + let body_client = WasiUnixStream::from_cap_std(body_client); + + store + .data_mut() + .insert_file(LOGS_FD, Box::new(logs_client), FileCaps::all()); + + store + .data_mut() + .insert_file(PARTS_FD, Box::new(parts_client), FileCaps::all()); + store + .data_mut() + .insert_file(BODY_FD, Box::new(body_client), FileCaps::all()); + + tokio::task::spawn_blocking(move || { + let mut iter = logs_stream.bytes().filter_map(Result::ok); + + while let Some(log) = Log::from_bytes(&mut iter) { + logs_tx.blocking_send(Ok(log.into())).expect("to send log"); + } + }); + + let (parts, body) = req.into_parts(); + + // Serialise request parts to rmp + let request_rmp = RequestWrapper::from(parts) + .into_rmp() + .context("failed to make request wrapper")?; + + // Write request parts to wasm module + parts_stream + .write_all(&request_rmp) + .context("failed to write http parts to wasm")?; + + // To protect our server, reject requests with bodies larger than + // 64kbs of data. + let body_size = body.size_hint().upper().unwrap_or(u64::MAX); + + if body_size > 1024 * 64 { + let response = Response::builder() + .status(hyper::http::StatusCode::PAYLOAD_TOO_LARGE) + .body(Body::empty()) + .expect("building request with empty body should not fail"); + + // Return early if body is too big + return Ok(response); + } + + let body_bytes = hyper::body::to_bytes(body) + .await + .context("failed to concatenate request body buffers")?; + + // Write body to wasm + body_stream + .write_all(body_bytes.as_ref()) + .context("failed to write body to wasm")?; + + // Shut down the write part of the stream to signal EOF + body_stream + .shutdown(Shutdown::Write) + .expect("failed to shut down body write half"); + + // Call our function in wasm, telling it to route the request we've written to it + // and write back a response + trace!("calling Router"); + self.linker + .get(&mut store, "axum", "__SHUTTLE_Axum_call") + .context("wasm module should be loaded and the router function should be available")? + .into_func() + .context("router function should be a function")? + .typed::<(RawFd, RawFd, RawFd), ()>(&store)? + .call( + &mut store, + (LOGS_FD as i32, PARTS_FD as i32, BODY_FD as i32), + )?; + + // Read response parts from wasm + let reader = BufReader::new(&mut parts_stream); + + // Deserialize response parts from rust messagepack + let wrapper: ResponseWrapper = + rmps::from_read(reader).context("failed to deserialize response parts")?; + + // Read response body from wasm, convert it to a Stream and pass it to hyper + let reader = BufReader::new(body_stream); + let stream = futures::stream::iter(reader.bytes()).try_chunks(2); + let body = hyper::Body::wrap_stream(stream); + + let response: Response = wrapper + .into_response_builder() + .body(body) + .context("failed to construct http response")?; + + Ok(response) + } +} + +/// Start a hyper server with a service that calls an axum router in WASM, +/// and a kill receiver for stopping the server. +async fn run_until_stopped( + router: Router, + address: SocketAddr, + logs_tx: Sender>, + kill_rx: tokio::sync::oneshot::Receiver, + stopped_tx: broadcast::Sender<(StopReason, String)>, +) { + let make_service = make_service_fn(move |_conn| { + let router = router.clone(); + let logs_tx = logs_tx.clone(); + async move { + Ok::<_, Infallible>(service_fn(move |req: Request| { + let mut router = router.clone(); + let logs_tx = logs_tx.clone(); + async move { + Ok::<_, Infallible>(match router.handle_request(req, logs_tx).await { + Ok(res) => res, + Err(err) => { + error!("error sending request: {}", err); + Response::builder() + .status(hyper::http::StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .expect("building request with empty body should not fail") + } + }) + } + })) + } + }); + + let server = hyper::Server::bind(&address).serve(make_service); + + trace!("starting hyper server on: {}", &address); + tokio::select! { + _ = server => { + stopped_tx.send((StopReason::End, String::new())).unwrap(); + trace!("axum wasm server stopped"); + }, + message = kill_rx => { + match message { + Ok(msg) =>{ + stopped_tx.send((StopReason::Request, String::new())).unwrap(); + trace!("{msg}") + } , + Err(_) => { + stopped_tx + .send((StopReason::Crash, "the kill sender dropped".to_string())) + .unwrap(); + trace!("the sender dropped") + } + } + } + }; +} + +#[cfg(test)] +pub mod tests { + use std::process::Command; + + use super::*; + use hyper::{http::HeaderValue, Method, Request, StatusCode, Version}; + + // Compile axum wasm module + fn compile_module() { + Command::new("cargo") + .arg("build") + .arg("--target") + .arg("wasm32-wasi") + .current_dir("tests/resources/axum-wasm-expanded") + .spawn() + .unwrap() + .wait() + .unwrap(); + } + + #[tokio::test(flavor = "multi_thread")] + async fn axum() { + compile_module(); + + let router = RouterBuilder::new() + .unwrap() + .src("tests/resources/axum-wasm-expanded/target/wasm32-wasi/debug/shuttle_axum_expanded.wasm") + .build() + .unwrap(); + + let (tx, mut rx) = mpsc::channel(1); + + tokio::spawn(async move { + while let Some(log) = rx.recv().await { + println!("{log:?}"); + } + }); + + // GET /hello + let request: Request = Request::builder() + .method(Method::GET) + .version(Version::HTTP_11) + .uri("https://axum-wasm.example/hello") + .body(Body::empty()) + .unwrap(); + + let res = router + .clone() + .handle_request(request, tx.clone()) + .await + .unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + &hyper::body::to_bytes(res.into_body()) + .await + .unwrap() + .iter() + .cloned() + .collect::>() + .as_ref(), + b"Hello, World!" + ); + + // GET /goodbye + let request: Request = Request::builder() + .method(Method::GET) + .version(Version::HTTP_11) + .header("test", HeaderValue::from_static("goodbye")) + .uri("https://axum-wasm.example/goodbye") + .body(Body::from("Goodbye world body")) + .unwrap(); + + let res = router + .clone() + .handle_request(request, tx.clone()) + .await + .unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + &hyper::body::to_bytes(res.into_body()) + .await + .unwrap() + .iter() + .cloned() + .collect::>() + .as_ref(), + b"Goodbye, World!" + ); + + // GET /invalid + let request: Request = Request::builder() + .method(Method::GET) + .version(Version::HTTP_11) + .header("test", HeaderValue::from_static("invalid")) + .uri("https://axum-wasm.example/invalid") + .body(Body::empty()) + .unwrap(); + + let res = router + .clone() + .handle_request(request, tx.clone()) + .await + .unwrap(); + + assert_eq!(res.status(), StatusCode::NOT_FOUND); + + // POST /uppercase + let request: Request = Request::builder() + .method(Method::POST) + .version(Version::HTTP_11) + .header("test", HeaderValue::from_static("invalid")) + .uri("https://axum-wasm.example/uppercase") + .body("this should be uppercased".into()) + .unwrap(); + + let res = router.clone().handle_request(request, tx).await.unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + &hyper::body::to_bytes(res.into_body()) + .await + .unwrap() + .iter() + .cloned() + .collect::>() + .as_ref(), + b"THIS SHOULD BE UPPERCASED" + ); + } +} diff --git a/runtime/src/provisioner_factory.rs b/runtime/src/provisioner_factory.rs new file mode 100644 index 000000000..87d2f3a12 --- /dev/null +++ b/runtime/src/provisioner_factory.rs @@ -0,0 +1,124 @@ +use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; + +use async_trait::async_trait; +use shuttle_common::{ + claims::{Claim, ClaimService, InjectPropagation}, + database, + resource::{self, ResourceInfo}, + storage_manager::StorageManager, + DatabaseReadyInfo, +}; +use shuttle_proto::provisioner::{provisioner_client::ProvisionerClient, DatabaseRequest}; +use shuttle_service::{Environment, Factory, ServiceName}; +use tokio::sync::Mutex; +use tonic::{transport::Channel, Request}; +use tracing::{debug, info, trace}; + +/// A factory (service locator) which goes through the provisioner crate +pub struct ProvisionerFactory { + service_name: ServiceName, + storage_manager: Arc, + provisioner_client: ProvisionerClient>>, + secrets: BTreeMap, + env: Environment, + claim: Option, + resources: Arc>>, +} + +impl ProvisionerFactory { + pub(crate) fn new( + provisioner_client: ProvisionerClient>>, + service_name: ServiceName, + secrets: BTreeMap, + storage_manager: Arc, + env: Environment, + claim: Option, + resources: Arc>>, + ) -> Self { + Self { + provisioner_client, + service_name, + storage_manager, + secrets, + env, + claim, + resources, + } + } +} + +#[async_trait] +impl Factory for ProvisionerFactory { + async fn get_db_connection_string( + &mut self, + db_type: database::Type, + ) -> Result { + info!("Provisioning a {db_type}. This can take a while..."); + + if let Some(info) = self + .resources + .lock() + .await + .iter() + .find(|resource| resource.r#type == resource::Type::Database(db_type.clone())) + { + debug!("A database has already been provisioned for this deployment, so reusing it"); + + let resource = info.get_resource_info(); + return Ok(resource.connection_string_private()); + } + + let mut request = Request::new(DatabaseRequest { + project_name: self.service_name.to_string(), + db_type: Some(db_type.clone().into()), + }); + + if let Some(claim) = &self.claim { + request.extensions_mut().insert(claim.clone()); + } + + let response = self + .provisioner_client + .provision_database(request) + .await + .map_err(shuttle_service::error::CustomError::new)? + .into_inner(); + + let info: DatabaseReadyInfo = response.into(); + let conn_str = info.connection_string_private(); + + self.resources.lock().await.push(resource::Response { + r#type: resource::Type::Database(db_type), + data: serde_json::to_value(&info).expect("to convert DB info"), + }); + + info!("Done provisioning database"); + trace!("giving a DB connection string: {}", conn_str); + + Ok(conn_str) + } + + async fn get_secrets(&mut self) -> Result, shuttle_service::Error> { + Ok(self.secrets.clone()) + } + + fn get_service_name(&self) -> ServiceName { + self.service_name.clone() + } + + fn get_environment(&self) -> shuttle_service::Environment { + self.env + } + + fn get_build_path(&self) -> Result { + self.storage_manager + .service_build_path(self.service_name.as_str()) + .map_err(Into::into) + } + + fn get_storage_path(&self) -> Result { + self.storage_manager + .service_storage_path(self.service_name.as_str()) + .map_err(Into::into) + } +} diff --git a/runtime/tests/integration/helpers.rs b/runtime/tests/integration/helpers.rs new file mode 100644 index 000000000..0f2330237 --- /dev/null +++ b/runtime/tests/integration/helpers.rs @@ -0,0 +1,104 @@ +use std::{ + collections::HashMap, + net::{Ipv4Addr, SocketAddr}, + path::{Path, PathBuf}, +}; + +use anyhow::Result; +use async_trait::async_trait; +use shuttle_common::claims::{ClaimService, InjectPropagation}; +use shuttle_proto::{ + provisioner::{ + provisioner_server::{Provisioner, ProvisionerServer}, + DatabaseDeletionResponse, DatabaseRequest, DatabaseResponse, + }, + runtime::{self, runtime_client::RuntimeClient}, +}; +use shuttle_service::builder::{build_crate, Runtime}; +use tonic::{ + transport::{Channel, Server}, + Request, Response, Status, +}; + +pub struct TestRuntime { + pub runtime_client: RuntimeClient>>, + pub bin_path: String, + pub service_name: String, + pub runtime_address: SocketAddr, + pub secrets: HashMap, +} + +pub async fn spawn_runtime(project_path: String, service_name: &str) -> Result { + let provisioner_address = SocketAddr::new( + Ipv4Addr::LOCALHOST.into(), + portpicker::pick_unused_port().unwrap(), + ); + let runtime_port = portpicker::pick_unused_port().unwrap(); + let runtime_address = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), runtime_port); + + let (tx, _) = crossbeam_channel::unbounded(); + let runtime = build_crate(Path::new(&project_path), false, tx).await?; + + let secrets: HashMap = Default::default(); + + let (is_wasm, bin_path) = match runtime { + Runtime::Next(path) => (true, path), + Runtime::Alpha(path) => (false, path), + }; + + start_provisioner(DummyProvisioner, provisioner_address); + + // TODO: update this to work with shuttle-next projects, see cargo-shuttle local run + let runtime_path = || bin_path.clone(); + + let (_, runtime_client) = runtime::start( + is_wasm, + runtime::StorageManagerType::WorkingDir(PathBuf::from(project_path.clone())), + &format!("http://{}", provisioner_address), + None, + runtime_port, + runtime_path, + ) + .await?; + + Ok(TestRuntime { + runtime_client, + bin_path: bin_path + .into_os_string() + .into_string() + .expect("to convert path to string"), + service_name: service_name.to_string(), + runtime_address, + secrets, + }) +} + +/// A dummy provisioner for tests, a provisioner connection is required +/// to start a project runtime. +pub struct DummyProvisioner; + +fn start_provisioner(provisioner: DummyProvisioner, address: SocketAddr) { + tokio::spawn(async move { + Server::builder() + .add_service(ProvisionerServer::new(provisioner)) + .serve(address) + .await + }); +} + +#[async_trait] +impl Provisioner for DummyProvisioner { + async fn provision_database( + &self, + _request: Request, + ) -> Result, Status> { + panic!("did not expect any runtime test to use dbs") + } + + async fn delete_database( + &self, + _request: Request, + ) -> Result, Status> { + panic!("did not expect any runtime test to delete dbs") + } +} diff --git a/runtime/tests/integration/loader.rs b/runtime/tests/integration/loader.rs new file mode 100644 index 000000000..32abc0c04 --- /dev/null +++ b/runtime/tests/integration/loader.rs @@ -0,0 +1,45 @@ +use shuttle_proto::runtime::{LoadRequest, StartRequest, StopReason, SubscribeStopRequest}; + +use crate::helpers::{spawn_runtime, TestRuntime}; + +#[tokio::test] +async fn bind_panic() { + let project_path = format!("{}/tests/resources/bind-panic", env!("CARGO_MANIFEST_DIR")); + + let TestRuntime { + bin_path, + service_name, + secrets, + mut runtime_client, + runtime_address, + } = spawn_runtime(project_path, "bind-panic").await.unwrap(); + + let load_request = tonic::Request::new(LoadRequest { + path: bin_path, + service_name, + resources: Default::default(), + secrets, + }); + + let _ = runtime_client.load(load_request).await.unwrap(); + + let mut stream = runtime_client + .subscribe_stop(tonic::Request::new(SubscribeStopRequest {})) + .await + .unwrap() + .into_inner(); + + let start_request = StartRequest { + ip: runtime_address.to_string(), + }; + + runtime_client + .start(tonic::Request::new(start_request)) + .await + .unwrap(); + + let reason = stream.message().await.unwrap().unwrap(); + + assert_eq!(reason.reason, StopReason::Crash as i32); + assert_eq!(reason.message, "panic in bind"); +} diff --git a/runtime/tests/integration/main.rs b/runtime/tests/integration/main.rs new file mode 100644 index 000000000..e52d9c483 --- /dev/null +++ b/runtime/tests/integration/main.rs @@ -0,0 +1,2 @@ +pub mod helpers; +pub mod loader; diff --git a/runtime/tests/resources/axum-wasm-expanded/Cargo.toml b/runtime/tests/resources/axum-wasm-expanded/Cargo.toml new file mode 100644 index 000000000..45f77cbe1 --- /dev/null +++ b/runtime/tests/resources/axum-wasm-expanded/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] + +[package] +name = "shuttle-axum-expanded" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = [ "cdylib" ] + +[dependencies] +futures = "0.3.25" +shuttle-next = { path = "../../../../services/shuttle-next" } +tracing = "0.1.37" diff --git a/runtime/tests/resources/axum-wasm-expanded/src/lib.rs b/runtime/tests/resources/axum-wasm-expanded/src/lib.rs new file mode 100644 index 000000000..017cdda38 --- /dev/null +++ b/runtime/tests/resources/axum-wasm-expanded/src/lib.rs @@ -0,0 +1,106 @@ +use futures::TryStreamExt; +use shuttle_next::{ + body::BoxBody, + extract::BodyStream, + response::{IntoResponse, Response}, +}; +use tracing::debug; + +pub fn handle_request(req: shuttle_next::Request) -> shuttle_next::response::Response { + shuttle_next::block_on(app(req)) +} + +async fn app(request: shuttle_next::Request) -> shuttle_next::response::Response { + use shuttle_next::Service; + + let mut router = shuttle_next::Router::new() + .route("/hello", shuttle_next::routing::get(hello)) + .route("/goodbye", shuttle_next::routing::get(goodbye)) + .route("/uppercase", shuttle_next::routing::post(uppercase)); + + let response = router.call(request).await.unwrap(); + + response +} + +async fn hello() -> &'static str { + debug!("in hello()"); + "Hello, World!" +} + +async fn goodbye() -> &'static str { + debug!("in goodbye()"); + "Goodbye, World!" +} + +// Map the bytes of the body stream to uppercase and return the stream directly. +async fn uppercase(body: BodyStream) -> impl IntoResponse { + debug!("in uppercase()"); + let chunk_stream = body.map_ok(|chunk| { + chunk + .iter() + .map(|byte| byte.to_ascii_uppercase()) + .collect::>() + }); + Response::new(shuttle_next::body::StreamBody::new(chunk_stream)) +} + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn __SHUTTLE_Axum_call( + logs_fd: std::os::wasi::prelude::RawFd, + parts_fd: std::os::wasi::prelude::RawFd, + body_fd: std::os::wasi::prelude::RawFd, +) { + use shuttle_next::body::{Body, HttpBody}; + use shuttle_next::tracing_prelude::*; + use shuttle_next::Logger; + use std::io::{Read, Write}; + use std::os::wasi::io::FromRawFd; + + // file descriptor 2 for writing logs to + let logs_fd = unsafe { std::fs::File::from_raw_fd(logs_fd) }; + + shuttle_next::tracing_registry() + .with(Logger::new(logs_fd)) + .init(); // this sets the subscriber as the global default and also adds a compatibility layer for capturing `log::Record`s + + // file descriptor 3 for reading and writing http parts + let mut parts_fd = unsafe { std::fs::File::from_raw_fd(parts_fd) }; + + let reader = std::io::BufReader::new(&mut parts_fd); + + // deserialize request parts from rust messagepack + let wrapper: shuttle_next::RequestWrapper = shuttle_next::from_read(reader).unwrap(); + + // file descriptor 4 for reading and writing http body + let mut body_stream = unsafe { std::fs::File::from_raw_fd(body_fd) }; + + let mut reader = std::io::BufReader::new(&mut body_stream); + let mut body_buf = Vec::new(); + reader.read_to_end(&mut body_buf).unwrap(); + + let body = Body::from(body_buf); + + let request = wrapper + .into_request_builder() + .body(shuttle_next::body::boxed(body)) + .unwrap(); + + let res = handle_request(request); + + let (parts, mut body) = res.into_parts(); + + // wrap and serialize response parts as rmp + let response_parts = shuttle_next::ResponseWrapper::from(parts) + .into_rmp() + .expect("failed to serialize response parts"); + + // write response parts + parts_fd.write_all(&response_parts).unwrap(); + + // write body if there is one + if let Some(body) = shuttle_next::block_on(body.data()) { + body_stream.write_all(body.unwrap().as_ref()).unwrap(); + } +} diff --git a/service/tests/resources/bind-panic/Cargo.toml b/runtime/tests/resources/bind-panic/Cargo.toml similarity index 56% rename from service/tests/resources/bind-panic/Cargo.toml rename to runtime/tests/resources/bind-panic/Cargo.toml index eb3331848..05c792584 100644 --- a/service/tests/resources/bind-panic/Cargo.toml +++ b/runtime/tests/resources/bind-panic/Cargo.toml @@ -3,10 +3,9 @@ name = "bind-panic" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] [workspace] [dependencies] -shuttle-service = { path = "../../../" } +shuttle-runtime = { path = "../../../" } +tokio = { version = "1.22.0" } diff --git a/runtime/tests/resources/bind-panic/src/main.rs b/runtime/tests/resources/bind-panic/src/main.rs new file mode 100644 index 000000000..d7badcd69 --- /dev/null +++ b/runtime/tests/resources/bind-panic/src/main.rs @@ -0,0 +1,13 @@ +struct MyService; + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for MyService { + async fn bind(mut self, _: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { + panic!("panic in bind"); + } +} + +#[shuttle_runtime::main] +async fn bind_panic() -> Result { + Ok(MyService) +} diff --git a/service/Cargo.toml b/service/Cargo.toml index 8fc56086e..604d5775b 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shuttle-service" -version = "0.11.0" +version = "0.12.0" edition.workspace = true license.workspace = true repository.workspace = true @@ -11,45 +11,18 @@ homepage = "https://www.shuttle.rs" doctest = false [dependencies] -actix-web = { version = "4.2.1", optional = true } anyhow = { workspace = true } async-trait = { workspace = true } -axum = { workspace = true, optional = true } -bincode = { version = "1.3.3", optional = true } # TODO: debug the libgit2-sys conflict with cargo-edit when upgrading cargo to 0.66 cargo = { version = "0.65.0", optional = true } -cargo_metadata = "0.15.2" -chrono = { workspace = true } -crossbeam-channel = "0.5.6" -futures = { version = "0.3.25", features = ["std"] } -hyper = { workspace = true, features = ["server", "tcp", "http1"], optional = true } -libloading = { version = "0.7.4", optional = true } -num_cpus = { version = "1.14.0", optional = true } -pipe = "0.4.0" -poem = { version = "1.3.49", optional = true } -poise = { version = "0.5.2", optional = true } -rocket = { version = "0.5.0-rc.2", optional = true } -salvo = { version = "0.37.5", optional = true } -serde_json = { workspace = true } -serenity = { version = "0.11.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model"], optional = true } +cargo_metadata = { version = "0.15.2", optional = true } +crossbeam-channel = { version = "0.5.6", optional = true } +pipe = { version = "0.4.0", optional = true } +serde_json = { workspace = true, optional = true } strfmt = "0.2.2" -sync_wrapper = { version = "0.1.1", optional = true } thiserror = { workspace = true } -thruster = { version = "1.3.0", optional = true } -tide = { version = "0.16.0", optional = true } -tokio = { version = "=1.22.0", features = ["rt", "rt-multi-thread", "sync"] } -tower = { workspace = true, features = ["make"], optional = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter"] } -uuid = { workspace = true, features = ["v4"] } -warp = { version = "0.3.3", optional = true } - -# Tide does not have tokio support. So make sure async-std is compatible with tokio -# https://github.com/http-rs/tide/issues/791 -[dependencies.async-std] -version = "1.12.0" -optional = true -features = ["tokio1"] +tokio = { version = "1.26.0", features = ["sync"], optional = true } +tracing = { workspace = true, optional = true } [dependencies.shuttle-codegen] workspace = true @@ -57,27 +30,13 @@ optional = true [dependencies.shuttle-common] workspace = true +features = ["service"] [dev-dependencies] -portpicker = { workspace = true } -sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres"] } -tokio = { version = "1.22.0", features = ["macros"] } -uuid = { workspace = true, features = ["v4"] } +tokio = { version = "1.26.0", features = ["macros", "rt"] } [features] default = ["codegen"] -codegen = ["shuttle-codegen"] -loader = ["cargo", "libloading"] - -web-actix-web = ["actix-web", "num_cpus"] -web-axum = ["axum", "sync_wrapper"] -web-poem = ["poem"] -web-rocket = ["rocket"] -web-salvo = ["salvo"] -web-thruster = ["thruster"] -web-tide = ["tide", "async-std"] -web-tower = ["tower", "hyper"] -web-warp = ["warp"] -bot-poise = ["poise"] -bot-serenity = ["serenity"] +codegen = ["shuttle-codegen/frameworks"] +builder = ["cargo", "cargo_metadata", "crossbeam-channel", "pipe", "tokio", "tracing"] diff --git a/service/src/builder.rs b/service/src/builder.rs new file mode 100644 index 000000000..5c5265130 --- /dev/null +++ b/service/src/builder.rs @@ -0,0 +1,221 @@ +use std::path::{Path, PathBuf}; + +use anyhow::{anyhow, bail, Context}; +use cargo::core::compiler::{CompileKind, CompileMode, CompileTarget, MessageFormat}; +use cargo::core::{Manifest, Shell, Summary, Verbosity, Workspace}; +use cargo::ops::{clean, compile, CleanOptions, CompileOptions}; +use cargo::util::homedir; +use cargo::util::interning::InternedString; +use cargo::Config; +use cargo_metadata::Message; +use crossbeam_channel::Sender; +use pipe::PipeWriter; +use tracing::{error, trace}; + +use crate::NEXT_NAME; + +/// How to run/build the project +pub enum Runtime { + Next(PathBuf), + Alpha(PathBuf), +} + +/// Given a project directory path, builds the crate +pub async fn build_crate( + project_path: &Path, + release_mode: bool, + tx: Sender, +) -> anyhow::Result { + let (read, write) = pipe::pipe(); + let project_path = project_path.to_owned(); + + // This needs to be on a separate thread, else deployer will block (reason currently unknown :D) + tokio::task::spawn_blocking(move || { + trace!("started thread to to capture build output stream"); + for message in Message::parse_stream(read) { + trace!(?message, "parsed cargo message"); + match message { + Ok(message) => { + if let Err(error) = tx.send(message) { + error!("failed to send cargo message on channel: {error}"); + } + } + Err(error) => { + error!("failed to parse cargo message: {error}"); + } + } + } + }); + + let config = get_config(write)?; + let manifest_path = project_path.join("Cargo.toml"); + let mut ws = Workspace::new(&manifest_path, &config)?; + + let current = ws.current_mut().map_err(|_| anyhow!("A Shuttle project cannot have a virtual manifest file - please ensure the `package` table is present in your Cargo.toml file."))?; + + let summary = current.manifest_mut().summary_mut(); + let is_next = is_next(summary); + + if !is_next { + ensure_binary(current.manifest())?; + } else { + ensure_cdylib(current.manifest_mut())?; + } + + check_no_panic(&ws)?; + + let opts = get_compile_options(&config, release_mode, is_next)?; + let compilation = compile(&ws, &opts)?; + + Ok(if is_next { + Runtime::Next(compilation.cdylibs[0].path.clone()) + } else { + Runtime::Alpha(compilation.binaries[0].path.clone()) + }) +} + +pub fn clean_crate(project_path: &Path, release_mode: bool) -> anyhow::Result> { + let (read, write) = pipe::pipe(); + let project_path = project_path.to_owned(); + + tokio::task::spawn_blocking(move || { + let config = get_config(write).unwrap(); + let manifest_path = project_path.join("Cargo.toml"); + let ws = Workspace::new(&manifest_path, &config).unwrap(); + + let requested_profile = if release_mode { + InternedString::new("release") + } else { + InternedString::new("dev") + }; + + let opts = CleanOptions { + config: &config, + spec: Vec::new(), + targets: Vec::new(), + requested_profile, + profile_specified: true, + doc: false, + }; + + clean(&ws, &opts).unwrap(); + }); + + let mut lines = Vec::new(); + + for message in Message::parse_stream(read) { + trace!(?message, "parsed cargo message"); + match message { + Ok(Message::TextLine(line)) => { + lines.push(line); + } + Ok(_) => {} + Err(error) => { + error!("failed to parse cargo message: {error}"); + } + } + } + + Ok(lines) +} + +/// Get the default compile config with output redirected to writer +pub fn get_config(writer: PipeWriter) -> anyhow::Result { + let mut shell = Shell::from_write(Box::new(writer)); + shell.set_verbosity(Verbosity::Normal); + let cwd = std::env::current_dir() + .with_context(|| "couldn't get the current directory of the process")?; + let homedir = homedir(&cwd).ok_or_else(|| { + anyhow!( + "Cargo couldn't find your home directory. \ + This probably means that $HOME was not set." + ) + })?; + + Ok(Config::new(shell, cwd, homedir)) +} + +/// Get options to compile in build mode +fn get_compile_options( + config: &Config, + release_mode: bool, + wasm: bool, +) -> anyhow::Result { + let mut opts = CompileOptions::new(config, CompileMode::Build)?; + opts.build_config.message_format = MessageFormat::Json { + render_diagnostics: false, + short: false, + ansi: false, + }; + + opts.build_config.requested_profile = if release_mode { + InternedString::new("release") + } else { + InternedString::new("dev") + }; + + // This sets the max workers for cargo build to 4 for release mode (aka deployment), + // but leaves it as default (num cpus) for local runs + if release_mode { + opts.build_config.jobs = 4 + }; + + opts.build_config.requested_kinds = vec![if wasm { + CompileKind::Target(CompileTarget::new("wasm32-wasi")?) + } else { + CompileKind::Host + }]; + + Ok(opts) +} + +fn is_next(summary: &Summary) -> bool { + summary + .dependencies() + .iter() + .any(|dependency| dependency.package_name() == NEXT_NAME) +} + +/// Make sure the project is a binary for alpha projects. +fn ensure_binary(manifest: &Manifest) -> anyhow::Result<()> { + if manifest.targets().iter().any(|target| target.is_bin()) { + Ok(()) + } else { + bail!("Your Shuttle project must be a binary.") + } +} + +/// Make sure "cdylib" is set for shuttle-next projects, else set it if possible. +fn ensure_cdylib(manifest: &mut Manifest) -> anyhow::Result<()> { + if let Some(target) = manifest + .targets_mut() + .iter_mut() + .find(|target| target.is_lib()) + { + if !target.is_cdylib() { + *target = cargo::core::manifest::Target::lib_target( + target.name(), + vec![cargo::core::compiler::CrateType::Cdylib], + target.src_path().path().unwrap().to_path_buf(), + target.edition(), + ); + } + + Ok(()) + } else { + bail!("Your Shuttle project must be a library. Please add `[lib]` to your Cargo.toml file.") + } +} + +/// Ensure `panic = "abort"` is not set: +fn check_no_panic(ws: &Workspace) -> anyhow::Result<()> { + if let Some(profiles) = ws.profiles() { + for profile in profiles.get_all().values() { + if profile.panic.as_deref() == Some("abort") { + return Err(anyhow!("Your Shuttle project cannot have panics that abort. Please ensure your Cargo.toml does not contain `panic = \"abort\"` for any profiles.")); + } + } + } + + Ok(()) +} diff --git a/service/src/error.rs b/service/src/error.rs index 2094beefe..980d219da 100644 --- a/service/src/error.rs +++ b/service/src/error.rs @@ -8,8 +8,6 @@ pub enum Error { Io(#[from] std::io::Error), #[error("Database error: {0}")] Database(String), - #[error("Secret error: {0}")] - Secret(String), #[error("Panic occurred in shuttle_service::main`: {0}")] BuildPanic(String), #[error("Panic occurred in `Service::bind`: {0}")] @@ -21,12 +19,3 @@ pub enum Error { } pub type CustomError = anyhow::Error; - -// This is implemented manually as defining `Error::Database(#[from] mongodb::error::Error)` resulted in a -// segfault even with a feature guard. -#[cfg(feature = "mongodb-integration")] -impl From for Error { - fn from(e: mongodb::error::Error) -> Self { - Error::Database(e.to_string()) - } -} diff --git a/service/src/lib.rs b/service/src/lib.rs index 209ab909f..36439ae54 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -1,235 +1,11 @@ -#![doc( - html_logo_url = "https://raw.githubusercontent.com/shuttle-hq/shuttle/main/assets/logo-square-transparent.png", - html_favicon_url = "https://raw.githubusercontent.com/shuttle-hq/shuttle/main/assets/favicon.ico" -)] -//! # Shuttle - Deploy Rust apps with a single Cargo subcommand -//!
-//! -//!
-//! -//! Hello, and welcome to the shuttle API documentation! -//! -//! Shuttle is an open-source app platform that uses traits and annotations to configure your backend deployments. -//! -//! ## Usage -//! Start by installing the [`cargo shuttle`](https://docs.rs/crate/cargo-shuttle/latest) subcommand by running the following in a terminal: -//! -//! ```bash -//! $ cargo install cargo-shuttle -//! ``` -//! -//! Now that shuttle is installed, you can initialize a project with Rocket boilerplate: -//! ```bash -//! $ cargo shuttle init --rocket my-rocket-app -//! ``` -//! -//! By looking at the `Cargo.toml` file of the generated `my-rocket-app` project you will see it has been made to -//! be a library crate with a `shuttle-service` dependency with the `web-rocket` feature on the `shuttle-service` dependency. -//! -//! ```toml -//! shuttle-service = { version = "0.11.0", features = ["web-rocket"] } -//! ``` -//! -//! A boilerplate code for your rocket project can also be found in `src/lib.rs`: -//! -//! ```rust,no_run -//! #[macro_use] -//! extern crate rocket; -//! -//! use shuttle_service::ShuttleRocket; -//! -//! #[get("/hello")] -//! fn hello() -> &'static str { -//! "Hello, world!" -//! } -//! -//! #[shuttle_service::main] -//! async fn init() -> ShuttleRocket { -//! let rocket = rocket::build().mount("/", routes![hello]); -//! -//! Ok(rocket) -//! } -//! ``` -//! -//! See the [shuttle_service::main][main] macro for more information on supported services - such as `axum`. -//! Or look at [more complete examples](https://github.com/shuttle-hq/examples), but -//! take note that the examples may update before official releases. -//! -//! ## Running locally -//! To test your app locally before deploying, use: -//! -//! ```bash -//! $ cargo shuttle run -//! ``` -//! -//! You should see your app build and start on the default port 8000. You can test this using; -//! -//! ```bash -//! $ curl http://localhost:8000/hello -//! Hello, world! -//! ``` -//! -//! ## Deploying -//! -//! You can deploy your service with the [`cargo shuttle`](https://docs.rs/crate/cargo-shuttle/latest) subcommand too. -//! But, you will need to authenticate with the shuttle service first using: -//! -//! ```bash -//! $ cargo shuttle login -//! ``` -//! -//! this will open a browser window and prompt you to connect using your GitHub account. -//! -//! Before you can deploy, you have to create a project. This will start a deployer container for your -//! project under the hood, ensuring isolation from other users' projects. -//! -//! ```bash -//! $ cargo shuttle project new -//! $ cargo shuttle project status // until the project is "ready" -//! ``` -//! -//! Then, deploy the service with: -//! -//! ```bash -//! $ cargo shuttle deploy -//! ``` -//! -//! Your service will immediately be available at `{crate_name}.shuttleapp.rs`. For example: -//! -//! ```bash -//! $ curl https://my-rocket-app.shuttleapp.rs/hello -//! Hello, world! -//! ``` -//! -//! ## Using `sqlx` -//! -//! Here is a quick example to deploy a service that uses a postgres database and [sqlx](http://docs.rs/sqlx): -//! -//! Add `shuttle-shared-db` as a dependency with the `postgres` feature, and add `sqlx` as a dependency with the `runtime-tokio-native-tls` and `postgres` features inside `Cargo.toml`: -//! -//! ```toml -//! shuttle-shared-db = { version = "0.11.0", features = ["postgres"] } -//! sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres"] } -//! ``` -//! -//! Now update the `#[shuttle_service::main]` function to take in a `PgPool`: -//! -//! ```rust,no_run -//! #[macro_use] -//! extern crate rocket; -//! -//! use rocket::State; -//! use sqlx::PgPool; -//! use shuttle_service::ShuttleRocket; -//! -//! struct MyState(PgPool); -//! -//! #[get("/hello")] -//! fn hello(state: &State) -> &'static str { -//! // Do things with `state.0`... -//! "Hello, Postgres!" -//! } -//! -//! #[shuttle_service::main] -//! async fn rocket(#[shuttle_shared_db::Postgres] pool: PgPool) -> ShuttleRocket { -//! let state = MyState(pool); -//! let rocket = rocket::build().manage(state).mount("/", routes![hello]); -//! -//! Ok(rocket) -//! } -//! ``` -//! -//! For a local run, shuttle will automatically provision a Postgres instance inside a [Docker](https://www.docker.com/) container on your machine and connect it to the `PgPool`. -//! -//! For deploys, shuttle will provision a database for your application and connect it to the `PgPool` on your behalf. -//! -//! To learn more about shuttle managed resources, see [shuttle_service::main][main#getting-shuttle-managed-resources]. -//! -//! ## Configuration -//! -//! The `cargo shuttle` command can be customised by creating a `Shuttle.toml` in the same location as your `Cargo.toml`. -//! -//! ##### Change the name of your service -//! -//! To have your service deployed with a different name, add a `name` entry in the `Shuttle.toml`: -//! -//! ```toml -//! name = "hello-world" -//! ``` -//! -//! If the `name` key is not specified, the service's name will be the same as the crate's name. -//! -//! Alternatively, you can override the project name on the command-line, by passing the --name argument to any subcommand like so: -//! -//! ```bash -//! cargo shuttle deploy --name=$PROJECT_NAME -//! ``` -//! -//! ##### Using Podman instead of Docker -//! If you are using [Podman](https://podman.io/) instead of Docker, then `cargo shuttle run` will give -//! `got unexpected error while inspecting docker container: error trying to connect: No such file or directory` error. -//! -//! To fix this error you will need to expose a rootless socket for Podman first. This can be done using: -//! -//! ```bash -//! podman system service --time=0 unix:///tmp/podman.sock -//! ``` -//! -//! Now set the `DOCKER_HOST` environment variable to point to this socket using: -//! -//! ```bash -//! export DOCKER_HOST=unix:///tmp/podman.sock -//! ``` -//! -//! Now all `cargo shuttle run` commands will work against Podman. -//! -//! ## Getting API keys -//! -//! After you've installed the [cargo-shuttle](https://docs.rs/crate/cargo-shuttle/latest) command, run: -//! -//! ```bash -//! $ cargo shuttle login -//! ``` -//! -//! this will open a browser window and prompt you to connect using your GitHub account. -//! -//! ## We're in alpha 🤗 -//! -//! Thanks for using shuttle! We're very happy to have you with us! -//! -//! During our alpha period, API keys are completely free and you can deploy as many services as you want. -//! -//! Just keep in mind that there may be some kinks that require us to take all deployments down once in a while. In certain circumstances we may also have to delete all the data associated with those deployments. -//! -//! To stay updated with the release status of shuttle, [join our Discord](https://discord.gg/shuttle)! -//! -//! ## Join Discord -//! -//! If you have any questions, [join our Discord server](https://discord.gg/shuttle). There's always someone on there that can help! -//! -//! You can also [open an issue or a discussion on GitHub](https://github.com/shuttle-hq/shuttle). -//! - use std::collections::BTreeMap; -use std::future::Future; use std::net::SocketAddr; use std::path::PathBuf; -use std::pin::Pin; - -pub use async_trait::async_trait; -// Pub uses by `codegen` -pub use anyhow::Context; -pub use strfmt::strfmt; -pub use tokio::runtime::Runtime; -pub use tracing; -pub use tracing_subscriber; +use async_trait::async_trait; pub mod error; -pub use error::Error; - -mod logger; -pub use logger::Logger; +pub use error::{CustomError, Error}; pub use shuttle_common::database; @@ -242,56 +18,54 @@ extern crate shuttle_codegen; /// The simplest usage is when your service does not require any shuttle managed resources, so you only need to return a shuttle supported service: /// /// ```rust,no_run -/// use shuttle_service::ShuttleRocket; +/// use shuttle_rocket::ShuttleRocket; /// -/// #[shuttle_service::main] +/// #[shuttle_rocket::main] /// async fn rocket() -> ShuttleRocket { /// let rocket = rocket::build(); /// -/// Ok(rocket) +/// Ok(rocket.into()) /// } /// ``` /// /// ## shuttle supported services -/// The following types can be returned from a `#[shuttle_service::main]` function and enjoy first class service support in shuttle. Be sure to also enable the correct feature on -/// `shuttle-service` in `Cargo.toml` for the type to be recognized. -/// -/// | Return type | Feature flag | Service | Version | Example | -/// | ------------------------------------- | ------------ | ------------------------------------------- | ---------- | ----------------------------------------------------------------------------------- | -/// | `ShuttleRocket` | web-rocket | [rocket](https://docs.rs/rocket/0.5.0-rc.2) | 0.5.0-rc.2 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/rocket/hello-world) | -/// | `ShuttleAxum` | web-axum | [axum](https://docs.rs/axum/0.5) | 0.5 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/axum/hello-world) | -/// | `ShuttleSalvo` | web-salvo | [salvo](https://docs.rs/salvo/0.34.3) | 0.34.3 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/salvo/hello-world) | -/// | `ShuttleTide` | web-tide | [tide](https://docs.rs/tide/0.16.0) | 0.16.0 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/tide/hello-world) | -/// | `ShuttlePoem` | web-poem | [poem](https://docs.rs/poem/1.3.35) | 1.3.35 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/poem/hello-world) | -/// | `Result` | web-tower | [tower](https://docs.rs/tower/0.4.12) | 0.14.12 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/tower/hello-world) | -/// | `ShuttleSerenity` | bot-serenity | [serenity](https://docs.rs/serenity/0.11.5) | 0.11.5 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/serenity/hello-world) | -/// | `ShuttlePoise` | bot-poise | [poise](https://docs.rs/poise/0.5.2) | 0.5.2 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/poise/hello-world) | -/// | `ShuttleActixWeb` | web-actix-web| [actix-web](https://docs.rs/actix-web/4.2.1)| 4.2.1 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/actix-web/hello-world) | - +/// The following types can be returned from a `#[shuttle_service::main]` function and enjoy first class service support in shuttle. +/// +/// | Return type | Crate | Service | Version | Example | +/// | ------------------------------------- |-------------------------------------------------------------- | ------------------------------------------- | ---------- | ----------------------------------------------------------------------------------- | +/// | `ShuttleActixWeb` |[shuttle-actix-web](https://crates.io/crates/shuttle-actix-web)| [actix-web](https://docs.rs/actix-web/4.3) | 4.3 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/actix-web/hello-world) | +/// | `ShuttleAxum` |[shuttle-axum](https://crates.io/crates/shuttle-axum) | [axum](https://docs.rs/axum/0.6) | 0.5 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/axum/hello-world) | +/// | `ShuttlePoem` |[shuttle-poem](https://crates.io/crates/shuttle-poem) | [poem](https://docs.rs/poem/1.3) | 1.3 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/poem/hello-world) | +/// | `ShuttlePoise` |[shuttle-poise](https://crates.io/crates/shuttle-poise) | [poise](https://docs.rs/poise/0.5) | 0.5 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/poise/hello-world) | +/// | `ShuttleRocket` |[shuttle-rocket](https://crates.io/crates/shuttle-rocket) | [rocket](https://docs.rs/rocket/0.5.0-rc.2) | 0.5.0-rc.2 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/rocket/hello-world) | +/// | `ShuttleSalvo` |[shuttle-salvo](https://crates.io/crates/shuttle-salvo) | [salvo](https://docs.rs/salvo/0.37) | 0.37 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/salvo/hello-world) | +/// | `ShuttleSerenity` |[shuttle-serenity](https://crates.io/crates/shuttle-serenity | [serenity](https://docs.rs/serenity/0.11) | 0.11 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/serenity/hello-world) | +/// | `ShuttleThruster` |[shuttle-thruster](https://crates.io/crates/shuttle-thruster) | [thruster](https://docs.rs/thruster/1.3) | 1.3 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/thruster/hello-world) | +/// | `ShuttleTower` |[shuttle-tower](https://crates.io/crates/shuttle-tower) | [tower](https://docs.rs/tower/0.4) | 0.4 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/tower/hello-world) | +/// | `ShuttleTide` |[shuttle-tide](https://crates.io/crates/shuttle-tide) | [tide](https://docs.rs/tide/0.16) | 0.16 | [GitHub](https://github.com/shuttle-hq/examples/tree/main/tide/hello-world) | /// /// # Getting shuttle managed resources -/// Shuttle is able to manage resource dependencies for you. These resources are passed in as inputs to your `#[shuttle_service::main]` function and are configured using attributes: +/// Shuttle is able to manage resource dependencies for you. These resources are passed in as inputs to your `#[shuttle_runtime::main]` function and are configured using attributes: /// ```rust,no_run /// use sqlx::PgPool; -/// use shuttle_service::ShuttleRocket; +/// use shuttle_rocket::ShuttleRocket; /// /// struct MyState(PgPool); /// -/// #[shuttle_service::main] +/// #[shuttle_runtime::main] /// async fn rocket(#[shuttle_shared_db::Postgres] pool: PgPool) -> ShuttleRocket { /// let state = MyState(pool); /// let rocket = rocket::build().manage(state); /// -/// Ok(rocket) +/// Ok(rocket.into()) /// } /// ``` /// /// More [shuttle managed resources can be found here](https://github.com/shuttle-hq/shuttle/tree/main/resources) pub use shuttle_codegen::main; -use tokio::task::JoinHandle; -#[cfg(feature = "loader")] -pub mod loader; +#[cfg(feature = "builder")] +pub mod builder; pub use shuttle_common::{deployment::Environment, project::ProjectName as ServiceName}; @@ -334,14 +108,11 @@ pub trait Factory: Send + Sync { /// You may want to create your own managed resource by implementing this trait for some builder `B` to construct resource `T`. [`Factory`] can be used to provision resources /// on shuttle's servers if your resource will need any. /// -/// The biggest thing to look out for is that your resource object might panic when it crosses the boundary between the shuttle's backend runtime and the runtime -/// of services. These resources should be created on the passed in `runtime` for this trait to prevent these panics. -/// -/// Your resource will be available on a [shuttle_service::main][main] function as follow: +/// Your resource will be available on a [shuttle_runtime::main][main] function as follow: /// ``` -/// #[shuttle_service::main] +/// #[shuttle_runtime::main] /// async fn my_service([custom_resource_crate::namespace::B] custom_resource: T) -/// -> shuttle_service::ShuttleAxum {} +/// -> shuttle_axum::ShuttleAxum {} /// ``` /// /// Here `custom_resource_crate::namespace` is the crate and namespace to a builder `B` that implements [`ResourceBuilder`] to create resource `T`. @@ -376,7 +147,6 @@ pub trait Factory: Send + Sync { /// async fn build( /// self, /// factory: &mut dyn Factory, -/// _runtime: &Runtime, /// ) -> Result { /// Ok(Resource { name: self.name }) /// } @@ -385,303 +155,27 @@ pub trait Factory: Send + Sync { /// /// Then using this resource in a service: /// ``` -/// #[shuttle_service::main] +/// #[shuttle_runtime::main] /// async fn my_service( /// [custom_resource_crate::Builder(name = "John")] resource: custom_resource_crate::Resource /// ) -/// -> shuttle_service::ShuttleAxum {} +/// -> shuttle_axum::ShuttleAxum {} /// ``` #[async_trait] pub trait ResourceBuilder { fn new() -> Self; - async fn build(self, factory: &mut dyn Factory, runtime: &Runtime) -> Result; + async fn build(self, factory: &mut dyn Factory) -> Result; } -/// A tokio handle the service was started on -pub type ServeHandle = JoinHandle>; - /// The core trait of the shuttle platform. Every crate deployed to shuttle needs to implement this trait. /// /// Use the [main][main] macro to expose your implementation to the deployment backend. -// -// TODO: our current state machine in the api crate stores this service and can move it across -// threads (handlers) causing `Service` to need `Sync`. We should remove this restriction #[async_trait] -pub trait Service: Send + Sync { +pub trait Service: Send { /// This function is run exactly once on each instance of a deployment. /// /// The deployer expects this instance of [Service][Service] to bind to the passed [SocketAddr][SocketAddr]. - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error>; -} - -/// This function is generated by our codegen. It uses the factory to get other services and instantiate them on -/// the correct tokio runtime. This function also sets the runtime logger. The output is a future where `T` -/// should implement [Service]. -pub type StateBuilder = - for<'a> fn( - &'a mut dyn Factory, - &'a Runtime, - Logger, - ) -> Pin> + Send + 'a>>; - -/// This function is generated by codegen to ensure binding happens on the other side of the FFI and on the correct -/// tokio runtime. -pub type Binder = for<'a> fn(Box, SocketAddr, &'a Runtime) -> ServeHandle; - -// Make sure every crate used in this struct has its version pinned down to prevent segmentation faults when crossing the FFI. -// Your future self will thank you! -// See https://github.com/shuttle-hq/shuttle/pull/348 -#[allow(dead_code)] -pub struct Bootstrapper { - service: Option>, - builder: Option>>, - binder: Binder, - runtime: Option, -} - -impl Bootstrapper { - pub fn new(builder: StateBuilder>, binder: Binder, runtime: Runtime) -> Self { - Self { - service: None, - builder: Some(builder), - binder, - runtime: Some(runtime), - } - } - - #[cfg(feature = "loader")] - async fn bootstrap(&mut self, factory: &mut dyn Factory, logger: Logger) -> Result<(), Error> { - if let Some(builder) = self.builder.take() { - let service = builder(factory, self.runtime.as_ref().unwrap(), logger).await?; - self.service = Some(service); - } - - Ok(()) - } - - #[cfg(feature = "loader")] - fn into_handle(mut self, addr: SocketAddr) -> Result { - let service = self.service.take().expect("service has already been bound"); - - let handle = (self.binder)(service, addr, self.runtime.as_ref().unwrap()); - - Ok(handle) - } -} - -impl Drop for Bootstrapper { - fn drop(&mut self) { - if let Some(runtime) = self.runtime.take() { - // TODO: find a way to drop the runtime - std::mem::forget(runtime); - } - } -} - -#[cfg(feature = "web-rocket")] -#[async_trait] -impl Service for rocket::Rocket { - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { - let shutdown = rocket::config::Shutdown { - ctrlc: false, - ..rocket::config::Shutdown::default() - }; - - let config = self - .figment() - .clone() - .merge((rocket::Config::ADDRESS, addr.ip())) - .merge((rocket::Config::PORT, addr.port())) - .merge((rocket::Config::LOG_LEVEL, rocket::config::LogLevel::Off)) - .merge((rocket::Config::SHUTDOWN, shutdown)); - - let _rocket = self - .configure(config) - .launch() - .await - .map_err(error::CustomError::new)?; - - Ok(()) - } -} - -#[cfg(feature = "web-rocket")] -pub type ShuttleRocket = Result, Error>; - -#[cfg(feature = "web-poem")] -#[async_trait] -impl Service for T -where - T: poem::Endpoint + Sync + Send + 'static, -{ - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { - poem::Server::new(poem::listener::TcpListener::bind(addr)) - .run(self) - .await - .map_err(error::CustomError::new)?; - - Ok(()) - } -} - -#[cfg(feature = "web-poem")] -pub type ShuttlePoem = Result; - -#[cfg(feature = "web-warp")] -#[async_trait] -impl Service for T -where - T: Send + Sync + Clone + 'static + warp::Filter, - T::Extract: warp::reply::Reply, -{ - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { - warp::serve(*self).run(addr).await; - Ok(()) - } -} - -#[cfg(feature = "web-warp")] -pub type ShuttleWarp = Result, Error>; - -#[cfg(feature = "web-axum")] -#[async_trait] -impl Service for sync_wrapper::SyncWrapper { - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { - let router = self.into_inner(); - - axum::Server::bind(&addr) - .serve(router.into_make_service()) - .await - .map_err(error::CustomError::new)?; - - Ok(()) - } -} - -#[cfg(feature = "web-actix-web")] -#[async_trait] -impl Service for F -where - F: FnOnce(&mut actix_web::web::ServiceConfig) + Sync + Send + Clone + 'static, -{ - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), Error> { - // Start a worker for each cpu, but no more than 4. - let worker_count = num_cpus::get().max(4); - - let srv = actix_web::HttpServer::new(move || actix_web::App::new().configure(self.clone())) - .workers(worker_count) - .bind(addr)? - .run(); - srv.await.map_err(error::CustomError::new)?; - - Ok(()) - } -} -#[cfg(feature = "web-actix-web")] -pub type ShuttleActixWeb = Result; - -#[cfg(feature = "web-axum")] -pub type ShuttleAxum = Result, Error>; - -#[cfg(feature = "web-salvo")] -#[async_trait] -impl Service for salvo::Router { - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { - salvo::Server::new(salvo::listener::TcpListener::bind(addr)) - .serve(self) - .await; - - Ok(()) - } -} - -#[cfg(feature = "web-salvo")] -pub type ShuttleSalvo = Result; - -#[cfg(feature = "web-thruster")] -#[async_trait] -impl Service for T -where - T: thruster::ThrusterServer + Sync + Send + 'static, -{ - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { - self.build(&addr.ip().to_string(), addr.port()).await; - - Ok(()) - } -} - -#[cfg(feature = "web-thruster")] -pub type ShuttleThruster = Result; - -#[cfg(feature = "web-tide")] -#[async_trait] -impl Service for tide::Server -where - T: Clone + Send + Sync + 'static, -{ - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { - self.listen(addr).await.map_err(error::CustomError::new)?; - - Ok(()) - } -} - -#[cfg(feature = "web-tide")] -pub type ShuttleTide = Result, Error>; - -#[cfg(feature = "web-tower")] -#[async_trait] -impl Service for T -where - T: tower::Service, Response = hyper::Response> - + Clone - + Send - + Sync - + 'static, - T::Error: std::error::Error + Send + Sync, - T::Future: std::future::Future + Send + Sync, -{ - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { - let shared = tower::make::Shared::new(self); - hyper::Server::bind(&addr) - .serve(shared) - .await - .map_err(error::CustomError::new)?; - - Ok(()) - } + async fn bind(mut self, addr: SocketAddr) -> Result<(), error::Error>; } -#[cfg(feature = "bot-serenity")] -#[async_trait] -impl Service for serenity::Client { - async fn bind(mut self: Box, _addr: SocketAddr) -> Result<(), error::Error> { - self.start().await.map_err(error::CustomError::new)?; - - Ok(()) - } -} - -#[cfg(feature = "bot-serenity")] -pub type ShuttleSerenity = Result; - -#[cfg(feature = "bot-poise")] -#[async_trait] -impl Service for std::sync::Arc> -where - T: std::marker::Send + std::marker::Sync + 'static, - E: std::marker::Send + std::marker::Sync + 'static, -{ - async fn bind(mut self: Box, _addr: SocketAddr) -> Result<(), error::Error> { - self.start().await.map_err(error::CustomError::new)?; - - Ok(()) - } -} - -#[cfg(feature = "bot-poise")] -pub type ShuttlePoise = Result>, Error>; - -pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -pub const NAME: &str = env!("CARGO_PKG_NAME"); +pub const NEXT_NAME: &str = "shuttle-next"; diff --git a/service/src/loader.rs b/service/src/loader.rs deleted file mode 100644 index 11f6ffaf6..000000000 --- a/service/src/loader.rs +++ /dev/null @@ -1,327 +0,0 @@ -use std::any::Any; -use std::ffi::OsStr; -use std::net::SocketAddr; -use std::panic::AssertUnwindSafe; -use std::path::{Path, PathBuf}; - -use anyhow::{anyhow, Context}; -use cargo::core::compiler::{CompileMode, MessageFormat}; -use cargo::core::{Manifest, PackageId, Shell, Summary, Verbosity, Workspace}; -use cargo::ops::{clean, compile, CleanOptions, CompileOptions}; -use cargo::util::interning::InternedString; -use cargo::util::{homedir, ToSemver}; -use cargo::Config; -use cargo_metadata::Message; -use crossbeam_channel::Sender; -use libloading::{Library, Symbol}; -use pipe::PipeWriter; -use thiserror::Error as ThisError; -use tracing::{error, trace}; - -use futures::FutureExt; -use uuid::Uuid; - -use crate::error::CustomError; -use crate::{logger, Bootstrapper, NAME, VERSION}; -use crate::{Error, Factory, ServeHandle}; - -const ENTRYPOINT_SYMBOL_NAME: &[u8] = b"_create_service\0"; - -type CreateService = unsafe extern "C" fn() -> *mut Bootstrapper; - -#[derive(Debug, ThisError)] -pub enum LoaderError { - #[error("failed to load library: {0}")] - Load(libloading::Error), - #[error("failed to find the shuttle entrypoint. Did you use the provided shuttle macros?")] - GetEntrypoint(libloading::Error), -} - -pub type LoadedService = (ServeHandle, Library); - -pub struct Loader { - bootstrapper: Bootstrapper, - so: Library, -} - -impl Loader { - /// Dynamically load from a `.so` file a value of a type implementing the - /// [`Service`][crate::Service] trait. Relies on the `.so` library having an `extern "C"` - /// function called `ENTRYPOINT_SYMBOL_NAME`, likely automatically generated - /// using the [`shuttle_service::main`][crate::main] macro. - pub fn from_so_file>(so_path: P) -> Result { - trace!(so_path = so_path.as_ref().to_str(), "loading .so path"); - unsafe { - let lib = Library::new(so_path).map_err(LoaderError::Load)?; - - let entrypoint: Symbol = lib - .get(ENTRYPOINT_SYMBOL_NAME) - .map_err(LoaderError::GetEntrypoint)?; - let raw = entrypoint(); - - Ok(Self { - bootstrapper: *Box::from_raw(raw), - so: lib, - }) - } - } - - pub async fn load( - self, - factory: &mut dyn Factory, - addr: SocketAddr, - logger: logger::Logger, - ) -> Result { - let mut bootstrapper = self.bootstrapper; - - AssertUnwindSafe(bootstrapper.bootstrap(factory, logger)) - .catch_unwind() - .await - .map_err(|e| Error::BuildPanic(map_any_to_panic_string(e)))??; - - trace!("bootstrapping done"); - - // Start service on this side of the FFI - let handle = tokio::spawn(async move { - bootstrapper.into_handle(addr)?.await.map_err(|e| { - if e.is_panic() { - let mes = e.into_panic(); - - Error::BindPanic(map_any_to_panic_string(mes)) - } else { - Error::Custom(CustomError::new(e)) - } - })? - }); - - trace!("creating handle done"); - - Ok((handle, self.so)) - } -} - -/// Given a project directory path, builds the crate -pub async fn build_crate( - deployment_id: Uuid, - project_path: &Path, - release_mode: bool, - tx: Sender, -) -> anyhow::Result { - let (read, write) = pipe::pipe(); - let project_path = project_path.to_owned(); - - // This needs to be on a separate thread, else deployer will block (reason currently unknown :D) - tokio::task::spawn_blocking(move || { - trace!("started thread to to capture build output stream"); - for message in Message::parse_stream(read) { - trace!(?message, "parsed cargo message"); - match message { - Ok(message) => { - if let Err(error) = tx.send(message) { - error!("failed to send cargo message on channel: {error}"); - } - } - Err(error) => { - error!("failed to parse cargo message: {error}"); - } - } - } - }); - - let config = get_config(write)?; - let manifest_path = project_path.join("Cargo.toml"); - let mut ws = Workspace::new(&manifest_path, &config)?; - - let current = ws.current_mut().map_err(|_| anyhow!("A Shuttle project cannot have a virtual manifest file - please ensure your Cargo.toml file specifies it as a library."))?; - let manifest = current.manifest_mut(); - ensure_cdylib(manifest)?; - - let summary = current.manifest_mut().summary_mut(); - make_name_unique(summary, deployment_id); - check_version(summary)?; - check_no_panic(&ws)?; - - let opts = get_compile_options(&config, release_mode)?; - let compilation = compile(&ws, &opts); - - Ok(compilation?.cdylibs[0].path.clone()) -} - -pub fn clean_crate(project_path: &Path, release_mode: bool) -> anyhow::Result> { - let (read, write) = pipe::pipe(); - let project_path = project_path.to_owned(); - - tokio::task::spawn_blocking(move || { - let config = get_config(write).unwrap(); - let manifest_path = project_path.join("Cargo.toml"); - let ws = Workspace::new(&manifest_path, &config).unwrap(); - - let requested_profile = if release_mode { - InternedString::new("release") - } else { - InternedString::new("dev") - }; - - let opts = CleanOptions { - config: &config, - spec: Vec::new(), - targets: Vec::new(), - requested_profile, - profile_specified: true, - doc: false, - }; - - clean(&ws, &opts).unwrap(); - }); - - let mut lines = Vec::new(); - - for message in Message::parse_stream(read) { - trace!(?message, "parsed cargo message"); - match message { - Ok(Message::TextLine(line)) => { - lines.push(line); - } - Ok(_) => {} - Err(error) => { - error!("failed to parse cargo message: {error}"); - } - } - } - - Ok(lines) -} - -/// Get the default compile config with output redirected to writer -pub fn get_config(writer: PipeWriter) -> anyhow::Result { - let mut shell = Shell::from_write(Box::new(writer)); - shell.set_verbosity(Verbosity::Normal); - let cwd = std::env::current_dir() - .with_context(|| "couldn't get the current directory of the process")?; - let homedir = homedir(&cwd).ok_or_else(|| { - anyhow!( - "Cargo couldn't find your home directory. \ - This probably means that $HOME was not set." - ) - })?; - - Ok(Config::new(shell, cwd, homedir)) -} - -/// Get options to compile in build mode -fn get_compile_options(config: &Config, release_mode: bool) -> anyhow::Result { - let mut opts = CompileOptions::new(config, CompileMode::Build)?; - opts.build_config.message_format = MessageFormat::Json { - render_diagnostics: false, - short: false, - ansi: false, - }; - - opts.build_config.requested_profile = if release_mode { - InternedString::new("release") - } else { - InternedString::new("dev") - }; - - // This sets the max workers for cargo build to 4 for release mode (aka deployment), - // but leaves it as default (num cpus) for local runs - if release_mode { - opts.build_config.jobs = 4 - }; - - Ok(opts) -} - -/// Make sure "cdylib" is set, else set it if possible -fn ensure_cdylib(manifest: &mut Manifest) -> anyhow::Result<()> { - if let Some(target) = manifest - .targets_mut() - .iter_mut() - .find(|target| target.is_lib()) - { - if !target.is_cdylib() { - *target = cargo::core::manifest::Target::lib_target( - target.name(), - vec![cargo::core::compiler::CrateType::Cdylib], - target.src_path().path().unwrap().to_path_buf(), - target.edition(), - ); - } - - Ok(()) - } else { - Err(anyhow!( - "Your Shuttle project must be a library. Please add `[lib]` to your Cargo.toml file." - )) - } -} - -/// Ensure name is unique. Without this `tracing`/`log` crashes because the global subscriber is somehow "already set" -// TODO: remove this when getting rid of the FFI -fn make_name_unique(summary: &mut Summary, deployment_id: Uuid) { - let old_package_id = summary.package_id(); - *summary = summary.clone().override_id( - PackageId::new( - format!("{}-{deployment_id}", old_package_id.name()), - old_package_id.version(), - old_package_id.source_id(), - ) - .unwrap(), - ); -} - -/// Check that the crate being build is compatible with this version of loader -fn check_version(summary: &Summary) -> anyhow::Result<()> { - let valid_version = VERSION.to_semver().unwrap(); - - let version_req = if let Some(shuttle) = summary - .dependencies() - .iter() - .find(|dependency| dependency.package_name() == NAME) - { - shuttle.version_req() - } else { - return Err(anyhow!("this crate does not use the shuttle service")); - }; - - if version_req.matches(&valid_version) { - Ok(()) - } else { - Err(anyhow!( - "the version of `shuttle-service` specified as a dependency to this service ({version_req}) is not supported by this project instance ({valid_version}); try updating `shuttle-service` to '{valid_version}' or update the project instance using `cargo shuttle project rm` and `cargo shuttle project new`" - )) - } -} - -/// Ensure `panic = "abort"` is not set: -fn check_no_panic(ws: &Workspace) -> anyhow::Result<()> { - if let Some(profiles) = ws.profiles() { - for profile in profiles.get_all().values() { - if profile.panic.as_deref() == Some("abort") { - return Err(anyhow!("Your Shuttle project cannot have panics that abort. Please ensure your Cargo.toml does not contain `panic = \"abort\"` for any profiles.")); - } - } - } - - Ok(()) -} - -fn map_any_to_panic_string(a: Box) -> String { - a.downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_string()) -} - -#[cfg(test)] -mod tests { - mod from_so_file { - use crate::loader::{Loader, LoaderError}; - - #[test] - fn invalid() { - let result = Loader::from_so_file("invalid.so"); - - assert!(matches!(result, Err(LoaderError::Load(_)))); - } - } -} diff --git a/service/src/logger.rs b/service/src/logger.rs deleted file mode 100644 index 8572aea99..000000000 --- a/service/src/logger.rs +++ /dev/null @@ -1,152 +0,0 @@ -use chrono::Utc; -use serde_json::json; -use shuttle_common::{deployment::State, DeploymentId, LogItem}; -use tokio::sync::mpsc::UnboundedSender; -use tracing::{field::Visit, Subscriber}; -use tracing_subscriber::Layer; - -pub struct Logger { - deployment_id: DeploymentId, - tx: UnboundedSender, -} - -impl Logger { - pub fn new(tx: UnboundedSender, deployment_id: DeploymentId) -> Self { - Self { tx, deployment_id } - } -} - -impl Layer for Logger -where - S: Subscriber, -{ - fn on_event( - &self, - event: &tracing::Event<'_>, - _ctx: tracing_subscriber::layer::Context<'_, S>, - ) { - let datetime = Utc::now(); - - let item = { - let metadata = event.metadata(); - let mut visitor = JsonVisitor::default(); - - event.record(&mut visitor); - - LogItem { - id: self.deployment_id, - state: State::Running, - level: metadata.level().into(), - timestamp: datetime, - file: visitor.file.or_else(|| metadata.file().map(str::to_string)), - line: visitor.line.or_else(|| metadata.line()), - target: visitor - .target - .unwrap_or_else(|| metadata.target().to_string()), - fields: serde_json::to_vec(&visitor.fields).unwrap(), - } - }; - - self.tx.send(item).expect("sending log should succeed"); - } -} - -// Boilerplate for extracting the fields from the event -#[derive(Default)] -struct JsonVisitor { - fields: serde_json::Map, - target: Option, - file: Option, - line: Option, -} - -impl JsonVisitor { - /// Ignores log metadata as it is included in the other LogItem fields (target, file, line...) - fn filter_insert(&mut self, field: &tracing::field::Field, value: serde_json::Value) { - match field.name() { - "log.line" => self.line = value.as_u64().map(|u| u as u32), - "log.target" => self.target = value.as_str().map(ToOwned::to_owned), - "log.file" => self.file = value.as_str().map(ToOwned::to_owned), - "log.module_path" => {} - name => { - self.fields.insert(name.to_string(), json!(value)); - } - } - } -} -impl Visit for JsonVisitor { - fn record_str(&mut self, field: &tracing::field::Field, value: &str) { - self.filter_insert(field, json!(value)); - } - fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { - self.filter_insert(field, json!(value)); - } - fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { - self.filter_insert(field, json!(value)); - } - fn record_i64(&mut self, field: &tracing::field::Field, value: i64) { - self.filter_insert(field, json!(value)); - } - fn record_f64(&mut self, field: &tracing::field::Field, value: f64) { - self.filter_insert(field, json!(value)); - } - fn record_error( - &mut self, - field: &tracing::field::Field, - value: &(dyn std::error::Error + 'static), - ) { - self.filter_insert(field, json!(value.to_string())); - } - fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { - self.filter_insert(field, json!(format!("{value:?}"))); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use shuttle_common::log::Level; - use tokio::sync::mpsc; - use tracing_subscriber::prelude::*; - - #[test] - fn logging() { - let (s, mut r) = mpsc::unbounded_channel(); - - let logger = Logger::new(s, Default::default()); - - tracing_subscriber::registry().with(logger).init(); - - tracing::debug!("this is"); - tracing::info!("hi"); - tracing::warn!("from"); - tracing::error!("logger"); - - assert_eq!( - r.blocking_recv().map(to_tuple), - Some(("this is".to_string(), Level::Debug)) - ); - assert_eq!( - r.blocking_recv().map(to_tuple), - Some(("hi".to_string(), Level::Info)) - ); - assert_eq!( - r.blocking_recv().map(to_tuple), - Some(("from".to_string(), Level::Warn)) - ); - assert_eq!( - r.blocking_recv().map(to_tuple), - Some(("logger".to_string(), Level::Error)) - ); - } - - fn to_tuple(log: LogItem) -> (String, Level) { - let fields: serde_json::Map = - serde_json::from_slice(&log.fields).unwrap(); - - let message = fields["message"].as_str().unwrap().to_owned(); - - (message, log.level) - } -} diff --git a/service/src/persist.rs b/service/src/persist.rs deleted file mode 100644 index 6b7510158..000000000 --- a/service/src/persist.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::{Factory, ResourceBuilder, ServiceName}; -use async_trait::async_trait; -use bincode::{deserialize_from, serialize_into, Error as BincodeError}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::fs; -use std::fs::File; -use std::io::BufReader; -use std::io::BufWriter; -use std::path::PathBuf; -use thiserror::Error; -use tokio::runtime::Runtime; - -#[derive(Error, Debug)] -pub enum PersistError { - #[error("failed to open file: {0}")] - Open(std::io::Error), - #[error("failed to create folder: {0}")] - CreateFolder(std::io::Error), - #[error("failed to serialize data: {0}")] - Serialize(BincodeError), - #[error("failed to deserialize data: {0}")] - Deserialize(BincodeError), -} - -pub struct Persist; - -pub struct PersistInstance { - service_name: ServiceName, -} - -impl PersistInstance { - pub fn save(&self, key: &str, struc: T) -> Result<(), PersistError> { - let storage_folder = self.get_storage_folder(); - fs::create_dir_all(storage_folder).map_err(PersistError::CreateFolder)?; - - let file_path = self.get_storage_file(key); - let file = File::create(file_path).map_err(PersistError::Open)?; - let mut writer = BufWriter::new(file); - Ok(serialize_into(&mut writer, &struc).map_err(PersistError::Serialize))? - } - - pub fn load(&self, key: &str) -> Result - where - T: DeserializeOwned, - { - let file_path = self.get_storage_file(key); - let file = File::open(file_path).map_err(PersistError::Open)?; - let reader = BufReader::new(file); - Ok(deserialize_from(reader).map_err(PersistError::Deserialize))? - } - - fn get_storage_folder(&self) -> PathBuf { - ["shuttle_persist", &self.service_name.to_string()] - .iter() - .collect() - } - - fn get_storage_file(&self, key: &str) -> PathBuf { - let mut path = self.get_storage_folder(); - path.push(format!("{key}.bin")); - - path - } -} - -#[async_trait] -impl ResourceBuilder for Persist { - fn new() -> Self { - Self {} - } - - async fn build( - self, - factory: &mut dyn Factory, - _runtime: &Runtime, - ) -> Result { - Ok(PersistInstance { - service_name: factory.get_service_name(), - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::str::FromStr; - - #[test] - fn test_save_and_load() { - let persist = PersistInstance { - service_name: ServiceName::from_str("test").unwrap(), - }; - - persist.save("test", "test").unwrap(); - let result: String = persist.load("test").unwrap(); - assert_eq!(result, "test"); - } - - #[test] - fn test_load_error() { - let persist = PersistInstance { - service_name: ServiceName::from_str("test").unwrap(), - }; - - // unwrapp error - let result = persist.load::("error").unwrap_err(); - assert_eq!( - result.to_string(), - "failed to open file: No such file or directory (os error 2)" - ); - } -} diff --git a/service/tests/integration/build_crate.rs b/service/tests/integration/build_crate.rs index 221682806..3e608fba4 100644 --- a/service/tests/integration/build_crate.rs +++ b/service/tests/integration/build_crate.rs @@ -1,62 +1,39 @@ use std::path::{Path, PathBuf}; -use shuttle_service::loader::build_crate; +use shuttle_service::builder::{build_crate, Runtime}; -#[tokio::test(flavor = "multi_thread")] +#[tokio::test] +#[should_panic(expected = "1 job failed")] async fn not_shuttle() { let (tx, _) = crossbeam_channel::unbounded(); let project_path = format!("{}/tests/resources/not-shuttle", env!("CARGO_MANIFEST_DIR")); - let so_path = build_crate(Default::default(), Path::new(&project_path), false, tx) + build_crate(Path::new(&project_path), false, tx) .await .unwrap(); - - assert!( - so_path - .display() - .to_string() - .ends_with("tests/resources/not-shuttle/target/debug/libnot_shuttle.so"), - "did not get expected so_path: {}", - so_path.display() - ); } #[tokio::test] -#[should_panic( - expected = "Your Shuttle project must be a library. Please add `[lib]` to your Cargo.toml file." -)] -async fn not_lib() { +#[should_panic(expected = "Your Shuttle project must be a binary.")] +async fn not_bin() { let (tx, _) = crossbeam_channel::unbounded(); - let project_path = format!("{}/tests/resources/not-lib", env!("CARGO_MANIFEST_DIR")); - build_crate(Default::default(), Path::new(&project_path), false, tx) - .await - .unwrap(); + let project_path = format!("{}/tests/resources/not-bin", env!("CARGO_MANIFEST_DIR")); + match build_crate(Path::new(&project_path), false, tx).await { + Ok(_) => {} + Err(e) => panic!("{}", e.to_string()), + } } -#[tokio::test(flavor = "multi_thread")] -async fn not_cdylib() { +#[tokio::test] +async fn is_bin() { let (tx, _) = crossbeam_channel::unbounded(); - let project_path = format!("{}/tests/resources/not-cdylib", env!("CARGO_MANIFEST_DIR")); - assert!( - build_crate(Default::default(), Path::new(&project_path), false, tx) - .await - .is_ok() - ); - assert!(PathBuf::from(project_path) - .join("target/debug/libnot_cdylib.so") - .exists()); -} + let project_path = format!("{}/tests/resources/is-bin", env!("CARGO_MANIFEST_DIR")); -#[tokio::test(flavor = "multi_thread")] -async fn is_cdylib() { - let (tx, _) = crossbeam_channel::unbounded(); - let project_path = format!("{}/tests/resources/is-cdylib", env!("CARGO_MANIFEST_DIR")); - assert!( - build_crate(Default::default(), Path::new(&project_path), false, tx) - .await - .is_ok() - ); + assert!(matches!( + build_crate(Path::new(&project_path), false, tx).await, + Ok(Runtime::Alpha(_)) + )); assert!(PathBuf::from(project_path) - .join("target/debug/libis_cdylib.so") + .join("target/debug/is-bin") .exists()); } @@ -68,7 +45,7 @@ async fn not_found() { "{}/tests/resources/non-existing", env!("CARGO_MANIFEST_DIR") ); - build_crate(Default::default(), Path::new(&project_path), false, tx) + build_crate(Path::new(&project_path), false, tx) .await .unwrap(); } diff --git a/service/tests/integration/helpers/loader.rs b/service/tests/integration/helpers/loader.rs deleted file mode 100644 index 2d09238c2..000000000 --- a/service/tests/integration/helpers/loader.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::path::PathBuf; -use std::process::Command; - -use shuttle_service::loader::{Loader, LoaderError}; - -pub fn build_so_create_loader(resources: &str, crate_name: &str) -> Result { - let crate_dir: PathBuf = [resources, crate_name].iter().collect(); - - Command::new("cargo") - .args(["build", "--release"]) - .current_dir(&crate_dir) - .spawn() - .unwrap() - .wait() - .unwrap(); - - let dashes_replaced = crate_name.replace('-', "_"); - - let lib_name = if cfg!(target_os = "windows") { - format!("{}.dll", dashes_replaced) - } else { - format!("lib{}.so", dashes_replaced) - }; - - let so_path = crate_dir.join("target/release").join(lib_name); - - Loader::from_so_file(so_path) -} diff --git a/service/tests/integration/helpers/mod.rs b/service/tests/integration/helpers/mod.rs deleted file mode 100644 index e9b2e29f0..000000000 --- a/service/tests/integration/helpers/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod sqlx; - -#[cfg(feature = "loader")] -pub mod loader; diff --git a/service/tests/integration/helpers/sqlx.rs b/service/tests/integration/helpers/sqlx.rs deleted file mode 100644 index 79632e967..000000000 --- a/service/tests/integration/helpers/sqlx.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::future::Future; -use std::process::Command; -use std::thread::sleep; -use std::time::Duration; - -use portpicker::pick_unused_port; -use sqlx::Connection; - -pub struct PostgresInstance { - port: u16, - container: String, - password: String, -} - -impl Default for PostgresInstance { - fn default() -> Self { - Self::new() - } -} - -impl PostgresInstance { - /// Creates a new [`PostgresInstance`] using the official postgres:11 docker image - /// - /// Does not wait for the container to be ready. Use [`PostgresInstance::wait_for_ready`] and - /// [`PostgresInstance::wait_for_connectable`] for that. - pub fn new() -> Self { - let port = pick_unused_port().expect("could not find a free port for postgres"); - let container = "postgres-shuttle-service-integration-test".to_string(); - let password = "password".to_string(); - - Command::new("docker") - .args([ - "run", - "--name", - &container, - "-e", - &format!("POSTGRES_PASSWORD={}", password), - "-p", - &format!("{}:5432", port), - "postgres:11", // Our Containerfile image is based on buster which has postgres version 11 - ]) - .spawn() - .expect("failed to start a postgres instance"); - - Self { - port, - container, - password, - } - } - - pub fn get_uri(&self) -> String { - format!( - "postgres://postgres:{}@localhost:{}", - self.password, self.port - ) - } - - pub fn wait_for_connectable(&self) -> impl Future + '_ { - self.async_wait_for(|instance| { - let uri = instance.get_uri().as_str().to_string(); - async move { sqlx::PgConnection::connect(uri.as_str()).await.is_ok() } - }) - } - - pub async fn async_wait_for(&self, f: F) - where - F: Fn(&Self) -> Fut, - Fut: Future, - { - let mut timeout = 20 * 10; - - while timeout > 0 { - timeout -= 1; - - if f(self).await { - return; - } - - sleep(Duration::from_millis(100)); - } - - panic!("timed out waiting for PostgresInstance"); - } - - pub fn wait_for_ready(&self) { - self.wait_for(|instance| { - let status = Command::new("docker") - .args(["exec", &instance.container, "pg_isready"]) - .output() - .expect("failed to get postgres ready status") - .status; - - status.success() - }) - } - - pub fn wait_for(&self, f: F) - where - F: Fn(&Self) -> bool, - { - let mut timeout = 20 * 10; - - while timeout > 0 { - timeout -= 1; - - if f(self) { - return; - } - - sleep(Duration::from_millis(100)); - } - - panic!("timed out waiting for PostgresInstance"); - } -} - -impl Drop for PostgresInstance { - fn drop(&mut self) { - Command::new("docker") - .args(["stop", &self.container]) - .spawn() - .expect("failed to spawn stop for postgres container") - .wait() - .expect("postgres container stop failed"); - - Command::new("docker") - .args(["rm", &self.container]) - .spawn() - .expect("failed to spawn stop for remove container") - .wait() - .expect("postgres container remove failed"); - } -} diff --git a/service/tests/integration/loader.rs b/service/tests/integration/loader.rs deleted file mode 100644 index 467354260..000000000 --- a/service/tests/integration/loader.rs +++ /dev/null @@ -1,196 +0,0 @@ -use crate::helpers::{loader::build_so_create_loader, sqlx::PostgresInstance}; - -use shuttle_common::log::Level; -use shuttle_common::LogItem; -use shuttle_service::loader::LoaderError; -use shuttle_service::{database, Error, Factory, Logger, ServiceName}; -use std::collections::BTreeMap; -use std::net::{Ipv4Addr, SocketAddr}; -use std::process::exit; -use std::str::FromStr; -use std::time::Duration; -use tokio::sync::mpsc::{self, UnboundedReceiver}; - -use async_trait::async_trait; - -const RESOURCES_PATH: &str = "tests/resources"; - -struct DummyFactory { - postgres_instance: Option, - service_name: ServiceName, -} - -impl DummyFactory { - fn new() -> Self { - Self { - postgres_instance: None, - service_name: ServiceName::from_str("test").unwrap(), - } - } -} - -fn get_logger() -> (Logger, UnboundedReceiver) { - let (tx, rx) = mpsc::unbounded_channel(); - let logger = Logger::new(tx, Default::default()); - - (logger, rx) -} - -#[async_trait] -impl Factory for DummyFactory { - fn get_service_name(&self) -> ServiceName { - self.service_name.clone() - } - - async fn get_db_connection_string(&mut self, _: database::Type) -> Result { - let uri = if let Some(postgres_instance) = &self.postgres_instance { - postgres_instance.get_uri() - } else { - let postgres_instance = PostgresInstance::new(); - postgres_instance.wait_for_ready(); - postgres_instance.wait_for_connectable().await; - let uri = postgres_instance.get_uri(); - self.postgres_instance = Some(postgres_instance); - uri - }; - - Ok(uri) - } - - fn get_environment(&self) -> shuttle_service::Environment { - shuttle_service::Environment::Local - } - - async fn get_secrets(&mut self) -> Result, Error> { - panic!("did not expect any loader test to get secrets") - } - - fn get_build_path(&self) -> Result { - panic!("did not expect any loader test to get the build path") - } - - fn get_storage_path(&self) -> Result { - panic!("did not expect any loader test to get the storage path") - } -} - -#[test] -fn not_shuttle() { - let result = build_so_create_loader(RESOURCES_PATH, "not-shuttle"); - assert!(matches!(result, Err(LoaderError::GetEntrypoint(_)))); -} - -#[tokio::test] -async fn sleep_async() { - let loader = build_so_create_loader(RESOURCES_PATH, "sleep-async").unwrap(); - - let mut factory = DummyFactory::new(); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, _rx) = get_logger(); - let (handler, _) = loader.load(&mut factory, addr, logger).await.unwrap(); - - // Give service some time to start up - tokio::time::sleep(Duration::from_secs(1)).await; - - tokio::spawn(async { - // Time is less than sleep in service - tokio::time::sleep(Duration::from_secs(5)).await; - println!("Test failed as async service was not aborted"); - exit(1); - }); - - handler.abort(); - assert!(handler.await.unwrap_err().is_cancelled()); -} - -#[tokio::test] -async fn sleep() { - let loader = build_so_create_loader(RESOURCES_PATH, "sleep").unwrap(); - - let mut factory = DummyFactory::new(); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, _rx) = get_logger(); - let (handler, _) = loader.load(&mut factory, addr, logger).await.unwrap(); - - // Give service some time to start up - tokio::time::sleep(Duration::from_secs(1)).await; - - tokio::spawn(async { - // Time is less than sleep in service - tokio::time::sleep(Duration::from_secs(5)).await; - println!("Test failed as blocking service was not aborted"); - exit(1); - }); - - handler.abort(); - assert!(handler.await.unwrap_err().is_cancelled()); -} - -#[tokio::test] -async fn sqlx_pool() { - let loader = build_so_create_loader(RESOURCES_PATH, "sqlx-pool").unwrap(); - - // Make sure we'll get a log entry - std::env::set_var("RUST_LOG", "info"); - - // Don't initialize a pre-existing PostgresInstance here because the `PostgresInstance::wait_for_connectable()` - // code has `awaits` and we want to make sure they do not block inside `Service::build()`. - // At the same time we also want to test the PgPool is created on the correct runtime (ie does not cause a - // "has to run on a tokio runtime" error) - let mut factory = DummyFactory::new(); - - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, mut rx) = get_logger(); - let (handler, _) = loader.load(&mut factory, addr, logger).await.unwrap(); - - handler.await.unwrap().unwrap(); - - let log = rx.recv().await.unwrap(); - let value = serde_json::from_slice::(&log.fields).unwrap(); - let message = value - .as_object() - .unwrap() - .get("message") - .unwrap() - .as_str() - .unwrap(); - assert!( - message.starts_with("SELECT 'Hello world';"), - "got: {}", - message - ); - assert_eq!(log.target, "sqlx::query"); - assert_eq!(log.level, Level::Info); -} - -#[tokio::test] -async fn build_panic() { - let loader = build_so_create_loader(RESOURCES_PATH, "build-panic").unwrap(); - - let mut factory = DummyFactory::new(); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, _rx) = get_logger(); - - if let Err(Error::BuildPanic(msg)) = loader.load(&mut factory, addr, logger).await { - assert_eq!(&msg, "panic in build"); - } else { - panic!("expected `Err(Error::BuildPanic(_))`"); - } -} - -#[tokio::test] -async fn bind_panic() { - let loader = build_so_create_loader(RESOURCES_PATH, "bind-panic").unwrap(); - - let mut factory = DummyFactory::new(); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, _rx) = get_logger(); - - let (handle, _) = loader.load(&mut factory, addr, logger).await.unwrap(); - - if let Err(Error::BindPanic(msg)) = handle.await.unwrap() { - assert_eq!(&msg, "panic in bind"); - } else { - panic!("expected `Err(Error::BindPanic(_))`"); - } -} diff --git a/service/tests/integration/main.rs b/service/tests/integration/main.rs index b09409f82..f8f5ab1f1 100644 --- a/service/tests/integration/main.rs +++ b/service/tests/integration/main.rs @@ -1,7 +1,2 @@ -pub mod helpers; - -#[cfg(feature = "loader")] -mod loader; - -#[cfg(feature = "loader")] +#[cfg(feature = "builder")] mod build_crate; diff --git a/service/tests/resources/bind-panic/src/lib.rs b/service/tests/resources/bind-panic/src/lib.rs deleted file mode 100644 index 1ecd700bf..000000000 --- a/service/tests/resources/bind-panic/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use shuttle_service::Service; - -struct MyService; - -#[shuttle_service::async_trait] -impl Service for MyService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::Error> { - panic!("panic in bind"); - } -} - -#[shuttle_service::main] -async fn bind_panic() -> Result { - Ok(MyService) -} diff --git a/service/tests/resources/build-panic/Cargo.toml b/service/tests/resources/build-panic/Cargo.toml deleted file mode 100644 index 93a0b14e8..000000000 --- a/service/tests/resources/build-panic/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "build-panic" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[workspace] - -[dependencies] -shuttle-service = { path = "../../../" } diff --git a/service/tests/resources/build-panic/src/lib.rs b/service/tests/resources/build-panic/src/lib.rs deleted file mode 100644 index 084e5c802..000000000 --- a/service/tests/resources/build-panic/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use shuttle_service::Service; - -struct MyService; - -#[shuttle_service::async_trait] -impl Service for MyService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::Error> { - Ok(()) - } -} - -#[shuttle_service::main] -async fn build_panic() -> Result { - panic!("panic in build"); -} diff --git a/service/tests/resources/is-bin/Cargo.toml b/service/tests/resources/is-bin/Cargo.toml new file mode 100644 index 000000000..71680d565 --- /dev/null +++ b/service/tests/resources/is-bin/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "is-bin" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +axum = "0.6.0" +shuttle-axum = { path = "../../../../services/shuttle-axum" } +shuttle-runtime = { path = "../../../../runtime" } +tokio = { version = "1.22.0" } diff --git a/service/tests/resources/is-bin/src/main.rs b/service/tests/resources/is-bin/src/main.rs new file mode 100644 index 000000000..19e9bd1a6 --- /dev/null +++ b/service/tests/resources/is-bin/src/main.rs @@ -0,0 +1,6 @@ +#[shuttle_runtime::main] +async fn axum() -> shuttle_axum::ShuttleAxum { + let router = axum::Router::new(); + + Ok(router.into()) +} diff --git a/service/tests/resources/is-cdylib/src/lib.rs b/service/tests/resources/is-cdylib/src/lib.rs deleted file mode 100644 index 80a1cf666..000000000 --- a/service/tests/resources/is-cdylib/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[shuttle_service::main] -async fn rocket() -> shuttle_service::ShuttleRocket { - let rocket = rocket::build(); - Ok(rocket) -} diff --git a/service/tests/resources/is-cdylib/Cargo.toml b/service/tests/resources/not-bin/Cargo.toml similarity index 51% rename from service/tests/resources/is-cdylib/Cargo.toml rename to service/tests/resources/not-bin/Cargo.toml index 99083de80..6bef68b6f 100644 --- a/service/tests/resources/is-cdylib/Cargo.toml +++ b/service/tests/resources/not-bin/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "is-cdylib" +name = "not-bin" version = "0.1.0" edition = "2021" @@ -10,4 +10,5 @@ crate-type = ["cdylib", "staticlib"] [dependencies] rocket = "0.5.0-rc.2" -shuttle-service = { path = "../../../", features = ["web-rocket"] } +shuttle-rocket = { path = "../../../../services/shuttle-rocket" } +shuttle-runtime = { path = "../../../../runtime" } diff --git a/service/tests/resources/not-bin/src/lib.rs b/service/tests/resources/not-bin/src/lib.rs new file mode 100644 index 000000000..1c00c9ad3 --- /dev/null +++ b/service/tests/resources/not-bin/src/lib.rs @@ -0,0 +1,7 @@ +// This will fail to compile since it's a library. + +#[shuttle_runtime::main] +async fn rocket() -> shuttle_rocket::ShuttleRocket { + let rocket = rocket::build(); + Ok(rocket.into()) +} diff --git a/service/tests/resources/not-cdylib/Cargo.toml b/service/tests/resources/not-cdylib/Cargo.toml deleted file mode 100644 index 47a2afa53..000000000 --- a/service/tests/resources/not-cdylib/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "not-cdylib" -version = "0.1.0" -edition = "2021" - -[workspace] - -[lib] -crate-type = ["staticlib"] - -[dependencies] -rocket = "0.5.0-rc.2" -shuttle-service = { path = "../../../", features = ["web-rocket"] } diff --git a/service/tests/resources/not-cdylib/src/lib.rs b/service/tests/resources/not-cdylib/src/lib.rs deleted file mode 100644 index 80a1cf666..000000000 --- a/service/tests/resources/not-cdylib/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[shuttle_service::main] -async fn rocket() -> shuttle_service::ShuttleRocket { - let rocket = rocket::build(); - Ok(rocket) -} diff --git a/service/tests/resources/not-lib/Cargo.toml b/service/tests/resources/not-lib/Cargo.toml deleted file mode 100644 index 3c2b2f01c..000000000 --- a/service/tests/resources/not-lib/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "not-lib" -version = "0.1.0" -edition = "2021" - -[workspace] diff --git a/service/tests/resources/not-lib/src/main.rs b/service/tests/resources/not-lib/src/main.rs deleted file mode 100644 index 2fc1633f9..000000000 --- a/service/tests/resources/not-lib/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -// This will fail to compile since it is missing a `[lib]` section in its Cargo.toml file. - -fn main() { - println!("this is not valid as it is not a library!"); -} diff --git a/service/tests/resources/not-shuttle/Cargo.toml b/service/tests/resources/not-shuttle/Cargo.toml index 77b9a4c66..ecd61ac3c 100644 --- a/service/tests/resources/not-shuttle/Cargo.toml +++ b/service/tests/resources/not-shuttle/Cargo.toml @@ -3,10 +3,9 @@ name = "not-shuttle" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] - [workspace] [dependencies] -shuttle-service = "0.11.0" +axum = "0.6.0" +shuttle-runtime = { path = "../../../../runtime" } +shuttle-axum = { path = "../../../../services/shuttle-axum" } diff --git a/service/tests/resources/not-shuttle/src/lib.rs b/service/tests/resources/not-shuttle/src/lib.rs deleted file mode 100644 index b25ff369e..000000000 --- a/service/tests/resources/not-shuttle/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -// This service cannot be hosted on shuttle since it is missing the entrypoint the shutlle macros would have added!!! diff --git a/service/tests/resources/not-shuttle/src/main.rs b/service/tests/resources/not-shuttle/src/main.rs new file mode 100644 index 000000000..2edbe4d18 --- /dev/null +++ b/service/tests/resources/not-shuttle/src/main.rs @@ -0,0 +1,6 @@ +// This service cannot be hosted on shuttle since it is missing the runtime the shuttle main macro would have added!!! +async fn axum() -> shuttle_axum::ShuttleAxum { + let router = axum::Router::new(); + + Ok(router.into()) +} diff --git a/service/tests/resources/sleep-async/Cargo.toml b/service/tests/resources/sleep-async/Cargo.toml deleted file mode 100644 index e9fa47aae..000000000 --- a/service/tests/resources/sleep-async/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "sleep-async" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[workspace] - -[dependencies] -tokio = { version = "1.22.0", features = ["time"] } -shuttle-service = { path = "../../../" } diff --git a/service/tests/resources/sleep-async/src/lib.rs b/service/tests/resources/sleep-async/src/lib.rs deleted file mode 100644 index 585537560..000000000 --- a/service/tests/resources/sleep-async/src/lib.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::time::Duration; - -use shuttle_service::Service; -use tokio::time::sleep; - -struct SleepService { - duration: u64, -} - -#[shuttle_service::main] -async fn simple() -> Result { - Ok(SleepService { duration: 10 }) -} - -#[shuttle_service::async_trait] -impl Service for SleepService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::error::Error> { - let duration = Duration::from_secs(self.duration); - - sleep(duration).await; - Ok(()) - } -} diff --git a/service/tests/resources/sleep/Cargo.toml b/service/tests/resources/sleep/Cargo.toml deleted file mode 100644 index 40b0f6d1f..000000000 --- a/service/tests/resources/sleep/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "sleep" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[workspace] - -[dependencies] -shuttle-service = { path = "../../../" } diff --git a/service/tests/resources/sleep/src/lib.rs b/service/tests/resources/sleep/src/lib.rs deleted file mode 100644 index ac67c50ae..000000000 --- a/service/tests/resources/sleep/src/lib.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::{thread::sleep, time::Duration}; - -use shuttle_service::Service; - -struct SleepService { - duration: u64, -} - -#[shuttle_service::main] -async fn simple() -> Result { - Ok(SleepService { duration: 10 }) -} - -#[shuttle_service::async_trait] -impl Service for SleepService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::error::Error> { - let duration = Duration::from_secs(self.duration); - - sleep(duration); - Ok(()) - } -} diff --git a/service/tests/resources/sqlx-pool/Cargo.toml b/service/tests/resources/sqlx-pool/Cargo.toml deleted file mode 100644 index 14487f75a..000000000 --- a/service/tests/resources/sqlx-pool/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "sqlx-pool" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[workspace] - -[dependencies] -shuttle-service = { path = "../../../" } -shuttle-shared-db = { path = "../../../../resources/shared-db", features = ["postgres"] } -sqlx = { version = "0.6.2", features = [ "runtime-tokio-native-tls" ] } diff --git a/service/tests/resources/sqlx-pool/src/lib.rs b/service/tests/resources/sqlx-pool/src/lib.rs deleted file mode 100644 index a3ae35690..000000000 --- a/service/tests/resources/sqlx-pool/src/lib.rs +++ /dev/null @@ -1,39 +0,0 @@ -use shuttle_service::error::CustomError; -use shuttle_service::Service; -use sqlx::PgPool; - -struct PoolService { - pool: PgPool, -} - -#[shuttle_service::main] -async fn init( - #[shuttle_shared_db::Postgres] pool: PgPool, -) -> Result { - Ok(PoolService { pool }) -} - -impl PoolService { - async fn start(&self) -> Result<(), shuttle_service::error::CustomError> { - let (rec,): (String,) = sqlx::query_as("SELECT 'Hello world'") - .fetch_one(&self.pool) - .await - .map_err(CustomError::new)?; - - assert_eq!(rec, "Hello world"); - - Ok(()) - } -} - -#[shuttle_service::async_trait] -impl Service for PoolService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::error::Error> { - self.start().await?; - - Ok(()) - } -} diff --git a/services/README.md b/services/README.md new file mode 100644 index 000000000..39272e508 --- /dev/null +++ b/services/README.md @@ -0,0 +1,5 @@ +## Service Integrations +The list of supported frameworks for shuttle is always growing. If you feel we are missing a framework you would like, then feel to create a feature request for your desired framework. + +## Writing your own service integration +Creating your own service integration is quite simple. You only need to implement the [`Service`](https://docs.rs/shuttle-service/latest/shuttle_service/trait.Service.html) trait for your framework. diff --git a/services/shuttle-actix-web/Cargo.toml b/services/shuttle-actix-web/Cargo.toml new file mode 100644 index 000000000..2cfae9bf7 --- /dev/null +++ b/services/shuttle-actix-web/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "shuttle-actix-web" +version = "0.12.0" +edition = "2021" +license = "Apache-2.0" +description = "Service implementation to run an actix webserver on shuttle" +keywords = ["shuttle-service", "actix"] + +[workspace] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = { version = "4.3.1" } +shuttle-runtime = { path = "../../runtime", version = "0.12.0" } +num_cpus = "1.15.0" + +[dev-dependencies] +tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } diff --git a/services/shuttle-actix-web/src/lib.rs b/services/shuttle-actix-web/src/lib.rs new file mode 100644 index 000000000..793edfc93 --- /dev/null +++ b/services/shuttle-actix-web/src/lib.rs @@ -0,0 +1,60 @@ +//! Shuttle service integration for the Actix Web framework. +//! ## Example +//! ```rust,no_run +//! use actix_web::{get, web::ServiceConfig}; +//! use shuttle_actix_web::ShuttleActixWeb; +//! +//! #[get("/hello")] +//! async fn hello_world() -> &'static str { +//! "Hello World!" +//! } +//! +//! #[shuttle_runtime::main] +//! async fn actix_web( +//! ) -> ShuttleActixWeb { +//! let config = move |cfg: &mut ServiceConfig| { +//! cfg.service(hello_world); +//! }; +//! +//! Ok(config.into()) +//! } +//! ``` +use std::net::SocketAddr; + +/// A wrapper type for a closure that returns an [actix_web::web::ServiceConfig] so we can implement +/// [shuttle_runtime::Service] for it. +#[derive(Clone)] +pub struct ActixWebService(pub F); + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for ActixWebService +where + F: FnOnce(&mut actix_web::web::ServiceConfig) + Send + Clone + 'static, +{ + async fn bind(mut self, addr: SocketAddr) -> Result<(), shuttle_runtime::Error> { + // Start a worker for each cpu, but no more than 4. + let worker_count = num_cpus::get().min(4); + + let server = + actix_web::HttpServer::new(move || actix_web::App::new().configure(self.0.clone())) + .workers(worker_count) + .bind(addr)? + .run(); + + server.await.map_err(shuttle_runtime::CustomError::new)?; + + Ok(()) + } +} + +impl From for ActixWebService +where + F: FnOnce(&mut actix_web::web::ServiceConfig) + Send + Clone + 'static, +{ + fn from(service_config: F) -> Self { + Self(service_config) + } +} + +/// The return type that should be returned from the [shuttle_runtime::main] function. +pub type ShuttleActixWeb = Result, shuttle_runtime::Error>; diff --git a/services/shuttle-axum/Cargo.toml b/services/shuttle-axum/Cargo.toml new file mode 100644 index 000000000..652d7fdc4 --- /dev/null +++ b/services/shuttle-axum/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "shuttle-axum" +version = "0.12.0" +edition = "2021" +license = "Apache-2.0" +description = "Service implementation to run an axum webserver on shuttle" +keywords = ["shuttle-service", "axum"] + +[workspace] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +axum = { version = "0.6.10" } +shuttle-runtime = { path = "../../runtime", version = "0.12.0" } + +[dev-dependencies] +tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } diff --git a/services/shuttle-axum/src/lib.rs b/services/shuttle-axum/src/lib.rs new file mode 100644 index 000000000..6ca9589ff --- /dev/null +++ b/services/shuttle-axum/src/lib.rs @@ -0,0 +1,43 @@ +//! Shuttle service integration for the Axum web framework. +//! ## Example +//! ```rust,no_run +//! use axum::{routing::get, Router}; +//! +//! async fn hello_world() -> &'static str { +//! "Hello, world!" +//! } +//! +//! #[shuttle_runtime::main] +//! async fn axum() -> shuttle_axum::ShuttleAxum { +//! let router = Router::new().route("/hello", get(hello_world)); +//! +//! Ok(router.into()) +//! } +//! ``` +use shuttle_runtime::{CustomError, Error}; +use std::net::SocketAddr; + +/// A wrapper type for [axum::Router] so we can implement [shuttle_runtime::Service] for it. +pub struct AxumService(pub axum::Router); + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for AxumService { + /// Takes the router that is returned by the user in their [shuttle_runtime::main] function + /// and binds to an address passed in by shuttle. + async fn bind(mut self, addr: SocketAddr) -> Result<(), Error> { + axum::Server::bind(&addr) + .serve(self.0.into_make_service()) + .await + .map_err(CustomError::new)?; + + Ok(()) + } +} + +impl From for AxumService { + fn from(router: axum::Router) -> Self { + Self(router) + } +} +/// The return type that should be returned from the [shuttle_runtime::main] function. +pub type ShuttleAxum = Result; diff --git a/services/shuttle-next/Cargo.toml b/services/shuttle-next/Cargo.toml new file mode 100644 index 000000000..3f42e57a1 --- /dev/null +++ b/services/shuttle-next/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "shuttle-next" +version = "0.12.0" +edition = "2021" +license = "Apache-2.0" +description = "Macros and aliases to deploy wasm on the shuttle platform (https://www.shuttle.rs/)" +homepage = "https://www.shuttle.rs" + +[workspace] + +[lib] + +[dependencies] +# most axum features can be enabled, but "tokio" and "ws" depend on socket2 +# via "hyper/tcp" which is not compatible with wasi +axum = { version = "0.6.0", default-features = false } +futures-executor = "0.3.21" +http = "0.2.7" +rmp-serde = "1.1.1" +tower-service = "0.3.1" +shuttle-common = { path = "../../common", version = "0.12.0", features = ["wasm"] } +shuttle-codegen = { path = "../../codegen", version = "0.12.0", features = ["next"] } +tracing-subscriber = { version = "0.3.16", default-features = false, features = ["registry", "std"] } diff --git a/services/shuttle-next/src/lib.rs b/services/shuttle-next/src/lib.rs new file mode 100644 index 000000000..a53c79c79 --- /dev/null +++ b/services/shuttle-next/src/lib.rs @@ -0,0 +1,8 @@ +pub use axum::*; +pub use futures_executor::block_on; +pub use http::Request; +pub use rmp_serde::from_read; +pub use shuttle_codegen::app; +pub use shuttle_common::wasm::{Logger, RequestWrapper, ResponseWrapper}; +pub use tower_service::Service; +pub use tracing_subscriber::{prelude as tracing_prelude, registry as tracing_registry}; diff --git a/services/shuttle-poem/Cargo.toml b/services/shuttle-poem/Cargo.toml new file mode 100644 index 000000000..c534cb076 --- /dev/null +++ b/services/shuttle-poem/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "shuttle-poem" +version = "0.12.0" +edition = "2021" +license = "Apache-2.0" +description = "Service implementation to run a poem webserver on shuttle" +keywords = ["shuttle-service", "poem"] + +[workspace] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +poem = { version = "1.3.55" } +shuttle-runtime = { path = "../../runtime", version = "0.12.0" } + +[dev-dependencies] +tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } diff --git a/services/shuttle-poem/src/lib.rs b/services/shuttle-poem/src/lib.rs new file mode 100644 index 000000000..4adef8782 --- /dev/null +++ b/services/shuttle-poem/src/lib.rs @@ -0,0 +1,49 @@ +//! Shuttle service integration for the Poem web framework. +//! ## Example +//! ```rust,no_run +//! use poem::{get, handler, Route}; +//! use shuttle_poem::ShuttlePoem; +//! +//! #[handler] +//! fn hello_world() -> &'static str { +//! "Hello, world!" +//! } +//! +//! #[shuttle_runtime::main] +//! async fn poem() -> ShuttlePoem { +//! let app = Route::new().at("/hello", get(hello_world)); +//! +//! Ok(app.into()) +//! } +//! +//! ``` + +/// A wrapper type for [poem::Endpoint] so we can implement [shuttle_runtime::Service] for it. +pub struct PoemService(pub T); + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for PoemService +where + T: poem::Endpoint + Send + 'static, +{ + async fn bind(mut self, addr: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { + poem::Server::new(poem::listener::TcpListener::bind(addr)) + .run(self.0) + .await + .map_err(shuttle_runtime::CustomError::new)?; + + Ok(()) + } +} + +impl From for PoemService +where + T: poem::Endpoint + Send + 'static, +{ + fn from(router: T) -> Self { + Self(router) + } +} + +/// The return type that should be returned from the [shuttle_runtime::main] function. +pub type ShuttlePoem = Result, shuttle_runtime::Error>; diff --git a/services/shuttle-poise/Cargo.toml b/services/shuttle-poise/Cargo.toml new file mode 100644 index 000000000..3aaf9a900 --- /dev/null +++ b/services/shuttle-poise/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "shuttle-poise" +version = "0.12.0" +edition = "2021" +license = "Apache-2.0" +description = "Service implementation to run a poise discord bot on shuttle" +keywords = ["shuttle-service", "poise", "discord-bot", "serenity"] + +[workspace] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +poise = { version = "0.5.2" } +shuttle-runtime = { path = "../../runtime", version = "0.12.0" } + +[dev-dependencies] +shuttle-secrets = { path = "../../resources/secrets" } +tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } diff --git a/services/shuttle-poise/src/lib.rs b/services/shuttle-poise/src/lib.rs new file mode 100644 index 000000000..fb88c86d5 --- /dev/null +++ b/services/shuttle-poise/src/lib.rs @@ -0,0 +1,76 @@ +//! Shuttle service integration for the Poise discord bot framework. +//! ## Example +//! ```rust,no_run +//! use shuttle_runtime::Context as _; +//! use poise::serenity_prelude as serenity; +//! use shuttle_secrets::SecretStore; +//! use shuttle_poise::ShuttlePoise; +//! +//! struct Data {} // User data, which is stored and accessible in all command invocations +//! type Error = Box; +//! type Context<'a> = poise::Context<'a, Data, Error>; +//! +//! /// Responds with "world!" +//! #[poise::command(slash_command)] +//! async fn hello(ctx: Context<'_>) -> Result<(), Error> { +//! ctx.say("world!").await?; +//! Ok(()) +//! } +//! +//! #[shuttle_runtime::main] +//! async fn poise(#[shuttle_secrets::Secrets] secret_store: SecretStore) -> ShuttlePoise { +//! // Get the discord token set in `Secrets.toml` +//! let discord_token = secret_store +//! .get("DISCORD_TOKEN") +//! .context("'DISCORD_TOKEN' was not found")?; +//! +//! let framework = poise::Framework::builder() +//! .options(poise::FrameworkOptions { +//! commands: vec![hello()], +//! ..Default::default() +//! }) +//! .token(discord_token) +//! .intents(serenity::GatewayIntents::non_privileged()) +//! .setup(|ctx, _ready, framework| { +//! Box::pin(async move { +//! poise::builtins::register_globally(ctx, &framework.options().commands).await?; +//! Ok(Data {}) +//! }) +//! }) +//! .build() +//! .await +//! .map_err(shuttle_runtime::CustomError::new)?; +//! +//! Ok(framework.into()) +//! } +//! ``` +use std::net::SocketAddr; +use std::sync::Arc; + +/// A wrapper type for [poise::Framework] so we can implement [shuttle_runtime::Service] for it. +pub struct PoiseService(pub Arc>); + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for PoiseService +where + T: Send + Sync + 'static, + E: Send + Sync + 'static, +{ + async fn bind(mut self, _addr: SocketAddr) -> Result<(), shuttle_runtime::Error> { + self.0 + .start() + .await + .map_err(shuttle_runtime::CustomError::new)?; + + Ok(()) + } +} + +impl From>> for PoiseService { + fn from(framework: Arc>) -> Self { + Self(framework) + } +} + +/// The return type that should be returned from the [shuttle_runtime::main] function. +pub type ShuttlePoise = Result, shuttle_runtime::Error>; diff --git a/services/shuttle-rocket/Cargo.toml b/services/shuttle-rocket/Cargo.toml new file mode 100644 index 000000000..1b4149476 --- /dev/null +++ b/services/shuttle-rocket/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "shuttle-rocket" +version = "0.12.0" +edition = "2021" +license = "Apache-2.0" +description = "Service implementation to run a rocket webserver on shuttle" +keywords = ["shuttle-service", "rocket"] + +[workspace] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rocket = { version = "0.5.0-rc.2" } +shuttle-runtime = { path = "../../runtime", version = "0.12.0" } + +[dev-dependencies] +tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } diff --git a/services/shuttle-rocket/src/lib.rs b/services/shuttle-rocket/src/lib.rs new file mode 100644 index 000000000..e891cec0f --- /dev/null +++ b/services/shuttle-rocket/src/lib.rs @@ -0,0 +1,63 @@ +//! Shuttle service integration for the Rocket web framework. +//! ## Example +//! ```rust,no_run +//! #[macro_use] +//! extern crate rocket; +//! +//! # fn main() { +//! #[get("/")] +//! fn index() -> &'static str { +//! "Hello, world!" +//! } +//! +//! #[shuttle_runtime::main] +//! async fn rocket() -> shuttle_rocket::ShuttleRocket { +//! let rocket = rocket::build().mount("/hello", routes![index]); +//! +//! Ok(rocket.into()) +//! } +//! # } +//! ``` +use std::net::SocketAddr; + +/// A wrapper type for [rocket::Rocket] so we can implement [shuttle_runtime::Service] for it. +pub struct RocketService(pub rocket::Rocket); + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for RocketService { + /// Takes the router that is returned by the user in their [shuttle_runtime::main] function + /// and binds to an address passed in by shuttle. + async fn bind(mut self, addr: SocketAddr) -> Result<(), shuttle_runtime::Error> { + let shutdown = rocket::config::Shutdown { + ctrlc: false, + ..rocket::config::Shutdown::default() + }; + + let config = self + .0 + .figment() + .clone() + .merge((rocket::Config::ADDRESS, addr.ip())) + .merge((rocket::Config::PORT, addr.port())) + .merge((rocket::Config::LOG_LEVEL, rocket::config::LogLevel::Off)) + .merge((rocket::Config::SHUTDOWN, shutdown)); + + let _rocket = self + .0 + .configure(config) + .launch() + .await + .map_err(shuttle_runtime::CustomError::new)?; + + Ok(()) + } +} + +impl From> for RocketService { + fn from(router: rocket::Rocket) -> Self { + Self(router) + } +} + +/// The return type that should be returned from the [shuttle_runtime::main] function. +pub type ShuttleRocket = Result; diff --git a/services/shuttle-salvo/Cargo.toml b/services/shuttle-salvo/Cargo.toml new file mode 100644 index 000000000..24e35a4a6 --- /dev/null +++ b/services/shuttle-salvo/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "shuttle-salvo" +version = "0.12.0" +edition = "2021" +license = "Apache-2.0" +description = "Service implementation to run a salvo webserver on shuttle" +keywords = ["shuttle-service", "salvo"] + +[workspace] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +salvo = { version = "0.37.5" } +shuttle-runtime = { path = "../../runtime", version = "0.12.0" } + +[dev-dependencies] +tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } diff --git a/services/shuttle-salvo/src/lib.rs b/services/shuttle-salvo/src/lib.rs new file mode 100644 index 000000000..a38d4ab28 --- /dev/null +++ b/services/shuttle-salvo/src/lib.rs @@ -0,0 +1,44 @@ +//! Shuttle service integration for the Salvo web framework. +//! ## Example +//! ```rust,no_run +//! use salvo::prelude::*; +//! +//! #[handler] +//! async fn hello_world(res: &mut Response) { +//! res.render(Text::Plain("Hello, world!")); +//! } +//! +//! #[shuttle_runtime::main] +//! async fn salvo() -> shuttle_salvo::ShuttleSalvo { +//! let router = Router::with_path("hello").get(hello_world); +//! +//! Ok(router.into()) +//! } +//! +//! ``` +use shuttle_runtime::Error; +use std::net::SocketAddr; + +/// A wrapper type for [salvo::Router] so we can implement [shuttle_runtime::Service] for it. +pub struct SalvoService(pub salvo::Router); + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for SalvoService { + /// Takes the router that is returned by the user in their [shuttle_runtime::main] function + /// and binds to an address passed in by shuttle. + async fn bind(mut self, addr: SocketAddr) -> Result<(), Error> { + salvo::Server::new(salvo::listener::TcpListener::bind(addr)) + .serve(self.0) + .await; + + Ok(()) + } +} + +impl From for SalvoService { + fn from(router: salvo::Router) -> Self { + Self(router) + } +} +/// The return type that should be returned from the [shuttle_runtime::main] function. +pub type ShuttleSalvo = Result; diff --git a/services/shuttle-serenity/Cargo.toml b/services/shuttle-serenity/Cargo.toml new file mode 100644 index 000000000..1e224a3f5 --- /dev/null +++ b/services/shuttle-serenity/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "shuttle-serenity" +version = "0.12.0" +edition = "2021" +license = "Apache-2.0" +description = "Service implementation to run a serenity server on shuttle" +keywords = ["shuttle-service", "serenity"] + +[workspace] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serenity = { version = "0.11.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } +shuttle-runtime = { path = "../../runtime", version = "0.12.0" } + +[dev-dependencies] +anyhow = "1.0.69" +shuttle-secrets = { path = "../../resources/secrets" } +tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } +tracing = "0.1.37" diff --git a/services/shuttle-serenity/src/lib.rs b/services/shuttle-serenity/src/lib.rs new file mode 100644 index 000000000..c216b80f9 --- /dev/null +++ b/services/shuttle-serenity/src/lib.rs @@ -0,0 +1,76 @@ +//! Shuttle service integration for the Serenity discord bot framework. +//! ## Example +//! ```rust,no_run +//! use anyhow::anyhow; +//! use serenity::async_trait; +//! use serenity::model::channel::Message; +//! use serenity::model::gateway::Ready; +//! use serenity::prelude::*; +//! use shuttle_secrets::SecretStore; +//! use tracing::{error, info}; +//! +//! struct Bot; +//! +//! #[async_trait] +//! impl EventHandler for Bot { +//! async fn message(&self, ctx: Context, msg: Message) { +//! if msg.content == "!hello" { +//! if let Err(e) = msg.channel_id.say(&ctx.http, "world!").await { +//! error!("Error sending message: {:?}", e); +//! } +//! } +//! } +//! +//! async fn ready(&self, _: Context, ready: Ready) { +//! info!("{} is connected!", ready.user.name); +//! } +//! } +//! +//! #[shuttle_runtime::main] +//! async fn serenity( +//! #[shuttle_secrets::Secrets] secret_store: SecretStore, +//! ) -> shuttle_serenity::ShuttleSerenity { +//! // Get the discord token set in `Secrets.toml` +//! let token = if let Some(token) = secret_store.get("DISCORD_TOKEN") { +//! token +//! } else { +//! return Err(anyhow!("'DISCORD_TOKEN' was not found").into()); +//! }; +//! +//! // Set gateway intents, which decides what events the bot will be notified about +//! let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; +//! +//! let client = Client::builder(&token, intents) +//! .event_handler(Bot) +//! .await +//! .expect("Err creating client"); +//! +//! Ok(client.into()) +//! } +//! +//! ``` +use shuttle_runtime::{CustomError, Error}; +use std::net::SocketAddr; + +/// A wrapper type for [serenity::Client] so we can implement [shuttle_runtime::Service] for it. +pub struct SerenityService(pub serenity::Client); + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for SerenityService { + /// Takes the client that is returned by the user in their [shuttle_runtime::main] function + /// and starts it. + async fn bind(mut self, _addr: SocketAddr) -> Result<(), Error> { + self.0.start().await.map_err(CustomError::new)?; + + Ok(()) + } +} + +impl From for SerenityService { + fn from(router: serenity::Client) -> Self { + Self(router) + } +} + +/// The return type that should be returned from the [shuttle_runtime::main] function. +pub type ShuttleSerenity = Result; diff --git a/services/shuttle-thruster/Cargo.toml b/services/shuttle-thruster/Cargo.toml new file mode 100644 index 000000000..14b82adfd --- /dev/null +++ b/services/shuttle-thruster/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "shuttle-thruster" +version = "0.12.0" +edition = "2021" +license = "Apache-2.0" +description = "Service implementation to run a thruster webserver on shuttle" +keywords = ["shuttle-service", "thruster"] + +[workspace] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +thruster = { version = "1.3.0" } +shuttle-runtime = { path = "../../runtime", version = "0.12.0" } + +[dev-dependencies] +thruster = { version = "1.3.0", features = ["hyper_server"] } +tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } diff --git a/services/shuttle-thruster/src/lib.rs b/services/shuttle-thruster/src/lib.rs new file mode 100644 index 000000000..f642db1dd --- /dev/null +++ b/services/shuttle-thruster/src/lib.rs @@ -0,0 +1,53 @@ +//! Shuttle service integration for the Thruster web framework. +//! ## Example +//! ```rust,no_run +//! use thruster::{ +//! context::basic_hyper_context::{generate_context, BasicHyperContext as Ctx, HyperRequest}, +//! m, middleware_fn, App, HyperServer, MiddlewareNext, MiddlewareResult, ThrusterServer, +//! }; +//! +//! #[middleware_fn] +//! async fn hello(mut context: Ctx, _next: MiddlewareNext) -> MiddlewareResult { +//! context.body("Hello, World!"); +//! Ok(context) +//! } +//! +//! #[shuttle_runtime::main] +//! async fn thruster() -> shuttle_thruster::ShuttleThruster> { +//! let server = HyperServer::new( +//! App::::create(generate_context, ()).get("/hello", m![hello]), +//! ); +//! +//! Ok(server.into()) +//! } +//! ``` +use shuttle_runtime::Error; +use std::net::SocketAddr; + +/// A wrapper type for [thruster::ThrusterServer] so we can implement [shuttle_runtime::Service] for it. +pub struct ThrusterService(pub T); + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for ThrusterService +where + T: thruster::ThrusterServer + Send + 'static, +{ + /// Takes the server that is returned by the user in their [shuttle_runtime::main] function + /// and binds to an address passed in by shuttle. + async fn bind(mut self, addr: SocketAddr) -> Result<(), Error> { + self.0.build(&addr.ip().to_string(), addr.port()).await; + + Ok(()) + } +} + +impl From for ThrusterService +where + T: thruster::ThrusterServer + Send + 'static, +{ + fn from(router: T) -> Self { + Self(router) + } +} +/// The return type that should be returned from the [shuttle_runtime::main] function. +pub type ShuttleThruster = Result, Error>; diff --git a/services/shuttle-tide/Cargo.toml b/services/shuttle-tide/Cargo.toml new file mode 100644 index 000000000..b58993dbb --- /dev/null +++ b/services/shuttle-tide/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "shuttle-tide" +version = "0.12.0" +edition = "2021" +license = "Apache-2.0" +description = "Service implementation to run a tide webserver on shuttle" +keywords = ["shuttle-service", "tide"] + +[workspace] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tide = { version = "0.16.0" } +shuttle-runtime = { path = "../../runtime", version = "0.12.0" } + +# Tide does not have tokio support. So make sure async-std is compatible with tokio +# https://github.com/http-rs/tide/issues/791 +[dependencies.async-std] +version = "1.12.0" +features = ["tokio1"] + +[dev-dependencies] +tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } diff --git a/services/shuttle-tide/src/lib.rs b/services/shuttle-tide/src/lib.rs new file mode 100644 index 000000000..c2ecb8807 --- /dev/null +++ b/services/shuttle-tide/src/lib.rs @@ -0,0 +1,40 @@ +//! Shuttle service integration for the Tide web framework. +//! ## Example +//! ```rust,no_run +//! #[shuttle_runtime::main] +//! async fn tide() -> shuttle_tide::ShuttleTide<()> { +//! let mut app = tide::new(); +//! app.with(tide::log::LogMiddleware::new()); +//! +//! app.at("/hello").get(|_| async { Ok("Hello, world!") }); +//! +//! Ok(app.into()) +//! } +//! ``` +use shuttle_runtime::{CustomError, Error}; +use std::net::SocketAddr; + +/// A wrapper type for [tide::Server(pub tide::Server); + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for TideService +where + T: Clone + Send + Sync + 'static, +{ + /// Takes the router that is returned by the user in their [shuttle_runtime::main] function + /// and binds to an address passed in by shuttle. + async fn bind(mut self, addr: SocketAddr) -> Result<(), Error> { + self.0.listen(addr).await.map_err(CustomError::new)?; + + Ok(()) + } +} + +impl From> for TideService { + fn from(router: tide::Server) -> Self { + Self(router) + } +} +/// The return type that should be returned from the [shuttle_runtime::main] function. +pub type ShuttleTide = Result, Error>; diff --git a/services/shuttle-tower/Cargo.toml b/services/shuttle-tower/Cargo.toml new file mode 100644 index 000000000..80ed9792d --- /dev/null +++ b/services/shuttle-tower/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "shuttle-tower" +version = "0.12.0" +edition = "2021" +license = "Apache-2.0" +description = "Service implementation to run a tower webserver on shuttle" +keywords = ["shuttle-service", "tower"] + +[workspace] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +hyper = { version = "0.14.23", features = ["server", "tcp", "http1"] } +shuttle-runtime = { path = "../../runtime", version = "0.12.0" } +tower = { version = "0.4.13", features = ["make"] } + +[dev-dependencies] +tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } diff --git a/services/shuttle-tower/src/lib.rs b/services/shuttle-tower/src/lib.rs new file mode 100644 index 000000000..6c00610cc --- /dev/null +++ b/services/shuttle-tower/src/lib.rs @@ -0,0 +1,87 @@ +//! Shuttle service integration for the Tower framework. +//! ## Example +//! ```rust,no_run +//! use std::convert::Infallible; +//! use std::future::Future; +//! use std::pin::Pin; +//! use std::task::{Context, Poll}; +//! +//! #[derive(Clone)] +//! struct HelloWorld; +//! +//! impl tower::Service> for HelloWorld { +//! type Response = hyper::Response; +//! type Error = Infallible; +//! type Future = Pin> + Send + Sync>>; +//! +//! fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { +//! Poll::Ready(Ok(())) +//! } +//! +//! fn call(&mut self, _req: hyper::Request) -> Self::Future { +//! let body = hyper::Body::from("Hello, world!"); +//! let resp = hyper::Response::builder() +//! .status(200) +//! .body(body) +//! .expect("Unable to create the `hyper::Response` object"); +//! +//! let fut = async { Ok(resp) }; +//! +//! Box::pin(fut) +//! } +//! } +//! +//! #[shuttle_runtime::main] +//! async fn tower() -> shuttle_tower::ShuttleTower { +//! let service = HelloWorld; +//! +//! Ok(service.into()) +//! } +//! ``` +use shuttle_runtime::{CustomError, Error}; +use std::net::SocketAddr; + +/// A wrapper type for [tower::Service] so we can implement [shuttle_runtime::Service] for it. +pub struct TowerService(pub T); + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for TowerService +where + T: tower::Service, Response = hyper::Response> + + Clone + + Send + + Sync + + 'static, + T::Error: std::error::Error + Send + Sync, + T::Future: std::future::Future + Send + Sync, +{ + /// Takes the service that is returned by the user in their [shuttle_runtime::main] function + /// and binds to an address passed in by shuttle. + async fn bind(mut self, addr: SocketAddr) -> Result<(), Error> { + let shared = tower::make::Shared::new(self.0); + hyper::Server::bind(&addr) + .serve(shared) + .await + .map_err(CustomError::new)?; + + Ok(()) + } +} + +impl From for TowerService +where + T: tower::Service, Response = hyper::Response> + + Clone + + Send + + Sync + + 'static, + T::Error: std::error::Error + Send + Sync, + T::Future: std::future::Future + Send + Sync, +{ + fn from(service: T) -> Self { + Self(service) + } +} + +/// The return type that should be returned from the [shuttle_runtime::main] function. +pub type ShuttleTower = Result, Error>; diff --git a/services/shuttle-warp/Cargo.toml b/services/shuttle-warp/Cargo.toml new file mode 100644 index 000000000..c7e84f5c7 --- /dev/null +++ b/services/shuttle-warp/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "shuttle-warp" +version = "0.12.0" +edition = "2021" +license = "Apache-2.0" +description = "Service implementation to run a warp webserver on shuttle" +keywords = ["shuttle-service", "warp"] + +[workspace] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +warp = { version = "0.3.3" } +shuttle-runtime = { path = "../../runtime", version = "0.12.0" } + +[dev-dependencies] +tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] } diff --git a/services/shuttle-warp/src/lib.rs b/services/shuttle-warp/src/lib.rs new file mode 100644 index 000000000..acaef062f --- /dev/null +++ b/services/shuttle-warp/src/lib.rs @@ -0,0 +1,53 @@ +//! Shuttle service integration for the Warp web framework. +//! ## Example +//! ```rust,no_run +//! use warp::Filter; +//! use warp::Reply; +//! +//! #[shuttle_runtime::main] +//! async fn warp() -> shuttle_warp::ShuttleWarp<(impl Reply,)> { +//! let route = warp::any().map(|| "Hello, World!"); +//! Ok(route.boxed().into()) +//! } +//! ``` +use shuttle_runtime::Error; +use std::net::SocketAddr; +use std::ops::Deref; + +/// A wrapper type for [warp::Filter] so we can implement [shuttle_runtime::Service] for it. +pub struct WarpService(pub T); + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for WarpService +where + T: Send + Sync + Clone + 'static + warp::Filter, + T::Extract: warp::reply::Reply, +{ + /// Takes the router that is returned by the user in their [shuttle_runtime::main] function + /// and binds to an address passed in by shuttle. + async fn bind(mut self, addr: SocketAddr) -> Result<(), Error> { + warp::serve((*self).clone()).run(addr).await; + Ok(()) + } +} + +impl From for WarpService +where + T: Send + Sync + Clone + 'static + warp::Filter, + T::Extract: warp::reply::Reply, +{ + fn from(router: T) -> Self { + Self(router) + } +} + +impl Deref for WarpService { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// The return type that should be returned from the [shuttle_runtime::main] function. +pub type ShuttleWarp = Result>, Error>; diff --git a/shell.nix b/shell.nix index 13fa0a32a..53928fbe1 100644 --- a/shell.nix +++ b/shell.nix @@ -13,6 +13,7 @@ in buildInputs = with nixpkgs; [ ((rustChannelOf{ channel = "1.65.0"; }).rust.override { extensions = ["rust-src"]; + targets = ["wasm32-wasi"]; }) cargo-watch terraform @@ -28,6 +29,7 @@ in sqlite fastmod pebble + kondo ]; PROTOC = "${protobuf}/bin/protoc";