mirror of
https://github.com/mii443/wasmer.git
synced 2025-08-22 16:35:33 +00:00
Merge branch 'release-5.0' into wamr
This commit is contained in:
4
.github/cross-linux-riscv64/Dockerfile
vendored
4
.github/cross-linux-riscv64/Dockerfile
vendored
@ -28,7 +28,7 @@ RUN apt-get update && \
|
||||
# install rust tools
|
||||
RUN curl --proto "=https" --tlsv1.2 --retry 3 -sSfL https://sh.rustup.rs | sh -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
RUN rustup -v toolchain install 1.74
|
||||
RUN rustup -v toolchain install 1.78
|
||||
# add docker the manual way
|
||||
COPY install_docker.sh /
|
||||
RUN /install_docker.sh
|
||||
@ -61,7 +61,7 @@ ENV CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER="$CROSS_TOOLCHAIN_PREFIX"gcc
|
||||
RUST_TEST_THREADS=1 \
|
||||
PKG_CONFIG_PATH="/usr/lib/riscv64-linux-gnu/pkgconfig/:${PKG_CONFIG_PATH}"
|
||||
|
||||
RUN rustup target add riscv64gc-unknown-linux-gnu --toolchain 1.74-x86_64-unknown-linux-gnu
|
||||
RUN rustup target add riscv64gc-unknown-linux-gnu --toolchain 1.78-x86_64-unknown-linux-gnu
|
||||
|
||||
#compile libssl-dev for riscv64!
|
||||
COPY build_openssl.sh /
|
||||
|
2
.github/workflows/benchmark.yaml
vendored
2
.github/workflows/benchmark.yaml
vendored
@ -7,7 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: git
|
||||
MSRV: "1.74"
|
||||
MSRV: "1.78"
|
||||
|
||||
jobs:
|
||||
run_benchmark:
|
||||
|
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@ -3,7 +3,7 @@ name: Builds
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: git
|
||||
MSRV: "1.74"
|
||||
MSRV: "1.78"
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -52,7 +52,7 @@ jobs:
|
||||
use_llvm: true
|
||||
build_wasm: true
|
||||
- build: macos-x64
|
||||
os: macos-11
|
||||
os: macos-12
|
||||
llvm_url: 'https://github.com/wasmerio/llvm-custom-builds/releases/download/15.x/llvm-darwin-amd64.tar.xz'
|
||||
artifact_name: 'wasmer-darwin-amd64'
|
||||
cross_compilation_artifact_name: 'cross_compiled_from_mac'
|
||||
@ -100,7 +100,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: matrix.use_sccache != true
|
||||
- name: Install LLVM (macOS Apple Silicon)
|
||||
if: matrix.os == 'macos-11.0' && !matrix.llvm_url
|
||||
if: matrix.os == 'macos-12' && !matrix.llvm_url
|
||||
run: |
|
||||
brew install llvm
|
||||
- name: Install LLVM
|
||||
@ -129,7 +129,7 @@ jobs:
|
||||
# using gnu-tar is a workaround for https://github.com/actions/cache/issues/403
|
||||
brew install gnu-tar
|
||||
echo PATH="/usr/local/opt/gnu-tar/libexec/gnubin:$PATH" >> $GITHUB_ENV
|
||||
if: matrix.os == 'macos-latest' || matrix.os == 'macos-11.0'
|
||||
if: matrix.os == 'macos-12'
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
@ -314,7 +314,7 @@ jobs:
|
||||
|
||||
darwin_aarch64_jsc:
|
||||
name: macOS aarch64 (JSC)
|
||||
runs-on: macos-11.0
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
@ -347,7 +347,7 @@ jobs:
|
||||
|
||||
darwin_x86_64_jsc:
|
||||
name: macOS x86_64 (JSC)
|
||||
runs-on: macos-11.0
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
2
.github/workflows/cloudcompiler.yaml
vendored
2
.github/workflows/cloudcompiler.yaml
vendored
@ -2,7 +2,7 @@ name: Release cloudcompiler.wasm
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
MSRV: "1.74"
|
||||
MSRV: "1.78"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
2
.github/workflows/documentation.yaml
vendored
2
.github/workflows/documentation.yaml
vendored
@ -8,7 +8,7 @@ on:
|
||||
- 'lib/**'
|
||||
|
||||
env:
|
||||
MSRV: "1.74"
|
||||
MSRV: "1.78"
|
||||
|
||||
jobs:
|
||||
documentation:
|
||||
|
82
.github/workflows/test.yaml
vendored
82
.github/workflows/test.yaml
vendored
@ -24,7 +24,7 @@ env:
|
||||
# Rust, but it's not stable on 1.69 yet. By explicitly setting the protocol we
|
||||
# can override that behaviour
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: git
|
||||
MSRV: "1.74"
|
||||
MSRV: "1.78"
|
||||
NEXTEST_PROFILE: "ci"
|
||||
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
|
||||
WASI_SDK_VERSION: "22"
|
||||
@ -213,7 +213,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "nightly-2023-10-05"
|
||||
toolchain: "nightly-2024-08-21"
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- run: cargo install toml-cli # toml-cli is required to run `make test-build-docs-rs`
|
||||
- name: make test-build-docs-rs-ci
|
||||
@ -371,13 +371,13 @@ jobs:
|
||||
},
|
||||
{
|
||||
build: macos-x64,
|
||||
os: macos-11,
|
||||
os: macos-12,
|
||||
target: x86_64-apple-darwin,
|
||||
llvm_url: 'https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.7/clang+llvm-15.0.7-x86_64-apple-darwin21.0.tar.xz'
|
||||
},
|
||||
{
|
||||
build: macos-arm,
|
||||
os: macos-11,
|
||||
os: macos-12,
|
||||
target: aarch64-apple-darwin,
|
||||
},
|
||||
{
|
||||
@ -413,7 +413,7 @@ jobs:
|
||||
# using gnu-tar is a workaround for https://github.com/actions/cache/issues/403
|
||||
brew install gnu-tar
|
||||
echo PATH="/usr/local/opt/gnu-tar/libexec/gnubin:$PATH" >> $GITHUB_ENV
|
||||
if: matrix.metadata.os == 'macos-latest' || matrix.metadata.os == 'macos-11.0'
|
||||
if: matrix.metadata.os == 'macos-12'
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@ -454,7 +454,7 @@ jobs:
|
||||
mkdir -p package/winsdk
|
||||
cp -r /tmp/winsdk/* package/winsdk
|
||||
- name: Install LLVM (macOS Apple Silicon)
|
||||
if: matrix.metadata.os == 'macos-11.0' && !matrix.metadata.llvm_url
|
||||
if: matrix.metadata.os == 'macos-12' && !matrix.metadata.llvm_url
|
||||
run: |
|
||||
brew install llvm
|
||||
- name: Install LLVM
|
||||
@ -596,7 +596,7 @@ jobs:
|
||||
},
|
||||
{
|
||||
build: macos-x64,
|
||||
os: macos-11,
|
||||
os: macos-12,
|
||||
target: x86_64-apple-darwin,
|
||||
exe: '',
|
||||
llvm_url: 'https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.7/clang+llvm-15.0.7-x86_64-apple-darwin21.0.tar.xz'
|
||||
@ -643,7 +643,7 @@ jobs:
|
||||
# using gnu-tar is a workaround for https://github.com/actions/cache/issues/403
|
||||
brew install gnu-tar
|
||||
echo PATH="/usr/local/opt/gnu-tar/libexec/gnubin:$PATH" >> $GITHUB_ENV
|
||||
if: matrix.metadata.os == 'macos-latest' || matrix.metadata.os == 'macos-11.0'
|
||||
if: matrix.metadata.os == 'macos-12'
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@ -652,7 +652,7 @@ jobs:
|
||||
- name: Install Nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: Install LLVM (macOS Apple Silicon)
|
||||
if: matrix.metadata.os == 'macos-11.0' && !matrix.metadata.llvm_url
|
||||
if: matrix.metadata.os == 'macos-12' && !matrix.metadata.llvm_url
|
||||
run: |
|
||||
brew install llvm
|
||||
- name: Install LLVM
|
||||
@ -710,12 +710,12 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
llvm_url: 'https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.6/clang+llvm-15.0.6-x86_64-linux-gnu-ubuntu-18.04.tar.xz'
|
||||
- build: macos-x64
|
||||
os: macos-11
|
||||
os: macos-12
|
||||
target: x86_64-apple-darwin
|
||||
llvm_url: 'https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.7/clang+llvm-15.0.7-x86_64-apple-darwin21.0.tar.xz'
|
||||
# we only build the integration-test CLI, we don't run tests
|
||||
- build: macos-arm
|
||||
os: macos-11
|
||||
os: macos-12
|
||||
target: aarch64-apple-darwin,
|
||||
- build: linux-musl
|
||||
target: x86_64-unknown-linux-musl
|
||||
@ -858,3 +858,63 @@ jobs:
|
||||
CARGO_TARGET: ${{ matrix.target }}
|
||||
WAPM_DEV_TOKEN: ${{ secrets.WAPM_DEV_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# there is another set of integration tests in 'wasmer-integration-tests' repo. Run those
|
||||
test-wasmer-integration-tests:
|
||||
needs: [build]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout wasmer-integration-tests repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: wasmerio/wasmer-integration-tests
|
||||
submodules: true
|
||||
token: ${{ secrets.CLONE_WASMER_INTEGRATION_TESTS }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: wasmer-cli-linux-x64
|
||||
- name: Cargo Registry Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/advisory-db
|
||||
~/.cargo/git
|
||||
~/.cargo/registry
|
||||
key: cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Cargo target cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
target/
|
||||
key: cargo-release-${{ hashFiles('**/Cargo.lock') }}
|
||||
- run: |
|
||||
# install rust toolchain
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
. "$HOME/.cargo/env"
|
||||
|
||||
# add wasmer cli to PATH
|
||||
tar -xzf build-wasmer.tar.gz
|
||||
|
||||
docker build -t tmp .
|
||||
docker run -v $PWD:/app -w /app tmp bash -c " \
|
||||
cp ./bin/wasmer /root/.wasmer/bin/wasmer &&\
|
||||
export MYSQL_HOST='${{ vars.INTEGRATION_TEST_MYSQL_HOST }}' &&\
|
||||
export MYSQL_DBNAME='${{ vars.INTEGRATION_TEST_MYSQL_DBNAME }}' &&\
|
||||
export MYSQL_USERNAME='${{ secrets.INTEGRATION_TEST_MYSQL_USERNAME }}' &&\
|
||||
export MYSQL_PASSWORD='${{ secrets.INTEGRATION_TEST_MYSQL_PASSWORD }}' &&\
|
||||
export MYSQL_PORT='${{ vars.INTEGRATION_TEST_MYSQL_PORT }}' &&\
|
||||
export MYSQL_CERT='${{ secrets.INTEGRATION_TEST_MYSQL_CERT }}' &&\
|
||||
export PG_HOST='${{ vars.INTEGRATION_TEST_PG_HOST }}' &&\
|
||||
export PG_DBNAME='${{ vars.INTEGRATION_TEST_PG_DBNAME }}' &&\
|
||||
export PG_USERNAME='${{ secrets.INTEGRATION_TEST_PG_USERNAME }}' &&\
|
||||
export PG_PASSWORD='${{ secrets.INTEGRATION_TEST_PG_PASSWORD }}' &&\
|
||||
export PG_PORT='${{ vars.INTEGRATION_TEST_PG_PORT }}' &&\
|
||||
wasmer config set registry.url https://registry.wasmer.io/graphql &&\
|
||||
wasmer login '${{ secrets.WAPM_PROD_TOKEN }}' &&\
|
||||
wasmer config set registry.url https://registry.wasmer.wtf/graphql &&\
|
||||
wasmer login '${{ secrets.WAPM_DEV_TOKEN }}' &&\
|
||||
cargo test --no-fail-fast"
|
||||
- name: notify failure in slack
|
||||
if: failure()
|
||||
run: |
|
||||
curl -X POST -H 'Content-type: application/json' --data '{"text":"Integration tests failed ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' ${{ secrets.INTEGRATION_TEST_SLACK_WEBHOOK }}
|
||||
|
51
.github/workflows/wasmer-integration-tests.yaml
vendored
Normal file
51
.github/workflows/wasmer-integration-tests.yaml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
name: test-sys
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- sre-383-re-enable-ignored-integration-test-test_php_extensions
|
||||
|
||||
jobs:
|
||||
# there is another set of integration tests in 'wasmer-integration-tests' repo. Run those
|
||||
test-wasmer-integration-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout wasmer-integration-tests repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: wasmerio/wasmer-integration-tests
|
||||
submodules: true
|
||||
token: ${{ secrets.CLONE_WASMER_INTEGRATION_TESTS }}
|
||||
- name: Cargo Registry Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/advisory-db
|
||||
~/.cargo/git
|
||||
~/.cargo/registry
|
||||
key: cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
- name: Cargo target cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
target/
|
||||
key: cargo-release-${{ hashFiles('**/Cargo.lock') }}
|
||||
- run: |
|
||||
docker build -t tmp .
|
||||
docker run -v $PWD:/app -w /app tmp bash -c " \
|
||||
export MYSQL_HOST='${{ vars.INTEGRATION_TEST_MYSQL_HOST }}' &&\
|
||||
export MYSQL_DBNAME='${{ vars.INTEGRATION_TEST_MYSQL_DBNAME }}' &&\
|
||||
export MYSQL_USERNAME='${{ secrets.INTEGRATION_TEST_MYSQL_USERNAME }}' &&\
|
||||
export MYSQL_PASSWORD='${{ secrets.INTEGRATION_TEST_MYSQL_PASSWORD }}' &&\
|
||||
export MYSQL_PORT='${{ vars.INTEGRATION_TEST_MYSQL_PORT }}' &&\
|
||||
export MYSQL_CERT='${{ secrets.INTEGRATION_TEST_MYSQL_CERT }}' &&\
|
||||
export PG_HOST='${{ vars.INTEGRATION_TEST_PG_HOST }}' &&\
|
||||
export PG_DBNAME='${{ vars.INTEGRATION_TEST_PG_DBNAME }}' &&\
|
||||
export PG_USERNAME='${{ secrets.INTEGRATION_TEST_PG_USERNAME }}' &&\
|
||||
export PG_PASSWORD='${{ secrets.INTEGRATION_TEST_PG_PASSWORD }}' &&\
|
||||
export PG_PORT='${{ vars.INTEGRATION_TEST_PG_PORT }}' &&\
|
||||
wasmer config set registry.url https://registry.wasmer.io/graphql &&\
|
||||
wasmer login ${{ secrets.WAPM_PROD_TOKEN }} &&\
|
||||
wasmer config set registry.url https://registry.wasmer.wtf/graphql &&\
|
||||
wasmer login ${{ secrets.WAPM_DEV_TOKEN }} &&\
|
||||
cargo test --no-fail-fast"
|
176
CHANGELOG.md
176
CHANGELOG.md
@ -9,6 +9,182 @@ Looking for changes that affect our C API? See the [C API Changelog](lib/c-api/C
|
||||
|
||||
## **Unreleased**
|
||||
|
||||
## 4.3.7 - 06/09/2024
|
||||
|
||||
This release adds support for rotating secrets, fixes a regression with the filesystem, and contains other fixes and improvments.
|
||||
|
||||
## Added
|
||||
|
||||
- [#5070](https://github.com/wasmerio/wasmer/pull/5070) Add `rotate-secrets` subcommand for volumes and minor changes
|
||||
- [#5057](https://github.com/wasmerio/wasmer/pull/5057) Add `cwd` to the manifest as a command annotation
|
||||
- [#5060](https://github.com/wasmerio/wasmer/pull/5060) feat(backend-api): Add env var to toggle GQL variable logging
|
||||
- [#5059](https://github.com/wasmerio/wasmer/pull/5059) feat(backend-api): Add updatedAt timestamps to Edge App/Version
|
||||
- [#5016](https://github.com/wasmerio/wasmer/pull/5016) add access to php integration test secrets
|
||||
- [#5036](https://github.com/wasmerio/wasmer/pull/5036) Add help-docs for the `--watch` flag in `wasmer app logs`
|
||||
|
||||
## Changed
|
||||
|
||||
- [#5066](https://github.com/wasmerio/wasmer/pull/5066) Prevent redundant merging of the filesystems
|
||||
- [#5037](https://github.com/wasmerio/wasmer/pull/5037) post slack message on integration tests ci failure
|
||||
- [#5063](https://github.com/wasmerio/wasmer/pull/5063) refactor volumes schema
|
||||
- [#5056](https://github.com/wasmerio/wasmer/pull/5056) Improve validation requests in "wasmer deploy"
|
||||
- [#5053](https://github.com/wasmerio/wasmer/pull/5053) Reduce overhead of chunk timeouts in wasix package downloads
|
||||
- [#5042](https://github.com/wasmerio/wasmer/pull/5042) Expose memory and wasi generic imports
|
||||
- [#5040](https://github.com/wasmerio/wasmer/pull/5040) deps: bump tun-tap version for wasmer-cli
|
||||
- [#5043](https://github.com/wasmerio/wasmer/pull/5043) Remove serde_cbor as a dependency
|
||||
- [#5030](https://github.com/wasmerio/wasmer/pull/5030) Improve loop metering tests
|
||||
|
||||
## Fixed
|
||||
|
||||
- [#5039](https://github.com/wasmerio/wasmer/pull/5039) Fix missing hash from a compiled artifact
|
||||
|
||||
|
||||
|
||||
## 4.3.6 - 22/08/2024
|
||||
|
||||
The star of this release is the volume subcommand that allows inspecting and interacting with volumes through the client. There are also
|
||||
numerous bug fixes and quality of life improvements included in this release.
|
||||
|
||||
## Added
|
||||
|
||||
- [#5022](https://github.com/wasmerio/wasmer/pull/5022) feat(cli): Add --print-rclone-config to "app volumes s3-credentials"
|
||||
- [#4980](https://github.com/wasmerio/wasmer/pull/4980) feat(backend-api): Add disabledAt/Reason to AppVersion
|
||||
- [#5007](https://github.com/wasmerio/wasmer/pull/5007) add access to php integration test secrets
|
||||
- [#5003](https://github.com/wasmerio/wasmer/pull/5003) Added a transaction and auto_consistency journal
|
||||
- [#5011](https://github.com/wasmerio/wasmer/pull/5011) Add module-level middleware errors
|
||||
- [#4982](https://github.com/wasmerio/wasmer/pull/4982) Mount [fs] entries as additional mapped directories when running a lo…
|
||||
- [#4990](https://github.com/wasmerio/wasmer/pull/4990) Added a patch for corrupt freed FD lists after replaying journals
|
||||
- [#4967](https://github.com/wasmerio/wasmer/pull/4967) CLI: Add flag and logic to redeploy an app after changing secrets
|
||||
- [#4960](https://github.com/wasmerio/wasmer/pull/4960) Add `regions` subcommand to `app` subcommand
|
||||
- [#4941](https://github.com/wasmerio/wasmer/pull/4941) App config: add `locality` field
|
||||
|
||||
## Changed
|
||||
|
||||
- [#5020](https://github.com/wasmerio/wasmer/pull/5020) Allow memories exported from WASI(X) modules to be named anything
|
||||
- [#5025](https://github.com/wasmerio/wasmer/pull/5025) CLI: Allow sorting in "app list" command
|
||||
- [#5018](https://github.com/wasmerio/wasmer/pull/5018) Translate English sentance in German README.md
|
||||
- [#5008](https://github.com/wasmerio/wasmer/pull/5008) Allow nested mounted paths in `UnionFilesystem`
|
||||
- [#5021](https://github.com/wasmerio/wasmer/pull/5021) feat(virtual-fs): Expose create_dir_all
|
||||
- [#5017](https://github.com/wasmerio/wasmer/pull/5017) Use ___chkstk_ms for mingw
|
||||
- [#4996](https://github.com/wasmerio/wasmer/pull/4996) CLI(package/tag): Don't cache previously fetched user package
|
||||
- [#5015](https://github.com/wasmerio/wasmer/pull/5015) Consume unused result in `lib/compiler/src/engine/artifact.rs`
|
||||
- [#5013](https://github.com/wasmerio/wasmer/pull/5013) CLI: Volumes commands
|
||||
- [#5001](https://github.com/wasmerio/wasmer/pull/5001) deps: use windows-sys instead of winapi
|
||||
- [#4994](https://github.com/wasmerio/wasmer/pull/4994) Check if a user can deploy an app before deploying it
|
||||
- [#4995](https://github.com/wasmerio/wasmer/pull/4995) refactor(cli): Rename "container unpack" command to "package unpack"
|
||||
- [#4988](https://github.com/wasmerio/wasmer/pull/4988) chore: Use hex encoding for Sha256Hash debug impl
|
||||
- [#4780](https://github.com/wasmerio/wasmer/pull/4780) WASIX Test Suite
|
||||
- [#4987](https://github.com/wasmerio/wasmer/pull/4987) feat: Support filtering logs by request id and by instance id
|
||||
- [#4975](https://github.com/wasmerio/wasmer/pull/4975) CLI: Ask users to allow networking capabilities if not set
|
||||
- [#4977](https://github.com/wasmerio/wasmer/pull/4977) feat(wasix): Expose the builtin package loder module
|
||||
- [#4968](https://github.com/wasmerio/wasmer/pull/4968) CLI: Introduce new `auth` subcommand
|
||||
- [#4973](https://github.com/wasmerio/wasmer/pull/4973) feat: Implement image validation in BuiltinPackageLoader
|
||||
- [#4976](https://github.com/wasmerio/wasmer/pull/4976) CLI: No string manipulation on `app.yaml`
|
||||
- [#4970](https://github.com/wasmerio/wasmer/pull/4970) CLI: Cache templates from different registries in diffferent directories
|
||||
- [#4965](https://github.com/wasmerio/wasmer/pull/4965) CLI: Try to fetch owner from `app_id` if necessary while deploying
|
||||
- [#4955](https://github.com/wasmerio/wasmer/pull/4955) Move `WasmerConfig` out of the `registry_api` crate and introduce a `logout` subcommand
|
||||
- [#4943](https://github.com/wasmerio/wasmer/pull/4943) Run tests in wasmer-integration-tests repo on pull requests
|
||||
- [#4954](https://github.com/wasmerio/wasmer/pull/4954) App config: https redirect setting
|
||||
- [#4689](https://github.com/wasmerio/wasmer/pull/4689) redirect to child node in a different fs
|
||||
- [#4939](https://github.com/wasmerio/wasmer/pull/4939) Clearer doc comments
|
||||
|
||||
## Fixed
|
||||
|
||||
- [#5032](https://github.com/wasmerio/wasmer/pull/5032) fix(wasix): Prevent blocking package hash validations after downloads
|
||||
- [#5026](https://github.com/wasmerio/wasmer/pull/5026) Various volumes CLI fixes
|
||||
- [#4986](https://github.com/wasmerio/wasmer/pull/4986) Multiple fixes for the journal to fix bootstrapping
|
||||
- [#5005](https://github.com/wasmerio/wasmer/pull/5005) Enable feature `net` for mio in virtual-net, fix #5004
|
||||
- [#4992](https://github.com/wasmerio/wasmer/pull/4992) fix(cli/WasmerEnv): strip `registry.` only if hostname starts with it
|
||||
- [#4989](https://github.com/wasmerio/wasmer/pull/4989) fix(wasix): Fix panic on unknown socket in sock_send
|
||||
- [#4956](https://github.com/wasmerio/wasmer/pull/4956) Fix impossible relocation error in emit sdiv64 for singlepass compiler
|
||||
- [#4938](https://github.com/wasmerio/wasmer/pull/4938) fix(cli/package/tag): Better messages when tagging an existing package
|
||||
- [#4946](https://github.com/wasmerio/wasmer/pull/4946) Fixes a panic caused on the recv_from path
|
||||
- [#4942](https://github.com/wasmerio/wasmer/pull/4942) Fix size computation for `pwrite`
|
||||
|
||||
|
||||
|
||||
## 4.3.5 - 16/07/2024
|
||||
|
||||
This release adds support for managing secrets alongside fixes and refactors to help with stability.
|
||||
|
||||
## Added
|
||||
|
||||
- [#4930](https://github.com/wasmerio/wasmer/pull/4930) CLI: Add support for Secrets
|
||||
|
||||
## Changed
|
||||
|
||||
- [#4933](https://github.com/wasmerio/wasmer/pull/4933) CLI: Manually exit upon SIGINT reception
|
||||
- [#4927](https://github.com/wasmerio/wasmer/pull/4927) Upgrade to Hyper 1.x
|
||||
- [#4932](https://github.com/wasmerio/wasmer/pull/4932) Follow directory when deploying from create
|
||||
- [#4928](https://github.com/wasmerio/wasmer/pull/4928) Prevent unnecessary panics when compiling on x86_64 CPUs that dont support AVX or SSE4.2
|
||||
- [#4920](https://github.com/wasmerio/wasmer/pull/4920) Compile `wasmer-api` crate to `wasm32-unknown-unknown`
|
||||
|
||||
## Fixed
|
||||
|
||||
- [#4925](https://github.com/wasmerio/wasmer/pull/4925) Fix stack_low detection when data_end is above stack_pointer and stac…
|
||||
- [#4929](https://github.com/wasmerio/wasmer/pull/4929) Fix indirect call to dynamic imported function
|
||||
|
||||
|
||||
|
||||
## 4.3.4 - 08/07/2024
|
||||
|
||||
This release contains a fix for the webc version resolution logic.
|
||||
|
||||
## Added
|
||||
|
||||
- [#4914](https://github.com/wasmerio/wasmer/pull/4914) Add tests for `msync`
|
||||
|
||||
## Changed
|
||||
|
||||
|
||||
## Fixed
|
||||
|
||||
- [#4922](https://github.com/wasmerio/wasmer/pull/4922) fix(wasix): Fix incorrect webc version mapping
|
||||
|
||||
|
||||
|
||||
## 4.3.3 - 04/07/2024
|
||||
|
||||
This release mainly contains fixes and refactors to help with the stability. Also some improvements to the cli and journaling.
|
||||
|
||||
## Added
|
||||
|
||||
- [#4885](https://github.com/wasmerio/wasmer/pull/4885) Added another journal transaction point used for durability
|
||||
- [#4906](https://github.com/wasmerio/wasmer/pull/4906) feat(backend-api): Add query for retrieving app versions by id
|
||||
- [#4861](https://github.com/wasmerio/wasmer/pull/4861) feat(cli/deploy): Add `--path` flag
|
||||
|
||||
## Changed
|
||||
|
||||
- [#4898](https://github.com/wasmerio/wasmer/pull/4898) chore: More logging during package downloads
|
||||
- [#4912](https://github.com/wasmerio/wasmer/pull/4912) feat(cli/push): Push with name if available (+ CLI flag)
|
||||
- [#4907](https://github.com/wasmerio/wasmer/pull/4907) Update README: new zig bindings for wasmer with WASI Support
|
||||
- [#4897](https://github.com/wasmerio/wasmer/pull/4897) App create: Ask for framework before fetching templates
|
||||
- [#4896](https://github.com/wasmerio/wasmer/pull/4896) Re-use previously closed FDs
|
||||
- [#4883](https://github.com/wasmerio/wasmer/pull/4883) cli(app/create): Ask for directory to create the app in
|
||||
- [#4884](https://github.com/wasmerio/wasmer/pull/4884) feat(cli): Amend `app` command description
|
||||
- [#4874](https://github.com/wasmerio/wasmer/pull/4874) Switch to webc v3 by default
|
||||
- [#4860](https://github.com/wasmerio/wasmer/pull/4860) CLI: Use a channel to signal that the main fn is done
|
||||
- [#4877](https://github.com/wasmerio/wasmer/pull/4877) Replace mach with mach2
|
||||
- [#4824](https://github.com/wasmerio/wasmer/pull/4824) Merge `ScopedDirectoryFileSystem` into `host_fs::Filesystem`
|
||||
- [#4862](https://github.com/wasmerio/wasmer/pull/4862) feat(cli/deploy): Check status when waiting for the app to be available
|
||||
- [#4848](https://github.com/wasmerio/wasmer/pull/4848) feat(cli/package-tag): Manifest is not mandatory anymore while tagging
|
||||
- [#4858](https://github.com/wasmerio/wasmer/pull/4858) chore: Small tracing log improvements for wasix
|
||||
- [#4841](https://github.com/wasmerio/wasmer/pull/4841) Restore cursor after SIGINT during dialogue
|
||||
- [#4846](https://github.com/wasmerio/wasmer/pull/4846) Restored sockets from journals will no longer attempt to send data
|
||||
- [#4836](https://github.com/wasmerio/wasmer/pull/4836) Dynamically detect libgcc-vs-libunwind
|
||||
- [#4838](https://github.com/wasmerio/wasmer/pull/4838) Don't pass auth header when downloading a package
|
||||
- [#4832](https://github.com/wasmerio/wasmer/pull/4832) 4.3.2 post release
|
||||
|
||||
## Fixed
|
||||
|
||||
- [#4913](https://github.com/wasmerio/wasmer/pull/4913) Fix mac runners
|
||||
- [#4840](https://github.com/wasmerio/wasmer/pull/4840) fix(journal): Correctly handle client socket closing during compaction
|
||||
- [#4844](https://github.com/wasmerio/wasmer/pull/4844) fix(cli): Respect WASMER_REGISTRY env var in all commands
|
||||
- [#4834](https://github.com/wasmerio/wasmer/pull/4834) Fix `path_open` trailing slash edge case
|
||||
- [#4835](https://github.com/wasmerio/wasmer/pull/4835) Fix for dead sockets restored in the journal
|
||||
|
||||
|
||||
|
||||
## 4.3.2 - 11/06/2024
|
||||
|
||||
This release mainly introduces the InstaBoot feature. Numerous bug fixes to the virtual-fs is also included, making it
|
||||
|
1837
Cargo.lock
generated
1837
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
35
Cargo.toml
35
Cargo.toml
@ -12,21 +12,21 @@ rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
wasmer = { version = "=4.3.2", path = "lib/api", default-features = false }
|
||||
wasmer-compiler = { version = "=4.3.2", path = "lib/compiler", features = [
|
||||
wasmer = { version = "=4.3.7", path = "lib/api", default-features = false }
|
||||
wasmer-compiler = { version = "=4.3.7", path = "lib/compiler", features = [
|
||||
"compiler",
|
||||
|
||||
], optional = true }
|
||||
wasmer-compiler-cranelift = { version = "=4.3.2", path = "lib/compiler-cranelift", optional = true }
|
||||
wasmer-compiler-singlepass = { version = "=4.3.2", path = "lib/compiler-singlepass", optional = true }
|
||||
wasmer-compiler-llvm = { version = "=4.3.2", path = "lib/compiler-llvm", optional = true }
|
||||
wasmer-emscripten = { version = "=4.3.2", path = "lib/emscripten", optional = true }
|
||||
wasmer-compiler-cranelift = { version = "=4.3.7", path = "lib/compiler-cranelift", optional = true }
|
||||
wasmer-compiler-singlepass = { version = "=4.3.7", path = "lib/compiler-singlepass", optional = true }
|
||||
wasmer-compiler-llvm = { version = "=4.3.7", path = "lib/compiler-llvm", optional = true }
|
||||
wasmer-emscripten = { version = "=4.3.7", path = "lib/emscripten", optional = true }
|
||||
wasmer-wasix = { path = "lib/wasix", optional = true }
|
||||
wasmer-wast = { version = "=4.3.2", path = "tests/lib/wast", optional = true }
|
||||
wasi-test-generator = { version = "=4.3.2", path = "tests/wasi-wast", optional = true }
|
||||
wasmer-cache = { version = "=4.3.2", path = "lib/cache", optional = true }
|
||||
wasmer-types = { version = "=4.3.2", path = "lib/types" }
|
||||
wasmer-middlewares = { version = "=4.3.2", path = "lib/middlewares", optional = true }
|
||||
wasmer-wast = { version = "=4.3.7", path = "tests/lib/wast", optional = true }
|
||||
wasi-test-generator = { version = "=4.3.7", path = "tests/wasi-wast", optional = true }
|
||||
wasmer-cache = { version = "=4.3.7", path = "lib/cache", optional = true }
|
||||
wasmer-types = { version = "=4.3.7", path = "lib/types" }
|
||||
wasmer-middlewares = { version = "=4.3.7", path = "lib/middlewares", optional = true }
|
||||
|
||||
# Third party dependencies
|
||||
cfg-if = "1.0"
|
||||
@ -86,7 +86,7 @@ homepage = "https://wasmer.io/"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/wasmerio/wasmer"
|
||||
rust-version = "1.74"
|
||||
version = "4.3.2"
|
||||
version = "4.3.7"
|
||||
|
||||
[workspace.dependencies]
|
||||
# Repo-local crates
|
||||
@ -94,20 +94,25 @@ wasmer-config = { path = "./lib/config" }
|
||||
wasmer-wasix = { path = "./lib/wasix" }
|
||||
|
||||
# Wasmer-owned crates
|
||||
webc = { version = "6.0.0-rc1", default-features = false, features = ["package"] }
|
||||
webc = { version = "6.0.0", default-features = false, features = ["package"] }
|
||||
edge-schema = { version = "=0.1.0" }
|
||||
shared-buffer = "0.1.4"
|
||||
|
||||
# Third-party crates
|
||||
dashmap = "6.0.1"
|
||||
http = "1.0.0"
|
||||
hyper = "1"
|
||||
reqwest = { version = "0.12.0", default-features = false }
|
||||
enumset = "1.1.0"
|
||||
memoffset = "0.9.0"
|
||||
wasmparser = { version = "0.121.0", default-features = false }
|
||||
wasmparser = { version = "0.216.0", default-features = false, features = ["validate"]}
|
||||
rkyv = { version = "0.7.40", features = ["indexmap", "validation", "strict"] }
|
||||
memmap2 = { version = "0.6.2" }
|
||||
toml = { version = "0.5.9", features = ["preserve_order"] }
|
||||
indexmap = "2"
|
||||
serde_yaml = "0.9.34"
|
||||
libc = { version = "^0.2", default-features = false }
|
||||
gimli = { version = "0.28.1"}
|
||||
|
||||
[build-dependencies]
|
||||
test-generator = { path = "tests/lib/test-generator" }
|
||||
@ -117,7 +122,7 @@ glob = "0.3"
|
||||
rustc_version = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
wasmer = { version = "=4.3.2", path = "lib/api", features = [
|
||||
wasmer = { version = "=4.3.7", path = "lib/api", features = [
|
||||
"compiler",
|
||||
"singlepass",
|
||||
"sys",
|
||||
|
4
Makefile
4
Makefile
@ -468,10 +468,10 @@ test-build-docs-rs-ci:
|
||||
fi; \
|
||||
printf "*** Building doc for package with manifest $$manifest_path ***\n\n"; \
|
||||
if [ -z "$$features" ]; then \
|
||||
RUSTDOCFLAGS="--cfg=docsrs" $(CARGO_BINARY) +nightly-2023-10-05 doc $(CARGO_TARGET_FLAG) --manifest-path "$$manifest_path" --no-deps --locked || exit 1; \
|
||||
RUSTDOCFLAGS="--cfg=docsrs" $(CARGO_BINARY) +nightly-2024-08-21 doc $(CARGO_TARGET_FLAG) --manifest-path "$$manifest_path" --no-deps --locked || exit 1; \
|
||||
else \
|
||||
printf "Following features are inferred from Cargo.toml: $$features\n\n\n"; \
|
||||
RUSTDOCFLAGS="--cfg=docsrs" $(CARGO_BINARY) +nightly-2023-10-05 doc $(CARGO_TARGET_FLAG) --manifest-path "$$manifest_path" --no-deps --features "$$features" --locked || exit 1; \
|
||||
RUSTDOCFLAGS="--cfg=docsrs" $(CARGO_BINARY) +nightly-2024-08-21 doc $(CARGO_TARGET_FLAG) --manifest-path "$$manifest_path" --no-deps --features "$$features" --locked || exit 1; \
|
||||
fi; \
|
||||
done
|
||||
|
||||
|
10
README.md
10
README.md
@ -22,7 +22,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
Wasmer is a _blazing fast_ and _secure_ [**WebAssembly**](https://webassembly.org) runtime that enables incredibly
|
||||
_lightweight containers_ to run anywhere: from _Desktop_ to the _Cloud_, _Edge_ and your browser.
|
||||
@ -128,6 +128,7 @@ languages** with the Wasmer SDK:
|
||||
| ![C++ logo] | [**C++**][C integration] | [`wasm.hh` header] | [Learn][c docs] |
|
||||
| ![C# logo] | [**C#**][C# integration] | [`WasmerSharp` NuGet package] | [Learn][c# docs] |
|
||||
| ![D logo] | [**D**][D integration] | [`wasmer` Dub package] | [Learn][d docs] |
|
||||
| ![Zig logo] | [**Zig**][Zig integration] | [`wasmer` Zig package] | [Learn][zig docs] |
|
||||
| ![Python logo] | [**Python**][Python integration] | [`wasmer` PyPI package] | [Learn][python docs] |
|
||||
| ![JS logo] | [**Javascript**][JS integration] | [`@wasmerio` NPM packages] | [Learn][js docs] |
|
||||
| ![Go logo] | [**Go**][Go integration] | [`wasmer` Go package] | [Learn][go docs] |
|
||||
@ -137,7 +138,6 @@ languages** with the Wasmer SDK:
|
||||
| ![R logo] | [**R**][R integration] | _no published package_ | [Learn][r docs] |
|
||||
| ![Postgres logo] | [**Postgres**][Postgres integration] | _no published package_ | [Learn][postgres docs] |
|
||||
| ![Swift logo] | [**Swift**][Swift integration] | _no published package_ | |
|
||||
| ![Zig logo] | [**Zig**][Zig integration] | _no published package_ | |
|
||||
| ![Dart logo] | [**Dart**][Dart integration] | [`wasm` pub package] | |
|
||||
| ![Crystal logo] | [**Crystal**][Crystal integration] | _no published package_ | [Learn][crystal docs] |
|
||||
| ![Lisp logo] | [**Lisp**][Lisp integration] | _no published package_ | |
|
||||
@ -201,8 +201,10 @@ languages** with the Wasmer SDK:
|
||||
[postgres docs]: https://github.com/wasmerio/wasmer-postgres#usage--documentation
|
||||
[swift logo]: https://raw.githubusercontent.com/wasmerio/wasmer/master/assets/languages/swift.svg
|
||||
[swift integration]: https://github.com/AlwaysRightInstitute/SwiftyWasmer
|
||||
[zig logo]: https://raw.githubusercontent.com/ziglang/logo/master/zig-favicon.png
|
||||
[zig integration]: https://github.com/zigwasm/wasmer-zig
|
||||
[zig logo]: https://raw.githubusercontent.com/ziglang/logo/master/zig-mark.svg
|
||||
[zig integration]: https://github.com/Afirium/wasmer-zig-api
|
||||
[`wasmer` Zig package]: https://github.com/Afirium/wasmer-zig-api/releases/
|
||||
[zig docs]: https://wasmer-zig-api.crappy.systems/
|
||||
[dart logo]: https://raw.githubusercontent.com/wasmerio/wasmer/master/assets/languages/dart.svg
|
||||
[dart integration]: https://github.com/dart-lang/wasm
|
||||
[`wasm` pub package]: https://pub.dev/packages/wasm
|
||||
|
@ -78,9 +78,9 @@ curl https://get.wasmer.io -sSfL | sh
|
||||
|
||||
* <a href="https://crates.io/crates/wasmer-cli/">Cargo</a>
|
||||
|
||||
_Note: All the available
|
||||
features are described in the [`wasmer-cli`
|
||||
crate docs](https://github.com/wasmerio/wasmer/tree/main/lib/cli/README.md)_
|
||||
_Notiz: Alle verfügbaren Merkmale sind in der [`wasmer-cli`
|
||||
crate Dokumentation](https://github.com/wasmerio/wasmer/tree/main/lib/cli/README.md)
|
||||
beschrieben._
|
||||
|
||||
```sh
|
||||
cargo install wasmer-cli
|
||||
|
475
docs/schema/generated/jsonschema/types/AppConfigV1.schema.json
Normal file
475
docs/schema/generated/jsonschema/types/AppConfigV1.schema.json
Normal file
@ -0,0 +1,475 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"title": "AppConfigV1",
|
||||
"description": "User-facing app.yaml config file for apps.\n\nNOTE: only used by the backend, Edge itself does not use this format, and uses [`super::AppVersionV1Spec`] instead.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"package"
|
||||
],
|
||||
"properties": {
|
||||
"app_id": {
|
||||
"description": "App id assigned by the backend.\n\nThis will get populated once the app has been deployed.\n\nThis id is also used to map to the existing app during deployments.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppConfigCapabilityMapV1"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cli_args": {
|
||||
"description": "Only applicable for runners that accept CLI arguments.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"description": "Enable debug mode, which will show detailed error pages in the web gateway.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"domains": {
|
||||
"description": "Domains for the app.\n\nThis can include both provider-supplied alias domains and custom domains.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"description": "Environment variables.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"health_checks": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/HealthCheckV1"
|
||||
}
|
||||
},
|
||||
"locality": {
|
||||
"description": "Location-related configuration for the app.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Locality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the app.",
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"description": "Owner of the app.\n\nThis is either a username or a namespace.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"package": {
|
||||
"description": "The package to execute.",
|
||||
"$ref": "#/definitions/PackageSource"
|
||||
},
|
||||
"redirect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Redirect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scaling": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppScalingConfigV1"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scheduled_tasks": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/AppScheduledTask"
|
||||
}
|
||||
},
|
||||
"volumes": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/AppVolume"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"definitions": {
|
||||
"AppConfigCapabilityInstaBootV1": {
|
||||
"description": "Enables accelerated instance boot times with startup snapshots.\n\nHow it works: The Edge runtime will create a pre-initialized snapshot of apps that is ready to serve requests Your app will then restore from the generated snapshot, which has the potential to significantly speed up cold starts.\n\nTo drive the initialization, multiple http requests can be specified. All the specified requests will be sent to the app before the snapshot is created, allowing the app to pre-load files, pre initialize caches, ...",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"max_age": {
|
||||
"description": "Maximum age of snapshots.\n\nFormat: 5m, 1h, 2d, ...\n\nAfter the specified time new snapshots will be created, and the old ones discarded.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"requests": {
|
||||
"description": "HTTP requests to perform during startup snapshot creation. Apps can perform all the appropriate warmup logic in these requests.\n\nNOTE: if no requests are configured, then a single HTTP request to '/' will be performed instead.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/HttpRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppConfigCapabilityMapV1": {
|
||||
"description": "Restricted version of [`super::CapabilityMapV1`], with only a select subset of settings.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"instaboot": {
|
||||
"description": "Enables app bootstrapping with startup snapshots.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppConfigCapabilityInstaBootV1"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"memory": {
|
||||
"description": "Instance memory settings.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppConfigCapabilityMemoryV1"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"AppConfigCapabilityMemoryV1": {
|
||||
"description": "Memory capability settings.\n\nNOTE: this is kept separate from the [`super::CapabilityMemoryV1`] struct to have separation between the high-level app.yaml and the more internal App entity.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"description": "Memory limit for an instance.\n\nFormat: [digit][unit], where unit is Mb/Gb/MiB/GiB,...",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppScalingConfigV1": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppScalingModeV1"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppScalingModeV1": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"single_concurrency"
|
||||
]
|
||||
},
|
||||
"AppScheduledTask": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppVolume": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"mount",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"mount": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HealthCheckHttpV1": {
|
||||
"description": "Health check configuration for http endpoints.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"body": {
|
||||
"description": "Request body as a string.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"expect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/HttpRequestExpect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"headers": {
|
||||
"description": "HTTP headers added to the request.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/HttpHeader"
|
||||
}
|
||||
},
|
||||
"healthy_threshold": {
|
||||
"description": "Number of retries before the health check is considered healthy again.\n\nDefaults to 1.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"interval": {
|
||||
"description": "Interval for the health check.\n\nFormat: 1s, 5m, 11h, ...\n\nDefaults to 60s.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"method": {
|
||||
"description": "HTTP method.\n\nDefaults to GET.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "Request path.",
|
||||
"type": "string"
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Request timeout.\n\nFormat: 1s, 5m, 11h, ...",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"unhealthy_threshold": {
|
||||
"description": "Number of retries before the health check is considered unhealthy.\n\nDefaults to 1.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"HealthCheckV1": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"http"
|
||||
],
|
||||
"properties": {
|
||||
"http": {
|
||||
"$ref": "#/definitions/HealthCheckHttpV1"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"HttpHeader": {
|
||||
"description": "Definition for an HTTP header.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"value"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"HttpRequest": {
|
||||
"description": "Defines an HTTP request.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"body": {
|
||||
"description": "Request body as a string.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"expect": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/HttpRequestExpect"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"headers": {
|
||||
"description": "HTTP headers added to the request.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/HttpHeader"
|
||||
}
|
||||
},
|
||||
"method": {
|
||||
"description": "HTTP method.\n\nDefaults to GET.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "Request path.",
|
||||
"type": "string"
|
||||
},
|
||||
"timeout": {
|
||||
"description": "Request timeout.\n\nFormat: 1s, 5m, 11h, ...",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"HttpRequestExpect": {
|
||||
"description": "Validation checks for an [`HttpRequest`].",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"body_includes": {
|
||||
"description": "Text that must be present in the response body.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"body_regex": {
|
||||
"description": "Regular expression that must match against the response body.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status_codes": {
|
||||
"description": "Expected HTTP status codes.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint16",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Locality": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"regions"
|
||||
],
|
||||
"properties": {
|
||||
"regions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PackageSource": {
|
||||
"type": "string"
|
||||
},
|
||||
"Redirect": {
|
||||
"description": "App redirect configuration.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"force_https": {
|
||||
"description": "Force https by redirecting http requests to https automatically.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -38,8 +38,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
(block
|
||||
(loop
|
||||
(call $add_to_counter (i32.const 1))
|
||||
(set_local $x (i32.sub (get_local $x) (i32.const 1)))
|
||||
(br_if 1 (i32.eq (get_local $x) (i32.const 0)))
|
||||
(local.set $x (i32.sub (local.get $x) (i32.const 1)))
|
||||
(br_if 1 (i32.eq (local.get $x) (i32.const 0)))
|
||||
(br 0)))
|
||||
call $get_counter)
|
||||
(export "increment_counter_loop" (func $increment_f)))
|
||||
|
@ -40,8 +40,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
(block
|
||||
(loop
|
||||
(call $add_to_counter (i32.const 1))
|
||||
(set_local $x (i32.sub (get_local $x) (i32.const 1)))
|
||||
(br_if 1 (i32.eq (get_local $x) (i32.const 0)))
|
||||
(local.set $x (i32.sub (local.get $x) (i32.const 1)))
|
||||
(br_if 1 (i32.eq (local.get $x) (i32.const 0)))
|
||||
(br 0)))
|
||||
call $get_counter)
|
||||
(export "increment_counter_loop" (func $increment_f)))
|
||||
|
@ -34,22 +34,22 @@ derivative = { version = "^2" }
|
||||
bytes = "1"
|
||||
tracing = { version = "0.1" }
|
||||
# - Optional shared dependencies.
|
||||
wat = { version = "=1.0.71", optional = true }
|
||||
wat = { version = "=1.216.0", optional = true }
|
||||
rustc-demangle = "0.1"
|
||||
shared-buffer = { workspace = true }
|
||||
|
||||
# Dependencies and Development Dependencies for `sys`.
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
# - Mandatory dependencies for `sys`.
|
||||
wasmer-vm = { path = "../vm", version = "=4.3.2" }
|
||||
wasmer-compiler = { path = "../compiler", version = "=4.3.2" }
|
||||
wasmer-derive = { path = "../derive", version = "=4.3.2" }
|
||||
wasmer-types = { path = "../types", version = "=4.3.2" }
|
||||
wasmer-vm = { path = "../vm", version = "=4.3.7" }
|
||||
wasmer-compiler = { path = "../compiler", version = "=4.3.7" }
|
||||
wasmer-derive = { path = "../derive", version = "=4.3.7" }
|
||||
wasmer-types = { path = "../types", version = "=4.3.7" }
|
||||
target-lexicon = { version = "0.12.2", default-features = false }
|
||||
# - Optional dependencies for `sys`.
|
||||
wasmer-compiler-singlepass = { path = "../compiler-singlepass", version = "=4.3.2", optional = true }
|
||||
wasmer-compiler-cranelift = { path = "../compiler-cranelift", version = "=4.3.2", optional = true }
|
||||
wasmer-compiler-llvm = { path = "../compiler-llvm", version = "=4.3.2", optional = true }
|
||||
wasmer-compiler-singlepass = { path = "../compiler-singlepass", version = "=4.3.7", optional = true }
|
||||
wasmer-compiler-cranelift = { path = "../compiler-cranelift", version = "=4.3.7", optional = true }
|
||||
wasmer-compiler-llvm = { path = "../compiler-llvm", version = "=4.3.7", optional = true }
|
||||
|
||||
wasm-bindgen = { version = "0.2.74", optional = true }
|
||||
js-sys = { version = "0.3.51", optional = true }
|
||||
@ -58,21 +58,21 @@ wasmparser = { workspace = true, default-features = false, optional = true }
|
||||
|
||||
# - Mandatory dependencies for `sys` on Windows.
|
||||
[target.'cfg(all(not(target_arch = "wasm32"), target_os = "windows"))'.dependencies]
|
||||
winapi = "0.3"
|
||||
windows-sys = "0.59"
|
||||
# - Development Dependencies for `sys`.
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
wat = "1.0"
|
||||
tempfile = "3.6.0"
|
||||
anyhow = "1.0"
|
||||
macro-wasmer-universal-test = { version = "4.3.2", path = "./macro-wasmer-universal-test" }
|
||||
macro-wasmer-universal-test = { version = "4.3.7", path = "./macro-wasmer-universal-test" }
|
||||
|
||||
# Dependencies and Develoment Dependencies for `js`.
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# - Mandatory dependencies for `js`.
|
||||
wasmer-types = { path = "../types", version = "=4.3.2", default-features = false, features = ["std"] }
|
||||
wasmer-types = { path = "../types", version = "=4.3.7", default-features = false, features = ["std"] }
|
||||
wasm-bindgen = "0.2.74"
|
||||
js-sys = "0.3.51"
|
||||
wasmer-derive = { path = "../derive", version = "=4.3.2" }
|
||||
wasmer-derive = { path = "../derive", version = "=4.3.7" }
|
||||
# - Optional dependencies for `js`.
|
||||
wasmparser = { workspace = true, default-features = false, optional = true }
|
||||
hashbrown = { version = "0.11", optional = true }
|
||||
@ -84,7 +84,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
wat = "1.0"
|
||||
anyhow = "1.0"
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
macro-wasmer-universal-test = { version = "4.3.2", path = "./macro-wasmer-universal-test" }
|
||||
macro-wasmer-universal-test = { version = "4.3.7", path = "./macro-wasmer-universal-test" }
|
||||
|
||||
# Specific to `js`.
|
||||
#
|
||||
|
@ -20,7 +20,7 @@ fn main() -> anyhow::Result<()> {
|
||||
(module
|
||||
(type $t0 (func (param i32) (result i32)))
|
||||
(func $add_one (export "add_one") (type $t0) (param $p0 i32) (result i32)
|
||||
get_local $p0
|
||||
local.get $p0
|
||||
i32.const 1
|
||||
i32.add))
|
||||
"#;
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "macro-wasmer-universal-test"
|
||||
version = "4.3.2"
|
||||
version = "4.3.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Universal test macro for wasmer-test"
|
||||
|
@ -93,6 +93,20 @@ impl<'a> WasmSliceAccess<'a, u8> {
|
||||
let dst = self.buf.as_mut();
|
||||
dst.copy_from_slice(src);
|
||||
}
|
||||
|
||||
/// Writes to the address pointed to by this `WasmPtr` in a memory.
|
||||
///
|
||||
/// If the source buffer is smaller than the destination buffer
|
||||
/// only the correct amount of bytes will be copied
|
||||
///
|
||||
/// Returns the number of bytes copied
|
||||
#[inline]
|
||||
pub fn copy_from_slice_min(&mut self, src: &[u8]) -> usize {
|
||||
let dst = self.buf.as_mut();
|
||||
let amt = dst.len().min(src.len());
|
||||
dst[..amt].copy_from_slice(&src[..amt]);
|
||||
amt
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Drop for WasmSliceAccess<'a, T>
|
||||
|
@ -301,6 +301,6 @@ pub trait ExportableWithGenerics<'a, Args: WasmTypeList, Rets: WasmTypeList>: Si
|
||||
/// with empty `Args` and `Rets`.
|
||||
impl<'a, T: Exportable<'a> + Clone + 'static> ExportableWithGenerics<'a, (), ()> for T {
|
||||
fn get_self_from_extern_with_generics(_extern: &'a Extern) -> Result<Self, ExportError> {
|
||||
T::get_self_from_extern(_extern).map(|i| i.clone())
|
||||
T::get_self_from_extern(_extern).cloned()
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
rustdoc::broken_intra_doc_links
|
||||
)]
|
||||
#![warn(unused_import_braces)]
|
||||
#![allow(clippy::new_without_default, clippy::vtable_address_comparisons)]
|
||||
#![allow(clippy::new_without_default, ambiguous_wide_pointer_comparisons)]
|
||||
#![warn(
|
||||
clippy::float_arithmetic,
|
||||
clippy::mut_mut,
|
||||
@ -42,7 +42,7 @@
|
||||
//! (module
|
||||
//! (type $t0 (func (param i32) (result i32)))
|
||||
//! (func $add_one (export "add_one") (type $t0) (param $p0 i32) (result i32)
|
||||
//! get_local $p0
|
||||
//! local.get $p0
|
||||
//! i32.const 1
|
||||
//! i32.add))
|
||||
//! "#;
|
||||
@ -355,7 +355,7 @@
|
||||
//! (module
|
||||
//! (type $t0 (func (param i32) (result i32)))
|
||||
//! (func $add_one (export "add_one") (type $t0) (param $p0 i32) (result i32)
|
||||
//! get_local $p0
|
||||
//! local.get $p0
|
||||
//! i32.const 1
|
||||
//! i32.add))
|
||||
//! "#;
|
||||
|
@ -288,7 +288,11 @@ pub fn translate_module<'data>(data: &'data [u8]) -> WasmResult<ModuleInfoPolyfi
|
||||
let name = sectionreader.name();
|
||||
if name == "name" {
|
||||
parse_name_section(
|
||||
NameSectionReader::new(sectionreader.data(), sectionreader.data_offset()),
|
||||
NameSectionReader::new(wasmparser::BinaryReader::new(
|
||||
sectionreader.data(),
|
||||
sectionreader.data_offset(),
|
||||
wasmparser::WasmFeatures::default(),
|
||||
)),
|
||||
&mut module_info,
|
||||
)?;
|
||||
}
|
||||
@ -335,8 +339,8 @@ pub fn parse_type_section(
|
||||
let group = res.map_err(transform_err)?;
|
||||
|
||||
for ty in group.into_types() {
|
||||
match ty.composite_type {
|
||||
wasmparser::CompositeType::Func(functype) => {
|
||||
match ty.composite_type.inner {
|
||||
wasmparser::CompositeInnerType::Func(functype) => {
|
||||
let params = functype.params();
|
||||
let returns = functype.results();
|
||||
let sig_params: Vec<Type> = params
|
||||
@ -394,6 +398,7 @@ pub fn parse_import_section<'data>(
|
||||
memory64,
|
||||
initial,
|
||||
maximum,
|
||||
..
|
||||
}) => {
|
||||
if memory64 {
|
||||
unimplemented!("64bit memory not implemented yet");
|
||||
@ -422,8 +427,8 @@ pub fn parse_import_section<'data>(
|
||||
module_info.declare_table_import(
|
||||
TableType {
|
||||
ty: wpreftype_to_type(tab.element_type).unwrap(),
|
||||
minimum: tab.initial,
|
||||
maximum: tab.maximum,
|
||||
minimum: tab.initial as u32,
|
||||
maximum: tab.maximum.map(|v| v as u32),
|
||||
},
|
||||
module_name,
|
||||
field_name,
|
||||
@ -461,8 +466,8 @@ pub fn parse_table_section(
|
||||
let table = entry.map_err(transform_err)?;
|
||||
module_info.declare_table(TableType {
|
||||
ty: wpreftype_to_type(table.ty.element_type).unwrap(),
|
||||
minimum: table.ty.initial,
|
||||
maximum: table.ty.maximum,
|
||||
minimum: table.ty.initial as u32,
|
||||
maximum: table.ty.maximum.map(|v| v as u32),
|
||||
})?;
|
||||
}
|
||||
|
||||
@ -482,6 +487,7 @@ pub fn parse_memory_section(
|
||||
memory64,
|
||||
initial,
|
||||
maximum,
|
||||
..
|
||||
} = entry.map_err(transform_err)?;
|
||||
if memory64 {
|
||||
unimplemented!("64bit memory not implemented yet");
|
||||
@ -507,6 +513,7 @@ pub fn parse_global_section(
|
||||
let WPGlobalType {
|
||||
content_type,
|
||||
mutable,
|
||||
..
|
||||
} = entry.map_err(transform_err)?.ty;
|
||||
let global = GlobalType {
|
||||
ty: wptype_to_type(content_type).unwrap(),
|
||||
@ -595,6 +602,7 @@ pub fn parse_name_section<'data>(
|
||||
| wasmparser::Name::Element(_)
|
||||
| wasmparser::Name::Data(_)
|
||||
| wasmparser::Name::Tag(_)
|
||||
| wasmparser::Name::Field(_)
|
||||
| wasmparser::Name::Unknown { .. } => {}
|
||||
};
|
||||
}
|
||||
|
@ -91,6 +91,11 @@ impl Store {
|
||||
self.inner.store.engine()
|
||||
}
|
||||
|
||||
/// Returns mutable reference to [`Engine`]
|
||||
pub fn engine_mut(&mut self) -> &mut Engine {
|
||||
&mut self.inner.engine
|
||||
}
|
||||
|
||||
/// Checks whether two stores are identical. A store is considered
|
||||
/// equal to another store if both have the same engine.
|
||||
pub fn same(a: &Self, b: &Self) -> bool {
|
||||
|
9
lib/api/src/sys/externals/function.rs
vendored
9
lib/api/src/sys/externals/function.rs
vendored
@ -46,8 +46,13 @@ impl Function {
|
||||
unsafe {
|
||||
let mut store = StoreMut::from_raw(raw_store as *mut StoreInner);
|
||||
let mut args = Vec::with_capacity(func_ty.params().len());
|
||||
|
||||
for (i, ty) in func_ty.params().iter().enumerate() {
|
||||
args.push(Value::from_raw(&mut store, *ty, *values_vec.add(i)));
|
||||
args.push(Value::from_raw(
|
||||
&mut store,
|
||||
*ty,
|
||||
values_vec.add(i).read_unaligned(),
|
||||
));
|
||||
}
|
||||
let store_mut = StoreMut::from_raw(raw_store as *mut StoreInner);
|
||||
let env = FunctionEnvMut {
|
||||
@ -67,7 +72,7 @@ impl Function {
|
||||
)));
|
||||
}
|
||||
for (i, ret) in returns.iter().enumerate() {
|
||||
*values_vec.add(i) = ret.as_raw(&store);
|
||||
values_vec.add(i).write_unaligned(ret.as_raw(&store));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -69,8 +69,7 @@ mod tests {
|
||||
impl VMTinyMemory {
|
||||
pub fn new() -> Result<Self, MemoryError> {
|
||||
let sz = 18 * WASM_PAGE_SIZE;
|
||||
let mut memory = Vec::new();
|
||||
memory.resize(sz, 0);
|
||||
let memory = vec![0; sz];
|
||||
let mut ret = Self {
|
||||
mem: memory,
|
||||
memory_definition: None,
|
||||
@ -256,9 +255,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_customtunables() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn check_custom_tunables() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use crate::{imports, wat2wasm, Engine, Instance, Memory, Module, Store};
|
||||
use wasmer_compiler_cranelift::Cranelift;
|
||||
|
||||
let wasm_bytes = wat2wasm(
|
||||
br#"(module
|
||||
@ -268,7 +266,16 @@ mod tests {
|
||||
(data (;0;) (i32.const 1048576) "*\00\00\00")
|
||||
)"#,
|
||||
)?;
|
||||
let compiler = Cranelift::default();
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "singlepass")] {
|
||||
let compiler = wasmer_compiler_singlepass::Singlepass::default();
|
||||
} else if #[cfg(feature = "llvm")] {
|
||||
let compiler = wasmer_compiler_llvm::LLVM::default();
|
||||
} else {
|
||||
let compiler = wasmer_compiler_cranelift::Cranelift::default();
|
||||
}
|
||||
}
|
||||
|
||||
let tunables = TinyTunables {};
|
||||
#[allow(deprecated)]
|
||||
@ -304,6 +311,7 @@ mod tests {
|
||||
all(target_os = "macos", target_arch = "aarch64")
|
||||
))
|
||||
))]
|
||||
#[allow(clippy::print_stdout)]
|
||||
fn check_small_stack() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use crate::{imports, wat2wasm, Engine, Instance, Module, Store};
|
||||
use wasmer_compiler_singlepass::Singlepass;
|
||||
|
@ -1,15 +1,10 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::string::{String, ToString};
|
||||
|
||||
use wasmer_types::Type;
|
||||
|
||||
use crate::store::AsStoreRef;
|
||||
use crate::vm::{VMExternRef, VMFuncRef};
|
||||
|
||||
use crate::ExternRef;
|
||||
use crate::Function;
|
||||
|
||||
use crate::store::AsStoreRef;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use wasmer_types::Type;
|
||||
|
||||
pub use wasmer_types::RawValue;
|
||||
|
||||
@ -175,17 +170,21 @@ impl fmt::Debug for Value {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Value {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::I32(v) => v.to_string(),
|
||||
Self::I64(v) => v.to_string(),
|
||||
Self::F32(v) => v.to_string(),
|
||||
Self::F64(v) => v.to_string(),
|
||||
Self::ExternRef(_) => "externref".to_string(),
|
||||
Self::FuncRef(_) => "funcref".to_string(),
|
||||
Self::V128(v) => v.to_string(),
|
||||
}
|
||||
impl fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::I32(v) => v.to_string(),
|
||||
Self::I64(v) => v.to_string(),
|
||||
Self::F32(v) => v.to_string(),
|
||||
Self::F64(v) => v.to_string(),
|
||||
Self::ExternRef(_) => "externref".to_string(),
|
||||
Self::FuncRef(_) => "funcref".to_string(),
|
||||
Self::V128(v) => v.to_string(),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,6 +132,77 @@ fn table_get() -> Result<(), String> {
|
||||
#[universal_test]
|
||||
fn table_set() -> Result<(), String> {
|
||||
// Table set not yet tested
|
||||
#[cfg(feature = "sys")]
|
||||
{
|
||||
let mut store = Store::default();
|
||||
|
||||
let table_type = TableType {
|
||||
ty: Type::ExternRef,
|
||||
minimum: 1,
|
||||
maximum: None,
|
||||
};
|
||||
let extern_ref = ExternRef::new(&mut store, 0u32);
|
||||
let table = Table::new(
|
||||
&mut store,
|
||||
table_type,
|
||||
Value::ExternRef(Some(extern_ref.clone())),
|
||||
)
|
||||
.map_err(|e| format!("{e:?}"))?;
|
||||
assert_eq!(table.ty(&store), table_type);
|
||||
|
||||
let v = table.get(&mut store, 0);
|
||||
assert!(v.is_some());
|
||||
|
||||
let v = v.unwrap();
|
||||
|
||||
let v = if let Value::ExternRef(Some(ext)) = v {
|
||||
ext.downcast::<u32>(&mut store)
|
||||
} else {
|
||||
return Err("table.get does not match `ExternRef(Some(..))`!".into());
|
||||
};
|
||||
|
||||
let v = v.unwrap();
|
||||
assert_eq!(*v, 0u32);
|
||||
|
||||
let extern_ref = ExternRef::new(&mut store, 1u32);
|
||||
table
|
||||
.set(&mut store, 0, Value::ExternRef(Some(extern_ref)))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let v = table.get(&mut store, 0);
|
||||
assert!(v.is_some());
|
||||
let v = v.unwrap();
|
||||
|
||||
let v = if let Value::ExternRef(Some(ext)) = v {
|
||||
ext.downcast::<u32>(&mut store)
|
||||
} else {
|
||||
return Err("table.get does not match `ExternRef(Some(..))`!".into());
|
||||
};
|
||||
|
||||
assert!(v.is_some());
|
||||
let v = v.unwrap();
|
||||
assert_eq!(*v, 1u32);
|
||||
|
||||
let extern_ref = ExternRef::new(&mut store, 2u32);
|
||||
table
|
||||
.set(&mut store, 0, Value::ExternRef(Some(extern_ref)))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let v = table.get(&mut store, 0);
|
||||
assert!(v.is_some());
|
||||
let v = v.unwrap();
|
||||
|
||||
let v = if let Value::ExternRef(Some(ext)) = v {
|
||||
ext.downcast::<u32>(&mut store)
|
||||
} else {
|
||||
return Err("table.get does not match `ExternRef(Some(..))`!".into());
|
||||
};
|
||||
|
||||
assert!(v.is_some());
|
||||
let v = v.unwrap();
|
||||
assert_eq!(*v, 2u32);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -149,15 +220,20 @@ fn table_grow() -> Result<(), String> {
|
||||
let f = Function::new_typed(&mut store, |num: i32| num + 1);
|
||||
let table = Table::new(&mut store, table_type, Value::FuncRef(Some(f.clone())))
|
||||
.map_err(|e| format!("{e:?}"))?;
|
||||
|
||||
let old_len = table.grow(&mut store, 1, Value::FuncRef(Some(f.clone())));
|
||||
assert_eq!(0, old_len.unwrap());
|
||||
let old_len = table.grow(&mut store, 1, Value::FuncRef(Some(f.clone())));
|
||||
assert_eq!(1, old_len.unwrap());
|
||||
|
||||
// Growing to a bigger maximum should return None
|
||||
let old_len = table.grow(&mut store, 12, Value::FuncRef(Some(f.clone())));
|
||||
assert!(old_len.is_err());
|
||||
|
||||
// Growing to a bigger maximum should return None
|
||||
let old_len = table
|
||||
.grow(&mut store, 5, Value::FuncRef(Some(f)))
|
||||
.map_err(|e| format!("{e:?}"))?;
|
||||
assert_eq!(old_len, 0);
|
||||
assert_eq!(old_len, 2);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -33,7 +33,7 @@ fn imports() -> Result<(), String> {
|
||||
let wat = r#"(module
|
||||
(import "host" "func" (func))
|
||||
(import "host" "memory" (memory 1))
|
||||
(import "host" "table" (table 1 anyfunc))
|
||||
(import "host" "table" (table 1 funcref))
|
||||
(import "host" "global" (global i32))
|
||||
)"#;
|
||||
let module = Module::new(&store, wat).map_err(|e| format!("{e:?}"))?;
|
||||
|
@ -487,4 +487,170 @@ pub mod reference_types {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[universal_test]
|
||||
fn extern_ref_ref_counting_table_instructions_in_module() -> Result<()> {
|
||||
let mut store = Store::default();
|
||||
let wat = r#"(module
|
||||
(table $table1 (export "table1") 2 12 externref)
|
||||
(table $table2 (export "table2") 6 12 externref)
|
||||
(func $grow_table_with_ref (export "grow_table_with_ref") (param $er externref) (param $size i32) (result i32)
|
||||
(table.grow $table1 (local.get $er) (local.get $size)))
|
||||
(func $fill_table_with_ref (export "fill_table_with_ref") (param $er externref) (param $start i32) (param $end i32)
|
||||
(table.fill $table1 (local.get $start) (local.get $er) (local.get $end)))
|
||||
(func $copy_into_table2 (export "copy_into_table2")
|
||||
(table.copy $table2 $table1 (i32.const 0) (i32.const 0) (i32.const 4)))
|
||||
(func (export "call_set_value") (param $er externref) (param $idx i32)
|
||||
(table.set $table2 (local.get $idx) (local.get $er)))
|
||||
)"#;
|
||||
let module = Module::new(&store, wat)?;
|
||||
let instance = Instance::new(&mut store, &module, &imports! {})?;
|
||||
|
||||
let grow_table_with_ref: TypedFunction<(Option<ExternRef>, i32), i32> = instance
|
||||
.exports
|
||||
.get_typed_function(&store, "grow_table_with_ref")?;
|
||||
let fill_table_with_ref: TypedFunction<(Option<ExternRef>, i32, i32), ()> = instance
|
||||
.exports
|
||||
.get_typed_function(&store, "fill_table_with_ref")?;
|
||||
let copy_into_table2: TypedFunction<(), ()> = instance
|
||||
.exports
|
||||
.get_typed_function(&store, "copy_into_table2")?;
|
||||
let call_set_value: TypedFunction<(Option<ExternRef>, i32), ()> = instance
|
||||
.exports
|
||||
.get_typed_function(&store, "call_set_value")?;
|
||||
let table1: &Table = instance.exports.get_table("table1")?;
|
||||
let table2: &Table = instance.exports.get_table("table2")?;
|
||||
|
||||
let er1 = ExternRef::new(&mut store, 3usize);
|
||||
let er2 = ExternRef::new(&mut store, 5usize);
|
||||
let er3 = ExternRef::new(&mut store, 7usize);
|
||||
{
|
||||
let result = grow_table_with_ref.call(&mut store, Some(er1.clone()), 0)?;
|
||||
assert_eq!(result, 2);
|
||||
|
||||
let result = grow_table_with_ref.call(&mut store, Some(er1.clone()), 10_000)?;
|
||||
assert_eq!(result, -1);
|
||||
|
||||
let result = grow_table_with_ref.call(&mut store, Some(er1), 8)?;
|
||||
assert_eq!(result, 2);
|
||||
|
||||
for i in 2..10 {
|
||||
let v = table1.get(&mut store, i);
|
||||
let e = v.as_ref().unwrap().unwrap_externref();
|
||||
let e_val: Option<&usize> = e.as_ref().unwrap().downcast(&store);
|
||||
assert_eq!(*e_val.unwrap(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
fill_table_with_ref.call(&mut store, Some(er2), 0, 2)?;
|
||||
}
|
||||
|
||||
{
|
||||
call_set_value.call(&mut store, Some(er3.clone()), 0)?;
|
||||
call_set_value.call(&mut store, Some(er3.clone()), 1)?;
|
||||
call_set_value.call(&mut store, Some(er3.clone()), 2)?;
|
||||
call_set_value.call(&mut store, Some(er3.clone()), 3)?;
|
||||
call_set_value.call(&mut store, Some(er3), 4)?;
|
||||
}
|
||||
|
||||
{
|
||||
copy_into_table2.call(&mut store)?;
|
||||
for i in 1..5 {
|
||||
let v = table2.get(&mut store, i);
|
||||
let e = v.as_ref().unwrap().unwrap_externref();
|
||||
let value: &usize = e.as_ref().unwrap().downcast(&store).unwrap();
|
||||
match i {
|
||||
0 | 1 => assert_eq!(*value, 5),
|
||||
4 => assert_eq!(*value, 7),
|
||||
_ => assert_eq!(*value, 3),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
for i in 0..table1.size(&store) {
|
||||
table1.set(&mut store, i, Value::ExternRef(None))?;
|
||||
}
|
||||
for i in 0..table2.size(&store) {
|
||||
table2.set(&mut store, i, Value::ExternRef(None))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[universal_test]
|
||||
fn extern_ref_table_host_guest() -> Result<()> {
|
||||
let mut store = Store::default();
|
||||
let wat = r#"(module
|
||||
(table $table (export "table") 1 12 externref)
|
||||
|
||||
(func (export "call_set_value") (param $er externref) (param $idx i32)
|
||||
(table.set $table (local.get $idx) (local.get $er)))
|
||||
|
||||
(func (export "call_get_value") (param $idx i32) (result externref)
|
||||
(table.get $table (local.get $idx)))
|
||||
|
||||
)"#;
|
||||
let module = Module::new(&store, wat)?;
|
||||
let instance = Instance::new(&mut store, &module, &imports! {})?;
|
||||
let call_set_value: TypedFunction<(Option<ExternRef>, i32), ()> = instance
|
||||
.exports
|
||||
.get_typed_function(&store, "call_set_value")?;
|
||||
|
||||
let call_get_value: TypedFunction<i32, Option<ExternRef>> = instance
|
||||
.exports
|
||||
.get_typed_function(&store, "call_get_value")?;
|
||||
|
||||
let table: &Table = instance.exports.get_table("table")?;
|
||||
|
||||
let er_even = ExternRef::new(&mut store, 4usize);
|
||||
let er_odd = ExternRef::new(&mut store, 9usize);
|
||||
|
||||
for i in 0..table.size(&store) {
|
||||
if i % 2 == 0 {
|
||||
call_set_value.call(&mut store, Some(er_even.clone()), i as i32)?;
|
||||
} else {
|
||||
call_set_value.call(&mut store, Some(er_odd.clone()), i as i32)?;
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..table.size(&store) {
|
||||
let v = table.get(&mut store, i);
|
||||
let e = v.as_ref().unwrap().unwrap_externref();
|
||||
let value: &usize = e.as_ref().unwrap().downcast(&store).unwrap();
|
||||
|
||||
if i % 2 == 0 {
|
||||
assert_eq!(*value, 4)
|
||||
} else {
|
||||
assert_eq!(*value, 9)
|
||||
}
|
||||
}
|
||||
|
||||
let er_even = ExternRef::new(&mut store, 6usize);
|
||||
let er_odd = ExternRef::new(&mut store, 11usize);
|
||||
|
||||
for i in 0..table.size(&store) {
|
||||
if i % 2 == 0 {
|
||||
table.set(&mut store, i, Value::ExternRef(Some(er_even.clone())))?;
|
||||
} else {
|
||||
table.set(&mut store, i, Value::ExternRef(Some(er_odd.clone())))?;
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..table.size(&store) {
|
||||
let v = call_get_value.call(&mut store, i as i32)?;
|
||||
let e = v.as_ref().unwrap();
|
||||
let value: &usize = e.downcast(&store).unwrap();
|
||||
|
||||
if i % 2 == 0 {
|
||||
assert_eq!(*value, 6)
|
||||
} else {
|
||||
assert_eq!(*value, 11)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "wasmer-api"
|
||||
version = "0.0.30"
|
||||
version = "0.0.34"
|
||||
description = "Client library for the Wasmer GraphQL API"
|
||||
readme = "README.md"
|
||||
documentation = "https://docs.rs/wasmer-api"
|
||||
@ -13,11 +13,10 @@ repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# Wasmer dependencies.
|
||||
edge-schema.workspace = true
|
||||
wasmer-config = { version = "0.4.0", path = "../config" }
|
||||
wasmer-config = { version = "0.8.0", path = "../config" }
|
||||
webc.workspace = true
|
||||
|
||||
# crates.io dependencies.
|
||||
@ -26,14 +25,19 @@ serde = { version = "1", features = ["derive"] }
|
||||
time = { version = "0.3", features = ["formatting", "parsing"] }
|
||||
tokio = { version = "1.23.0" }
|
||||
serde_json = "1"
|
||||
url = "2"
|
||||
url = { version = "2", features = ["serde"] }
|
||||
futures = "0.3"
|
||||
tracing = "0.1"
|
||||
cynic = { version = "=3.4.3", features = ["http-reqwest"] }
|
||||
cynic = { version = "3.7.2", features = ["http-reqwest"] }
|
||||
pin-project-lite = "0.2.10"
|
||||
serde_path_to_error = "0.1.14"
|
||||
harsh = "0.2.2"
|
||||
reqwest = { version = "0.11.13", default-features = false, features = ["json"] }
|
||||
reqwest = { workspace = true, default-features = false, features = ["json"] }
|
||||
merge-streams = "0.1.2"
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dependencies.getrandom]
|
||||
version = "0.2.14"
|
||||
features = ["js"]
|
||||
|
||||
[dev-dependencies]
|
||||
base64 = "0.13.1"
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,11 @@
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::GraphQLApiFailure;
|
||||
use anyhow::{bail, Context as _};
|
||||
use cynic::{http::CynicReqwestError, GraphQlResponse, Operation};
|
||||
use url::Url;
|
||||
|
||||
use crate::GraphQLApiFailure;
|
||||
|
||||
/// API client for the Wasmer API.
|
||||
///
|
||||
/// Use the queries in [`crate::queries`] to interact with the API.
|
||||
@ -17,10 +17,16 @@ pub struct WasmerClient {
|
||||
pub(crate) client: reqwest::Client,
|
||||
pub(crate) user_agent: reqwest::header::HeaderValue,
|
||||
#[allow(unused)]
|
||||
extra_debugging: bool,
|
||||
log_variables: bool,
|
||||
}
|
||||
|
||||
impl WasmerClient {
|
||||
/// Env var used to enable logging of request variables.
|
||||
///
|
||||
/// This is somewhat dangerous since it can log sensitive information, hence
|
||||
/// it is gated by a custom env var.
|
||||
const ENV_VAR_LOG_VARIABLES: &'static str = "WASMER_API_INSECURE_LOG_VARIABLES";
|
||||
|
||||
pub fn graphql_endpoint(&self) -> &Url {
|
||||
&self.graphql_endpoint
|
||||
}
|
||||
@ -43,21 +49,67 @@ impl WasmerClient {
|
||||
graphql_endpoint: Url,
|
||||
user_agent: &str,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let log_variables = {
|
||||
let v = std::env::var(Self::ENV_VAR_LOG_VARIABLES).unwrap_or_default();
|
||||
match v.as_str() {
|
||||
"1" | "true" => true,
|
||||
"0" | "false" => false,
|
||||
// Default case if not provided.
|
||||
"" => false,
|
||||
other => {
|
||||
bail!(
|
||||
"invalid value for {} - expected 0/false|1/true: '{}'",
|
||||
Self::ENV_VAR_LOG_VARIABLES,
|
||||
other
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
auth_token: None,
|
||||
user_agent: Self::parse_user_agent(user_agent)?,
|
||||
graphql_endpoint,
|
||||
extra_debugging: false,
|
||||
log_variables,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_with_proxy(
|
||||
graphql_endpoint: Url,
|
||||
user_agent: &str,
|
||||
proxy: reqwest::Proxy,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let builder = {
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
let mut builder = reqwest::ClientBuilder::new();
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
|
||||
let builder = reqwest::ClientBuilder::new()
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.timeout(Duration::from_secs(90));
|
||||
|
||||
builder.proxy(proxy)
|
||||
};
|
||||
|
||||
let client = builder.build().context("failed to create reqwest client")?;
|
||||
|
||||
Self::new_with_client(client, graphql_endpoint, user_agent)
|
||||
}
|
||||
|
||||
pub fn new(graphql_endpoint: Url, user_agent: &str) -> Result<Self, anyhow::Error> {
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
let client = reqwest::Client::builder()
|
||||
.build()
|
||||
.context("could not construct http client")?;
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.timeout(Duration::from_secs(90))
|
||||
.build()
|
||||
.context("could not construct http client")?;
|
||||
|
||||
Self::new_with_client(client, graphql_endpoint, user_agent)
|
||||
}
|
||||
|
||||
@ -84,20 +136,22 @@ impl WasmerClient {
|
||||
req
|
||||
};
|
||||
|
||||
if self.extra_debugging {
|
||||
tracing::trace!(
|
||||
query=%operation.query,
|
||||
vars=?operation.variables,
|
||||
"running GraphQL query"
|
||||
);
|
||||
}
|
||||
let query = operation.query.clone();
|
||||
|
||||
tracing::trace!(
|
||||
endpoint=%self.graphql_endpoint,
|
||||
query=serde_json::to_string(&operation).unwrap_or_default(),
|
||||
"sending graphql query"
|
||||
);
|
||||
if self.log_variables {
|
||||
tracing::trace!(
|
||||
endpoint=%self.graphql_endpoint,
|
||||
query=serde_json::to_string(&operation).unwrap_or_default(),
|
||||
vars=?operation.variables,
|
||||
"sending graphql query"
|
||||
);
|
||||
} else {
|
||||
tracing::trace!(
|
||||
endpoint=%self.graphql_endpoint,
|
||||
query=serde_json::to_string(&operation).unwrap_or_default(),
|
||||
"sending graphql query"
|
||||
);
|
||||
}
|
||||
|
||||
let res = req.json(&operation).send().await;
|
||||
|
||||
|
@ -1,26 +1,372 @@
|
||||
use std::{collections::HashSet, pin::Pin, time::Duration};
|
||||
use std::{collections::HashSet, time::Duration};
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use cynic::{MutationBuilder, QueryBuilder};
|
||||
use edge_schema::schema::NetworkTokenV1;
|
||||
use futures::{Stream, StreamExt};
|
||||
use futures::StreamExt;
|
||||
use merge_streams::MergeStreams;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::Instrument;
|
||||
use url::Url;
|
||||
use wasmer_config::package::PackageIdent;
|
||||
|
||||
use crate::{
|
||||
types::{
|
||||
self, CreateNamespaceVars, DeployApp, DeployAppConnection, DeployAppVersion,
|
||||
DeployAppVersionConnection, DnsDomain, GetAppTemplateFromSlugVariables,
|
||||
GetAppTemplatesVars, GetCurrentUserWithAppsVars, GetDeployAppAndVersion,
|
||||
GetDeployAppVersionsVars, GetNamespaceAppsVars, GetSignedUrlForPackageUploadVariables, Log,
|
||||
LogStream, PackageVersionConnection, PublishDeployAppVars, PushPackageReleasePayload,
|
||||
SignedUrl, TagPackageReleasePayload, UpsertDomainFromZoneFileVars,
|
||||
},
|
||||
types::{self, *},
|
||||
GraphQLApiFailure, WasmerClient,
|
||||
};
|
||||
|
||||
/// Rotate the s3 secrets tied to an app given its id.
|
||||
pub async fn rotate_s3_secrets(
|
||||
client: &WasmerClient,
|
||||
app_id: types::Id,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::RotateS3SecretsForApp::build(
|
||||
RotateS3SecretsForAppVariables { id: app_id },
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn viewer_can_deploy_to_namespace(
|
||||
client: &WasmerClient,
|
||||
owner_name: &str,
|
||||
) -> Result<bool, anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::ViewerCan::build(ViewerCanVariables {
|
||||
action: OwnerAction::DeployApp,
|
||||
owner_name,
|
||||
}))
|
||||
.await
|
||||
.map(|v| v.viewer_can)
|
||||
}
|
||||
|
||||
pub async fn redeploy_app_by_id(
|
||||
client: &WasmerClient,
|
||||
app_id: impl Into<String>,
|
||||
) -> Result<Option<DeployApp>, anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::RedeployActiveApp::build(
|
||||
RedeployActiveAppVariables {
|
||||
id: types::Id::from(app_id),
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map(|v| v.redeploy_active_version.map(|v| v.app))
|
||||
}
|
||||
|
||||
/// Revoke an existing token
|
||||
pub async fn revoke_token(
|
||||
client: &WasmerClient,
|
||||
token: String,
|
||||
) -> Result<Option<bool>, anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::RevokeToken::build(RevokeTokenVariables { token }))
|
||||
.await
|
||||
.map(|v| v.revoke_api_token.and_then(|v| v.success))
|
||||
}
|
||||
|
||||
/// Generate a new Nonce
|
||||
///
|
||||
/// Takes a name and a callbackUrl and returns a nonce
|
||||
pub async fn create_nonce(
|
||||
client: &WasmerClient,
|
||||
name: String,
|
||||
callback_url: String,
|
||||
) -> Result<Option<Nonce>, anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::CreateNewNonce::build(CreateNewNonceVariables {
|
||||
callback_url,
|
||||
name,
|
||||
}))
|
||||
.await
|
||||
.map(|v| v.new_nonce.map(|v| v.nonce))
|
||||
}
|
||||
|
||||
pub async fn get_app_secret_value_by_id(
|
||||
client: &WasmerClient,
|
||||
secret_id: impl Into<String>,
|
||||
) -> Result<Option<String>, anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::GetAppSecretValue::build(
|
||||
GetAppSecretValueVariables {
|
||||
id: types::Id::from(secret_id),
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map(|v| v.get_secret_value)
|
||||
}
|
||||
|
||||
pub async fn get_app_secret_by_name(
|
||||
client: &WasmerClient,
|
||||
app_id: impl Into<String>,
|
||||
name: impl Into<String>,
|
||||
) -> Result<Option<Secret>, anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::GetAppSecret::build(GetAppSecretVariables {
|
||||
app_id: types::Id::from(app_id),
|
||||
secret_name: name.into(),
|
||||
}))
|
||||
.await
|
||||
.map(|v| v.get_app_secret)
|
||||
}
|
||||
|
||||
/// Update or create an app secret.
|
||||
pub async fn upsert_app_secret(
|
||||
client: &WasmerClient,
|
||||
app_id: impl Into<String>,
|
||||
name: impl Into<String>,
|
||||
value: impl Into<String>,
|
||||
) -> Result<Option<UpsertAppSecretPayload>, anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::UpsertAppSecret::build(UpsertAppSecretVariables {
|
||||
app_id: cynic::Id::from(app_id.into()),
|
||||
name: name.into().as_str(),
|
||||
value: value.into().as_str(),
|
||||
}))
|
||||
.await
|
||||
.map(|v| v.upsert_app_secret)
|
||||
}
|
||||
|
||||
/// Update or create app secrets in bulk.
|
||||
pub async fn upsert_app_secrets(
|
||||
client: &WasmerClient,
|
||||
app_id: impl Into<String>,
|
||||
secrets: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
|
||||
) -> Result<Option<UpsertAppSecretsPayload>, anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::UpsertAppSecrets::build(UpsertAppSecretsVariables {
|
||||
app_id: cynic::Id::from(app_id.into()),
|
||||
secrets: Some(
|
||||
secrets
|
||||
.into_iter()
|
||||
.map(|(name, value)| SecretInput {
|
||||
name: name.into(),
|
||||
value: value.into(),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
}))
|
||||
.await
|
||||
.map(|v| v.upsert_app_secrets)
|
||||
}
|
||||
|
||||
/// Load all secrets of an app.
|
||||
///
|
||||
/// Will paginate through all versions and return them in a single list.
|
||||
pub async fn get_all_app_secrets_filtered(
|
||||
client: &WasmerClient,
|
||||
app_id: impl Into<String>,
|
||||
names: impl IntoIterator<Item = impl Into<String>>,
|
||||
) -> Result<Vec<Secret>, anyhow::Error> {
|
||||
let mut vars = GetAllAppSecretsVariables {
|
||||
after: None,
|
||||
app_id: types::Id::from(app_id),
|
||||
before: None,
|
||||
first: None,
|
||||
last: None,
|
||||
offset: None,
|
||||
names: Some(names.into_iter().map(|s| s.into()).collect()),
|
||||
};
|
||||
|
||||
let mut all_secrets = Vec::<Secret>::new();
|
||||
|
||||
loop {
|
||||
let page = get_app_secrets(client, vars.clone()).await?;
|
||||
if page.edges.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for edge in page.edges {
|
||||
let edge = match edge {
|
||||
Some(edge) => edge,
|
||||
None => continue,
|
||||
};
|
||||
let version = match edge.node {
|
||||
Some(item) => item,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
all_secrets.push(version);
|
||||
|
||||
// Update pagination.
|
||||
vars.after = Some(edge.cursor);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(all_secrets)
|
||||
}
|
||||
|
||||
/// Retrieve volumes for an app.
|
||||
pub async fn get_app_volumes(
|
||||
client: &WasmerClient,
|
||||
owner: impl Into<String>,
|
||||
name: impl Into<String>,
|
||||
) -> Result<Vec<types::AppVersionVolume>, anyhow::Error> {
|
||||
let vars = types::GetAppVolumesVars {
|
||||
owner: owner.into(),
|
||||
name: name.into(),
|
||||
};
|
||||
let res = client
|
||||
.run_graphql_strict(types::GetAppVolumes::build(vars))
|
||||
.await?;
|
||||
let volumes = res
|
||||
.get_deploy_app
|
||||
.context("app not found")?
|
||||
.active_version
|
||||
.volumes
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
Ok(volumes)
|
||||
}
|
||||
|
||||
/// Load the S3 credentials.
|
||||
///
|
||||
/// S3 can be used to get access to an apps volumes.
|
||||
pub async fn get_app_s3_credentials(
|
||||
client: &WasmerClient,
|
||||
app_id: impl Into<String>,
|
||||
) -> Result<types::S3Credentials, anyhow::Error> {
|
||||
let app_id = app_id.into();
|
||||
|
||||
// Firt load the app to get the s3 url.
|
||||
let app1 = get_app_by_id(client, app_id.clone()).await?;
|
||||
|
||||
let vars = types::GetDeployAppVars {
|
||||
owner: app1.owner.global_name,
|
||||
name: app1.name,
|
||||
};
|
||||
client
|
||||
.run_graphql_strict(types::GetDeployAppS3Credentials::build(vars))
|
||||
.await?
|
||||
.get_deploy_app
|
||||
.context("app not found")?
|
||||
.s3_credentials
|
||||
.context("app does not have S3 credentials")
|
||||
}
|
||||
|
||||
/// Load all available regions.
|
||||
///
|
||||
/// Will paginate through all versions and return them in a single list.
|
||||
pub async fn get_all_app_regions(client: &WasmerClient) -> Result<Vec<AppRegion>, anyhow::Error> {
|
||||
let mut vars = GetAllAppRegionsVariables {
|
||||
after: None,
|
||||
before: None,
|
||||
first: None,
|
||||
last: None,
|
||||
offset: None,
|
||||
};
|
||||
|
||||
let mut all_regions = Vec::<AppRegion>::new();
|
||||
|
||||
loop {
|
||||
let page = get_regions(client, vars.clone()).await?;
|
||||
if page.edges.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for edge in page.edges {
|
||||
let edge = match edge {
|
||||
Some(edge) => edge,
|
||||
None => continue,
|
||||
};
|
||||
let version = match edge.node {
|
||||
Some(item) => item,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
all_regions.push(version);
|
||||
|
||||
// Update pagination.
|
||||
vars.after = Some(edge.cursor);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(all_regions)
|
||||
}
|
||||
|
||||
/// Retrieve regions.
|
||||
pub async fn get_regions(
|
||||
client: &WasmerClient,
|
||||
vars: GetAllAppRegionsVariables,
|
||||
) -> Result<AppRegionConnection, anyhow::Error> {
|
||||
let res = client
|
||||
.run_graphql_strict(types::GetAllAppRegions::build(vars))
|
||||
.await?;
|
||||
Ok(res.get_app_regions)
|
||||
}
|
||||
|
||||
/// Load all secrets of an app.
|
||||
///
|
||||
/// Will paginate through all versions and return them in a single list.
|
||||
pub async fn get_all_app_secrets(
|
||||
client: &WasmerClient,
|
||||
app_id: impl Into<String>,
|
||||
) -> Result<Vec<Secret>, anyhow::Error> {
|
||||
let mut vars = GetAllAppSecretsVariables {
|
||||
after: None,
|
||||
app_id: types::Id::from(app_id),
|
||||
before: None,
|
||||
first: None,
|
||||
last: None,
|
||||
offset: None,
|
||||
names: None,
|
||||
};
|
||||
|
||||
let mut all_secrets = Vec::<Secret>::new();
|
||||
|
||||
loop {
|
||||
let page = get_app_secrets(client, vars.clone()).await?;
|
||||
if page.edges.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for edge in page.edges {
|
||||
let edge = match edge {
|
||||
Some(edge) => edge,
|
||||
None => continue,
|
||||
};
|
||||
let version = match edge.node {
|
||||
Some(item) => item,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
all_secrets.push(version);
|
||||
|
||||
// Update pagination.
|
||||
vars.after = Some(edge.cursor);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(all_secrets)
|
||||
}
|
||||
|
||||
/// Retrieve secrets for an app.
|
||||
pub async fn get_app_secrets(
|
||||
client: &WasmerClient,
|
||||
vars: GetAllAppSecretsVariables,
|
||||
) -> Result<SecretConnection, anyhow::Error> {
|
||||
let res = client
|
||||
.run_graphql_strict(types::GetAllAppSecrets::build(vars))
|
||||
.await?;
|
||||
res.get_app_secrets.context("app not found")
|
||||
}
|
||||
|
||||
pub async fn delete_app_secret(
|
||||
client: &WasmerClient,
|
||||
secret_id: impl Into<String>,
|
||||
) -> Result<Option<DeleteAppSecretPayload>, anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::DeleteAppSecret::build(DeleteAppSecretVariables {
|
||||
id: types::Id::from(secret_id.into()),
|
||||
}))
|
||||
.await
|
||||
.map(|v| v.delete_app_secret)
|
||||
}
|
||||
|
||||
/// Load a webc package from the registry.
|
||||
///
|
||||
/// NOTE: this uses the public URL instead of the download URL available through
|
||||
@ -69,6 +415,27 @@ pub async fn fetch_app_template_from_slug(
|
||||
.map(|v| v.get_app_template)
|
||||
}
|
||||
|
||||
/// Fetch app templates.
|
||||
pub async fn fetch_app_templates_from_framework(
|
||||
client: &WasmerClient,
|
||||
framework_slug: String,
|
||||
first: i32,
|
||||
after: Option<String>,
|
||||
sort_by: Option<types::AppTemplatesSortBy>,
|
||||
) -> Result<Option<types::AppTemplateConnection>, anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::GetAppTemplatesFromFramework::build(
|
||||
GetAppTemplatesFromFrameworkVars {
|
||||
framework_slug,
|
||||
first,
|
||||
after,
|
||||
sort_by,
|
||||
},
|
||||
))
|
||||
.await
|
||||
.map(|r| r.get_app_templates)
|
||||
}
|
||||
|
||||
/// Fetch app templates.
|
||||
pub async fn fetch_app_templates(
|
||||
client: &WasmerClient,
|
||||
@ -145,6 +512,270 @@ pub fn fetch_all_app_templates(
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch all app templates by paginating through the responses.
|
||||
///
|
||||
/// Will fetch at most `max` templates.
|
||||
pub fn fetch_all_app_templates_from_language(
|
||||
client: &WasmerClient,
|
||||
page_size: i32,
|
||||
sort_by: Option<types::AppTemplatesSortBy>,
|
||||
language: String,
|
||||
) -> impl futures::Stream<Item = Result<Vec<types::AppTemplate>, anyhow::Error>> + '_ {
|
||||
let vars = GetAppTemplatesFromLanguageVars {
|
||||
language_slug: language.clone().to_string(),
|
||||
first: page_size,
|
||||
sort_by,
|
||||
after: None,
|
||||
};
|
||||
|
||||
futures::stream::try_unfold(
|
||||
Some(vars),
|
||||
move |vars: Option<types::GetAppTemplatesFromLanguageVars>| async move {
|
||||
let vars = match vars {
|
||||
Some(vars) => vars,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let con = client
|
||||
.run_graphql_strict(types::GetAppTemplatesFromLanguage::build(vars.clone()))
|
||||
.await?
|
||||
.get_app_templates
|
||||
.context("backend did not return any data")?;
|
||||
|
||||
let items = con
|
||||
.edges
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|edge| edge.node)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let next_cursor = con
|
||||
.page_info
|
||||
.end_cursor
|
||||
.filter(|_| con.page_info.has_next_page);
|
||||
|
||||
let next_vars = next_cursor.map(|after| types::GetAppTemplatesFromLanguageVars {
|
||||
after: Some(after),
|
||||
..vars
|
||||
});
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
let res: Result<
|
||||
Option<(
|
||||
Vec<types::AppTemplate>,
|
||||
Option<types::GetAppTemplatesFromLanguageVars>,
|
||||
)>,
|
||||
anyhow::Error,
|
||||
> = Ok(Some((items, next_vars)));
|
||||
|
||||
res
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch languages from available app templates.
|
||||
pub async fn fetch_app_template_languages(
|
||||
client: &WasmerClient,
|
||||
after: Option<String>,
|
||||
first: Option<i32>,
|
||||
) -> Result<Option<types::TemplateLanguageConnection>, anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::GetTemplateLanguages::build(
|
||||
GetTemplateLanguagesVars { after, first },
|
||||
))
|
||||
.await
|
||||
.map(|r| r.get_template_languages)
|
||||
}
|
||||
|
||||
/// Fetch all languages from available app templates by paginating through the responses.
|
||||
///
|
||||
/// Will fetch at most `max` templates.
|
||||
pub fn fetch_all_app_template_languages(
|
||||
client: &WasmerClient,
|
||||
page_size: Option<i32>,
|
||||
) -> impl futures::Stream<Item = Result<Vec<types::TemplateLanguage>, anyhow::Error>> + '_ {
|
||||
let vars = GetTemplateLanguagesVars {
|
||||
after: None,
|
||||
first: page_size,
|
||||
};
|
||||
|
||||
futures::stream::try_unfold(
|
||||
Some(vars),
|
||||
move |vars: Option<types::GetTemplateLanguagesVars>| async move {
|
||||
let vars = match vars {
|
||||
Some(vars) => vars,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let con = client
|
||||
.run_graphql_strict(types::GetTemplateLanguages::build(vars.clone()))
|
||||
.await?
|
||||
.get_template_languages
|
||||
.context("backend did not return any data")?;
|
||||
|
||||
let items = con
|
||||
.edges
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|edge| edge.node)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let next_cursor = con
|
||||
.page_info
|
||||
.end_cursor
|
||||
.filter(|_| con.page_info.has_next_page);
|
||||
|
||||
let next_vars = next_cursor.map(|after| types::GetTemplateLanguagesVars {
|
||||
after: Some(after),
|
||||
..vars
|
||||
});
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
let res: Result<
|
||||
Option<(
|
||||
Vec<types::TemplateLanguage>,
|
||||
Option<types::GetTemplateLanguagesVars>,
|
||||
)>,
|
||||
anyhow::Error,
|
||||
> = Ok(Some((items, next_vars)));
|
||||
|
||||
res
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch all app templates by paginating through the responses.
|
||||
///
|
||||
/// Will fetch at most `max` templates.
|
||||
pub fn fetch_all_app_templates_from_framework(
|
||||
client: &WasmerClient,
|
||||
page_size: i32,
|
||||
sort_by: Option<types::AppTemplatesSortBy>,
|
||||
framework: String,
|
||||
) -> impl futures::Stream<Item = Result<Vec<types::AppTemplate>, anyhow::Error>> + '_ {
|
||||
let vars = GetAppTemplatesFromFrameworkVars {
|
||||
framework_slug: framework.clone().to_string(),
|
||||
first: page_size,
|
||||
sort_by,
|
||||
after: None,
|
||||
};
|
||||
|
||||
futures::stream::try_unfold(
|
||||
Some(vars),
|
||||
move |vars: Option<types::GetAppTemplatesFromFrameworkVars>| async move {
|
||||
let vars = match vars {
|
||||
Some(vars) => vars,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let con = client
|
||||
.run_graphql_strict(types::GetAppTemplatesFromFramework::build(vars.clone()))
|
||||
.await?
|
||||
.get_app_templates
|
||||
.context("backend did not return any data")?;
|
||||
|
||||
let items = con
|
||||
.edges
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|edge| edge.node)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let next_cursor = con
|
||||
.page_info
|
||||
.end_cursor
|
||||
.filter(|_| con.page_info.has_next_page);
|
||||
|
||||
let next_vars = next_cursor.map(|after| types::GetAppTemplatesFromFrameworkVars {
|
||||
after: Some(after),
|
||||
..vars
|
||||
});
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
let res: Result<
|
||||
Option<(
|
||||
Vec<types::AppTemplate>,
|
||||
Option<types::GetAppTemplatesFromFrameworkVars>,
|
||||
)>,
|
||||
anyhow::Error,
|
||||
> = Ok(Some((items, next_vars)));
|
||||
|
||||
res
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch frameworks from available app templates.
|
||||
pub async fn fetch_app_template_frameworks(
|
||||
client: &WasmerClient,
|
||||
after: Option<String>,
|
||||
first: Option<i32>,
|
||||
) -> Result<Option<types::TemplateFrameworkConnection>, anyhow::Error> {
|
||||
client
|
||||
.run_graphql_strict(types::GetTemplateFrameworks::build(
|
||||
GetTemplateFrameworksVars { after, first },
|
||||
))
|
||||
.await
|
||||
.map(|r| r.get_template_frameworks)
|
||||
}
|
||||
|
||||
/// Fetch all frameworks from available app templates by paginating through the responses.
|
||||
///
|
||||
/// Will fetch at most `max` templates.
|
||||
pub fn fetch_all_app_template_frameworks(
|
||||
client: &WasmerClient,
|
||||
page_size: Option<i32>,
|
||||
) -> impl futures::Stream<Item = Result<Vec<types::TemplateFramework>, anyhow::Error>> + '_ {
|
||||
let vars = GetTemplateFrameworksVars {
|
||||
after: None,
|
||||
first: page_size,
|
||||
};
|
||||
|
||||
futures::stream::try_unfold(
|
||||
Some(vars),
|
||||
move |vars: Option<types::GetTemplateFrameworksVars>| async move {
|
||||
let vars = match vars {
|
||||
Some(vars) => vars,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let con = client
|
||||
.run_graphql_strict(types::GetTemplateFrameworks::build(vars.clone()))
|
||||
.await?
|
||||
.get_template_frameworks
|
||||
.context("backend did not return any data")?;
|
||||
|
||||
let items = con
|
||||
.edges
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|edge| edge.node)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let next_cursor = con
|
||||
.page_info
|
||||
.end_cursor
|
||||
.filter(|_| con.page_info.has_next_page);
|
||||
|
||||
let next_vars = next_cursor.map(|after| types::GetTemplateFrameworksVars {
|
||||
after: Some(after),
|
||||
..vars
|
||||
});
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
let res: Result<
|
||||
Option<(
|
||||
Vec<types::TemplateFramework>,
|
||||
Option<types::GetTemplateFrameworksVars>,
|
||||
)>,
|
||||
anyhow::Error,
|
||||
> = Ok(Some((items, next_vars)));
|
||||
|
||||
res
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Get a signed URL to upload packages.
|
||||
pub async fn get_signed_url_for_package_upload(
|
||||
client: &WasmerClient,
|
||||
@ -193,7 +824,7 @@ pub async fn tag_package_release(
|
||||
homepage: Option<&str>,
|
||||
license: Option<&str>,
|
||||
license_file: Option<&str>,
|
||||
manifest: &str,
|
||||
manifest: Option<&str>,
|
||||
name: &str,
|
||||
namespace: Option<&str>,
|
||||
package_release_id: &cynic::Id,
|
||||
@ -429,6 +1060,71 @@ pub async fn all_app_versions(
|
||||
Ok(all_versions)
|
||||
}
|
||||
|
||||
/// Retrieve versions for an app.
|
||||
pub async fn get_deploy_app_versions_by_id(
|
||||
client: &WasmerClient,
|
||||
vars: types::GetDeployAppVersionsByIdVars,
|
||||
) -> Result<DeployAppVersionConnection, anyhow::Error> {
|
||||
let res = client
|
||||
.run_graphql_strict(types::GetDeployAppVersionsById::build(vars))
|
||||
.await?;
|
||||
let versions = res
|
||||
.node
|
||||
.context("app not found")?
|
||||
.into_app()
|
||||
.context("invalid node type returned")?
|
||||
.versions;
|
||||
Ok(versions)
|
||||
}
|
||||
|
||||
/// Load all versions of an app id.
|
||||
///
|
||||
/// Will paginate through all versions and return them in a single list.
|
||||
pub async fn all_app_versions_by_id(
|
||||
client: &WasmerClient,
|
||||
app_id: impl Into<String>,
|
||||
) -> Result<Vec<DeployAppVersion>, anyhow::Error> {
|
||||
let mut vars = types::GetDeployAppVersionsByIdVars {
|
||||
id: cynic::Id::new(app_id),
|
||||
offset: None,
|
||||
before: None,
|
||||
after: None,
|
||||
first: Some(10),
|
||||
last: None,
|
||||
sort_by: None,
|
||||
};
|
||||
|
||||
let mut all_versions = Vec::<DeployAppVersion>::new();
|
||||
|
||||
loop {
|
||||
let page = get_deploy_app_versions_by_id(client, vars.clone()).await?;
|
||||
if page.edges.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for edge in page.edges {
|
||||
let edge = match edge {
|
||||
Some(edge) => edge,
|
||||
None => continue,
|
||||
};
|
||||
let version = match edge.node {
|
||||
Some(item) => item,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Sanity check to avoid duplication.
|
||||
if all_versions.iter().any(|v| v.id == version.id) == false {
|
||||
all_versions.push(version);
|
||||
}
|
||||
|
||||
// Update pagination.
|
||||
vars.after = Some(edge.cursor);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(all_versions)
|
||||
}
|
||||
|
||||
/// Activate a particular version of an app.
|
||||
pub async fn app_version_activate(
|
||||
client: &WasmerClient,
|
||||
@ -571,11 +1267,15 @@ pub async fn get_app_version_by_id_with_app(
|
||||
/// NOTE: this will only include the first pages and does not provide pagination.
|
||||
pub async fn user_apps(
|
||||
client: &WasmerClient,
|
||||
sort: types::DeployAppsSortBy,
|
||||
) -> impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_ {
|
||||
futures::stream::try_unfold(None, move |cursor| async move {
|
||||
let user = client
|
||||
.run_graphql(types::GetCurrentUserWithApps::build(
|
||||
GetCurrentUserWithAppsVars { after: cursor },
|
||||
GetCurrentUserWithAppsVars {
|
||||
after: cursor,
|
||||
sort: Some(sort),
|
||||
},
|
||||
))
|
||||
.await?
|
||||
.viewer
|
||||
@ -602,12 +1302,12 @@ pub async fn user_apps(
|
||||
/// List all apps that are accessible by the current user.
|
||||
pub async fn user_accessible_apps(
|
||||
client: &WasmerClient,
|
||||
sort: types::DeployAppsSortBy,
|
||||
) -> Result<
|
||||
impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_,
|
||||
anyhow::Error,
|
||||
> {
|
||||
let apps: Pin<Box<dyn Stream<Item = Result<Vec<DeployApp>, anyhow::Error>> + Send + Sync>> =
|
||||
Box::pin(user_apps(client).await);
|
||||
let user_apps = user_apps(client, sort).await;
|
||||
|
||||
// Get all aps in user-accessible namespaces.
|
||||
let namespace_res = client
|
||||
@ -627,17 +1327,13 @@ pub async fn user_accessible_apps(
|
||||
.map(|node| node.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut all_apps = vec![apps];
|
||||
let mut ns_apps = vec![];
|
||||
for ns in namespace_names {
|
||||
let apps: Pin<Box<dyn Stream<Item = Result<Vec<DeployApp>, anyhow::Error>> + Send + Sync>> =
|
||||
Box::pin(namespace_apps(client, ns).await);
|
||||
|
||||
all_apps.push(apps);
|
||||
let apps = namespace_apps(client, ns, sort).await;
|
||||
ns_apps.push(apps);
|
||||
}
|
||||
|
||||
let apps = futures::stream::select_all(all_apps);
|
||||
|
||||
Ok(apps)
|
||||
Ok((user_apps, ns_apps.merge()).merge())
|
||||
}
|
||||
|
||||
/// Get apps for a specific namespace.
|
||||
@ -646,6 +1342,7 @@ pub async fn user_accessible_apps(
|
||||
pub async fn namespace_apps(
|
||||
client: &WasmerClient,
|
||||
namespace: String,
|
||||
sort: types::DeployAppsSortBy,
|
||||
) -> impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_ {
|
||||
let namespace = namespace.clone();
|
||||
|
||||
@ -654,6 +1351,7 @@ pub async fn namespace_apps(
|
||||
.run_graphql(types::GetNamespaceApps::build(GetNamespaceAppsVars {
|
||||
name: namespace.to_string(),
|
||||
after: cursor,
|
||||
sort: Some(sort),
|
||||
}))
|
||||
.await?;
|
||||
|
||||
@ -963,6 +1661,8 @@ fn get_app_logs(
|
||||
end: Option<OffsetDateTime>,
|
||||
watch: bool,
|
||||
streams: Option<Vec<LogStream>>,
|
||||
request_id: Option<String>,
|
||||
instance_ids: Option<Vec<String>>,
|
||||
) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
|
||||
// Note: the backend will limit responses to a certain number of log
|
||||
// messages, so we use try_unfold() to keep calling it until we stop getting
|
||||
@ -978,6 +1678,8 @@ fn get_app_logs(
|
||||
starting_from: unix_timestamp(start),
|
||||
until: end.map(unix_timestamp),
|
||||
streams: streams.clone(),
|
||||
request_id: request_id.clone(),
|
||||
instance_ids: instance_ids.clone(),
|
||||
};
|
||||
|
||||
let fut = async move {
|
||||
@ -999,10 +1701,15 @@ fn get_app_logs(
|
||||
if page.is_empty() {
|
||||
if watch {
|
||||
/*
|
||||
TODO: the resolution of watch should be configurable
|
||||
TODO: should this be async?
|
||||
*/
|
||||
* [TODO]: The resolution here should be configurable.
|
||||
*/
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -1051,7 +1758,103 @@ pub async fn get_app_logs_paginated(
|
||||
watch: bool,
|
||||
streams: Option<Vec<LogStream>>,
|
||||
) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
|
||||
let stream = get_app_logs(client, name, owner, tag, start, end, watch, streams);
|
||||
let stream = get_app_logs(
|
||||
client, name, owner, tag, start, end, watch, streams, None, None,
|
||||
);
|
||||
|
||||
stream.map(|res| {
|
||||
let mut logs = Vec::new();
|
||||
let mut hasher = HashSet::new();
|
||||
let mut page = res?;
|
||||
|
||||
// Prevent duplicates.
|
||||
// TODO: don't clone the message, just hash it.
|
||||
page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
|
||||
|
||||
logs.extend(page);
|
||||
|
||||
Ok(logs)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get pages of logs associated with an application that lie within the
|
||||
/// specified date range with a specific instance identifier.
|
||||
///
|
||||
/// In contrast to [`get_app_logs`], this function collects the stream into a
|
||||
/// final vector.
|
||||
#[tracing::instrument(skip_all, level = "debug")]
|
||||
#[allow(clippy::let_with_type_underscore)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn get_app_logs_paginated_filter_instance(
|
||||
client: &WasmerClient,
|
||||
name: String,
|
||||
owner: String,
|
||||
tag: Option<String>,
|
||||
start: OffsetDateTime,
|
||||
end: Option<OffsetDateTime>,
|
||||
watch: bool,
|
||||
streams: Option<Vec<LogStream>>,
|
||||
instance_ids: Vec<String>,
|
||||
) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
|
||||
let stream = get_app_logs(
|
||||
client,
|
||||
name,
|
||||
owner,
|
||||
tag,
|
||||
start,
|
||||
end,
|
||||
watch,
|
||||
streams,
|
||||
None,
|
||||
Some(instance_ids),
|
||||
);
|
||||
|
||||
stream.map(|res| {
|
||||
let mut logs = Vec::new();
|
||||
let mut hasher = HashSet::new();
|
||||
let mut page = res?;
|
||||
|
||||
// Prevent duplicates.
|
||||
// TODO: don't clone the message, just hash it.
|
||||
page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
|
||||
|
||||
logs.extend(page);
|
||||
|
||||
Ok(logs)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get pages of logs associated with an specific request for application that lie within the
|
||||
/// specified date range.
|
||||
///
|
||||
/// In contrast to [`get_app_logs`], this function collects the stream into a
|
||||
/// final vector.
|
||||
#[tracing::instrument(skip_all, level = "debug")]
|
||||
#[allow(clippy::let_with_type_underscore)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn get_app_logs_paginated_filter_request(
|
||||
client: &WasmerClient,
|
||||
name: String,
|
||||
owner: String,
|
||||
tag: Option<String>,
|
||||
start: OffsetDateTime,
|
||||
end: Option<OffsetDateTime>,
|
||||
watch: bool,
|
||||
streams: Option<Vec<LogStream>>,
|
||||
request_id: String,
|
||||
) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
|
||||
let stream = get_app_logs(
|
||||
client,
|
||||
name,
|
||||
owner,
|
||||
tag,
|
||||
start,
|
||||
end,
|
||||
watch,
|
||||
streams,
|
||||
Some(request_id),
|
||||
None,
|
||||
);
|
||||
|
||||
stream.map(|res| {
|
||||
let mut logs = Vec::new();
|
||||
|
@ -41,6 +41,73 @@ mod queries {
|
||||
Viewer,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct ViewerCanVariables<'a> {
|
||||
pub action: OwnerAction,
|
||||
pub owner_name: &'a str,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Query", variables = "ViewerCanVariables")]
|
||||
pub struct ViewerCan {
|
||||
#[arguments(action: $action, ownerName: $owner_name)]
|
||||
pub viewer_can: bool,
|
||||
}
|
||||
|
||||
#[derive(cynic::Enum, Clone, Copy, Debug)]
|
||||
pub enum OwnerAction {
|
||||
DeployApp,
|
||||
PublishPackage,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct RevokeTokenVariables {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Mutation", variables = "RevokeTokenVariables")]
|
||||
pub struct RevokeToken {
|
||||
#[arguments(input: { token: $token })]
|
||||
pub revoke_api_token: Option<RevokeAPITokenPayload>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct RevokeAPITokenPayload {
|
||||
pub success: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct CreateNewNonceVariables {
|
||||
pub callback_url: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Mutation", variables = "CreateNewNonceVariables")]
|
||||
pub struct CreateNewNonce {
|
||||
#[arguments(input: { callbackUrl: $callback_url, name: $name })]
|
||||
pub new_nonce: Option<NewNoncePayload>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct NewNoncePayload {
|
||||
pub client_mutation_id: Option<String>,
|
||||
pub nonce: Nonce,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct Nonce {
|
||||
pub auth_url: String,
|
||||
pub callback_url: String,
|
||||
pub created_at: DateTime,
|
||||
pub expired: bool,
|
||||
pub id: cynic::Id,
|
||||
pub is_validated: bool,
|
||||
pub name: String,
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Query")]
|
||||
pub struct GetCurrentUser {
|
||||
@ -58,7 +125,7 @@ mod queries {
|
||||
pub viewer: Option<UserWithNamespaces>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[derive(cynic::QueryFragment, Debug, serde::Serialize)]
|
||||
pub struct User {
|
||||
pub id: cynic::Id,
|
||||
pub username: String,
|
||||
@ -81,6 +148,7 @@ mod queries {
|
||||
pub size: Option<i32>,
|
||||
pub pirita_size: Option<i32>,
|
||||
pub webc_version: Option<WebcVersion>,
|
||||
pub webc_manifest: Option<JSONString>,
|
||||
}
|
||||
|
||||
#[derive(cynic::Enum, Clone, Copy, Debug)]
|
||||
@ -157,6 +225,46 @@ mod queries {
|
||||
Popular,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug, Clone)]
|
||||
pub struct GetAppTemplatesFromFrameworkVars {
|
||||
pub framework_slug: String,
|
||||
pub first: i32,
|
||||
pub after: Option<String>,
|
||||
pub sort_by: Option<AppTemplatesSortBy>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Query", variables = "GetAppTemplatesFromFrameworkVars")]
|
||||
pub struct GetAppTemplatesFromFramework {
|
||||
#[arguments(
|
||||
frameworkSlug: $framework_slug,
|
||||
first: $first,
|
||||
after: $after,
|
||||
sortBy: $sort_by
|
||||
)]
|
||||
pub get_app_templates: Option<AppTemplateConnection>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug, Clone)]
|
||||
pub struct GetAppTemplatesFromLanguageVars {
|
||||
pub language_slug: String,
|
||||
pub first: i32,
|
||||
pub after: Option<String>,
|
||||
pub sort_by: Option<AppTemplatesSortBy>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Query", variables = "GetAppTemplatesFromLanguageVars")]
|
||||
pub struct GetAppTemplatesFromLanguage {
|
||||
#[arguments(
|
||||
languageSlug: $language_slug,
|
||||
first: $first,
|
||||
after: $after,
|
||||
sortBy: $sort_by
|
||||
)]
|
||||
pub get_app_templates: Option<AppTemplateConnection>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug, Clone)]
|
||||
pub struct GetAppTemplatesVars {
|
||||
pub category_slug: String,
|
||||
@ -214,6 +322,80 @@ mod queries {
|
||||
pub use_cases: Jsonstring,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug, Clone)]
|
||||
pub struct GetTemplateFrameworksVars {
|
||||
pub after: Option<String>,
|
||||
pub first: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Query", variables = "GetTemplateFrameworksVars")]
|
||||
pub struct GetTemplateFrameworks {
|
||||
#[arguments(after: $after, first: $first)]
|
||||
pub get_template_frameworks: Option<TemplateFrameworkConnection>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct TemplateFrameworkConnection {
|
||||
pub edges: Vec<Option<TemplateFrameworkEdge>>,
|
||||
pub page_info: PageInfo,
|
||||
pub total_count: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct TemplateFrameworkEdge {
|
||||
pub cursor: String,
|
||||
pub node: Option<TemplateFramework>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, cynic::QueryFragment, PartialEq, Eq, Debug)]
|
||||
pub struct TemplateFramework {
|
||||
#[serde(rename = "createdAt")]
|
||||
pub created_at: DateTime,
|
||||
pub id: cynic::Id,
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
#[serde(rename = "updatedAt")]
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug, Clone)]
|
||||
pub struct GetTemplateLanguagesVars {
|
||||
pub after: Option<String>,
|
||||
pub first: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Query", variables = "GetTemplateLanguagesVars")]
|
||||
pub struct GetTemplateLanguages {
|
||||
#[arguments(after: $after, first: $first)]
|
||||
pub get_template_languages: Option<TemplateLanguageConnection>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct TemplateLanguageConnection {
|
||||
pub edges: Vec<Option<TemplateLanguageEdge>>,
|
||||
pub page_info: PageInfo,
|
||||
pub total_count: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct TemplateLanguageEdge {
|
||||
pub cursor: String,
|
||||
pub node: Option<TemplateLanguage>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, cynic::QueryFragment, PartialEq, Eq, Debug)]
|
||||
pub struct TemplateLanguage {
|
||||
#[serde(rename = "createdAt")]
|
||||
pub created_at: DateTime,
|
||||
pub id: cynic::Id,
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
#[serde(rename = "updatedAt")]
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(cynic::Scalar, Debug, Clone, PartialEq, Eq)]
|
||||
#[cynic(graphql_type = "JSONString")]
|
||||
pub struct Jsonstring(pub String);
|
||||
@ -288,7 +470,7 @@ mod queries {
|
||||
pub homepage: Option<&'a str>,
|
||||
pub license: Option<&'a str>,
|
||||
pub license_file: Option<&'a str>,
|
||||
pub manifest: &'a str,
|
||||
pub manifest: Option<&'a str>,
|
||||
pub name: &'a str,
|
||||
pub namespace: Option<&'a str>,
|
||||
pub package_release_id: &'a cynic::Id,
|
||||
@ -451,6 +633,7 @@ mod queries {
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct GetCurrentUserWithAppsVars {
|
||||
pub after: Option<String>,
|
||||
pub sort: Option<DeployAppsSortBy>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
@ -465,7 +648,7 @@ mod queries {
|
||||
pub struct UserWithApps {
|
||||
pub id: cynic::Id,
|
||||
pub username: String,
|
||||
#[arguments(after: $after)]
|
||||
#[arguments(after: $after, sortBy: $sort)]
|
||||
pub apps: DeployAppConnection,
|
||||
}
|
||||
|
||||
@ -508,6 +691,46 @@ mod queries {
|
||||
pub get_deploy_app: Option<DeployApp>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Query", variables = "GetDeployAppVars")]
|
||||
pub struct GetDeployAppS3Credentials {
|
||||
#[arguments(owner: $owner, name: $name)]
|
||||
pub get_deploy_app: Option<AppWithS3Credentials>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "DeployApp", variables = "GetDeployAppVars")]
|
||||
pub struct AppWithS3Credentials {
|
||||
pub s3_credentials: Option<S3Credentials>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct S3Credentials {
|
||||
pub access_key: String,
|
||||
pub secret_key: String,
|
||||
pub endpoint: String,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct RotateS3SecretsForAppVariables {
|
||||
pub id: cynic::Id,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(
|
||||
graphql_type = "Mutation",
|
||||
variables = "RotateS3SecretsForAppVariables"
|
||||
)]
|
||||
pub struct RotateS3SecretsForApp {
|
||||
#[arguments(input: { id: $id })]
|
||||
pub rotate_s3_secrets_for_app: Option<RotateS3SecretsForAppPayload>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct RotateS3SecretsForAppPayload {
|
||||
pub client_mutation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug, Clone)]
|
||||
pub struct PaginationVars {
|
||||
pub offset: Option<i32>,
|
||||
@ -593,6 +816,38 @@ mod queries {
|
||||
pub get_deploy_app_version: Option<DeployAppVersion>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub(crate) struct GetAppVolumesVars {
|
||||
pub name: String,
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Query", variables = "GetAppVolumesVars")]
|
||||
pub(crate) struct GetAppVolumes {
|
||||
#[arguments(owner: $owner, name: $name)]
|
||||
pub get_deploy_app: Option<AppVolumes>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "DeployApp")]
|
||||
pub(crate) struct AppVolumes {
|
||||
pub active_version: AppVersionVolumes,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "DeployAppVersion")]
|
||||
pub(crate) struct AppVersionVolumes {
|
||||
pub volumes: Option<Vec<Option<AppVersionVolume>>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, cynic::QueryFragment, Debug)]
|
||||
pub struct AppVersionVolume {
|
||||
pub name: String,
|
||||
pub size: Option<i32>,
|
||||
pub used_size: Option<BigInt>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct RegisterDomainPayload {
|
||||
pub success: bool,
|
||||
@ -681,6 +936,7 @@ mod queries {
|
||||
pub id: cynic::Id,
|
||||
pub name: String,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
pub description: Option<String>,
|
||||
pub active_version: DeployAppVersion,
|
||||
pub admin_url: String,
|
||||
@ -689,6 +945,7 @@ mod queries {
|
||||
pub permalink: String,
|
||||
pub deleted: bool,
|
||||
pub aliases: AppAliasConnection,
|
||||
pub s3_url: Option<Url>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
|
||||
@ -765,6 +1022,39 @@ mod queries {
|
||||
pub versions: DeployAppVersionConnection,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug, Clone)]
|
||||
pub struct GetDeployAppVersionsByIdVars {
|
||||
pub id: cynic::Id,
|
||||
|
||||
pub offset: Option<i32>,
|
||||
pub before: Option<String>,
|
||||
pub after: Option<String>,
|
||||
pub first: Option<i32>,
|
||||
pub last: Option<i32>,
|
||||
pub sort_by: Option<DeployAppVersionsSortBy>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug, Clone, Serialize)]
|
||||
#[cynic(graphql_type = "DeployApp", variables = "GetDeployAppVersionsByIdVars")]
|
||||
pub struct DeployAppVersionsById {
|
||||
#[arguments(
|
||||
first: $first,
|
||||
last: $last,
|
||||
before: $before,
|
||||
after: $after,
|
||||
offset: $offset,
|
||||
sortBy: $sort_by
|
||||
)]
|
||||
pub versions: DeployAppVersionConnection,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug, Clone)]
|
||||
#[cynic(graphql_type = "Query", variables = "GetDeployAppVersionsByIdVars")]
|
||||
pub struct GetDeployAppVersionsById {
|
||||
#[arguments(id: $id)]
|
||||
pub node: Option<NodeDeployAppVersions>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
|
||||
#[cynic(graphql_type = "DeployApp")]
|
||||
pub struct SparseDeployApp {
|
||||
@ -775,6 +1065,7 @@ mod queries {
|
||||
pub struct DeployAppVersion {
|
||||
pub id: cynic::Id,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
pub version: String,
|
||||
pub description: Option<String>,
|
||||
pub yaml_config: String,
|
||||
@ -782,6 +1073,8 @@ mod queries {
|
||||
pub config: String,
|
||||
pub json_config: String,
|
||||
pub url: String,
|
||||
pub disabled_at: Option<DateTime>,
|
||||
pub disabled_reason: Option<String>,
|
||||
|
||||
pub app: Option<SparseDeployApp>,
|
||||
}
|
||||
@ -854,6 +1147,7 @@ mod queries {
|
||||
pub struct GetNamespaceAppsVars {
|
||||
pub name: String,
|
||||
pub after: Option<String>,
|
||||
pub sort: Option<DeployAppsSortBy>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
@ -869,10 +1163,27 @@ mod queries {
|
||||
pub struct NamespaceWithApps {
|
||||
pub id: cynic::Id,
|
||||
pub name: String,
|
||||
#[arguments(after: $after)]
|
||||
#[arguments(after: $after, sortBy: $sort)]
|
||||
pub apps: DeployAppConnection,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct RedeployActiveAppVariables {
|
||||
pub id: cynic::Id,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Mutation", variables = "RedeployActiveAppVariables")]
|
||||
pub struct RedeployActiveApp {
|
||||
#[arguments(input: { id: $id })]
|
||||
pub redeploy_active_version: Option<RedeployActiveVersionPayload>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct RedeployActiveVersionPayload {
|
||||
pub app: DeployApp,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct PublishDeployAppVars {
|
||||
pub config: String,
|
||||
@ -932,6 +1243,10 @@ mod queries {
|
||||
pub until: Option<f64>,
|
||||
pub first: Option<i32>,
|
||||
|
||||
pub request_id: Option<String>,
|
||||
|
||||
pub instance_ids: Option<Vec<String>>,
|
||||
|
||||
pub streams: Option<Vec<LogStream>>,
|
||||
}
|
||||
|
||||
@ -945,7 +1260,7 @@ mod queries {
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "DeployAppVersion", variables = "GetDeployAppLogsVars")]
|
||||
pub struct DeployAppVersionLogs {
|
||||
#[arguments(startingFrom: $starting_from, until: $until, first: $first)]
|
||||
#[arguments(startingFrom: $starting_from, until: $until, first: $first, instanceIds: $instance_ids, requestId: $request_id, streams: $streams)]
|
||||
pub logs: LogConnection,
|
||||
}
|
||||
|
||||
@ -965,6 +1280,7 @@ mod queries {
|
||||
/// When the message was recorded, in nanoseconds since the Unix epoch.
|
||||
pub timestamp: f64,
|
||||
pub stream: Option<LogStream>,
|
||||
pub instance_id: String,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
@ -1038,6 +1354,168 @@ mod queries {
|
||||
pub version: Option<Node>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct DeleteAppSecretVariables {
|
||||
pub id: cynic::Id,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Mutation", variables = "DeleteAppSecretVariables")]
|
||||
pub struct DeleteAppSecret {
|
||||
#[arguments(input: { id: $id })]
|
||||
pub delete_app_secret: Option<DeleteAppSecretPayload>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct DeleteAppSecretPayload {
|
||||
pub success: bool,
|
||||
}
|
||||
#[derive(cynic::QueryVariables, Debug, Clone)]
|
||||
pub struct GetAllAppSecretsVariables {
|
||||
pub after: Option<String>,
|
||||
pub app_id: cynic::Id,
|
||||
pub before: Option<String>,
|
||||
pub first: Option<i32>,
|
||||
pub last: Option<i32>,
|
||||
pub offset: Option<i32>,
|
||||
pub names: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Query", variables = "GetAllAppSecretsVariables")]
|
||||
pub struct GetAllAppSecrets {
|
||||
#[arguments(appId: $app_id, after: $after, before: $before, first: $first, last: $last, offset: $offset, names: $names)]
|
||||
pub get_app_secrets: Option<SecretConnection>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct SecretConnection {
|
||||
pub edges: Vec<Option<SecretEdge>>,
|
||||
pub page_info: PageInfo,
|
||||
pub total_count: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct SecretEdge {
|
||||
pub cursor: String,
|
||||
pub node: Option<Secret>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct GetAppSecretVariables {
|
||||
pub app_id: cynic::Id,
|
||||
pub secret_name: String,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Query", variables = "GetAppSecretVariables")]
|
||||
pub struct GetAppSecret {
|
||||
#[arguments(appId: $app_id, secretName: $secret_name)]
|
||||
pub get_app_secret: Option<Secret>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct GetAppSecretValueVariables {
|
||||
pub id: cynic::Id,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Query", variables = "GetAppSecretValueVariables")]
|
||||
pub struct GetAppSecretValue {
|
||||
#[arguments(id: $id)]
|
||||
pub get_secret_value: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct UpsertAppSecretVariables<'a> {
|
||||
pub app_id: cynic::Id,
|
||||
pub name: &'a str,
|
||||
pub value: &'a str,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Mutation", variables = "UpsertAppSecretVariables")]
|
||||
pub struct UpsertAppSecret {
|
||||
#[arguments(input: { appId: $app_id, name: $name, value: $value })]
|
||||
pub upsert_app_secret: Option<UpsertAppSecretPayload>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct UpsertAppSecretPayload {
|
||||
pub secret: Secret,
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct UpsertAppSecretsVariables {
|
||||
pub app_id: cynic::Id,
|
||||
pub secrets: Option<Vec<SecretInput>>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Mutation", variables = "UpsertAppSecretsVariables")]
|
||||
pub struct UpsertAppSecrets {
|
||||
#[arguments(input: { appId: $app_id, secrets: $secrets })]
|
||||
pub upsert_app_secrets: Option<UpsertAppSecretsPayload>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct UpsertAppSecretsPayload {
|
||||
pub secrets: Vec<Option<Secret>>,
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(cynic::InputObject, Debug)]
|
||||
pub struct SecretInput {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
#[derive(cynic::QueryFragment, Debug, Serialize)]
|
||||
pub struct Secret {
|
||||
#[serde(skip_serializing)]
|
||||
pub id: cynic::Id,
|
||||
pub name: String,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug, Clone)]
|
||||
pub struct GetAllAppRegionsVariables {
|
||||
pub after: Option<String>,
|
||||
pub before: Option<String>,
|
||||
pub first: Option<i32>,
|
||||
pub last: Option<i32>,
|
||||
pub offset: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Query", variables = "GetAllAppRegionsVariables")]
|
||||
pub struct GetAllAppRegions {
|
||||
#[arguments(after: $after, offset: $offset, before: $before, first: $first, last: $last)]
|
||||
pub get_app_regions: AppRegionConnection,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct AppRegionConnection {
|
||||
pub edges: Vec<Option<AppRegionEdge>>,
|
||||
pub page_info: PageInfo,
|
||||
pub total_count: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct AppRegionEdge {
|
||||
pub cursor: String,
|
||||
pub node: Option<AppRegion>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug, Serialize)]
|
||||
pub struct AppRegion {
|
||||
pub city: String,
|
||||
pub country: String,
|
||||
pub id: cynic::Id,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug, Clone, Serialize)]
|
||||
#[cynic(graphql_type = "TXTRecord")]
|
||||
pub struct TxtRecord {
|
||||
@ -1592,9 +2070,30 @@ mod queries {
|
||||
pub purge_cache_for_app_version: Option<PurgeCacheForAppVersionPayload>,
|
||||
}
|
||||
|
||||
#[derive(cynic::Scalar, Debug, Clone)]
|
||||
#[cynic(graphql_type = "URL")]
|
||||
pub struct Url(pub String);
|
||||
|
||||
#[derive(cynic::Scalar, Debug, Clone)]
|
||||
pub struct BigInt(pub i64);
|
||||
|
||||
#[derive(cynic::InlineFragments, Debug, Clone)]
|
||||
#[cynic(graphql_type = "Node", variables = "GetDeployAppVersionsByIdVars")]
|
||||
pub enum NodeDeployAppVersions {
|
||||
DeployApp(Box<DeployAppVersionsById>),
|
||||
#[cynic(fallback)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl NodeDeployAppVersions {
|
||||
pub fn into_app(self) -> Option<DeployAppVersionsById> {
|
||||
match self {
|
||||
Self::DeployApp(v) => Some(*v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(cynic::InlineFragments, Debug)]
|
||||
pub enum Node {
|
||||
DeployApp(Box<DeployApp>),
|
||||
|
@ -24,17 +24,17 @@ crate-type = ["staticlib", "cdylib"] #"cdylib", "rlib", "staticlib"]
|
||||
[dependencies]
|
||||
# We rename `wasmer` to `wasmer-api` to avoid the conflict with this
|
||||
# library name (see `[lib]`).
|
||||
wasmer-api = { version = "=4.3.2", path = "../api", default-features = false, package = "wasmer" }
|
||||
wasmer-compiler = { version = "=4.3.2", path = "../compiler", optional = true }
|
||||
wasmer-compiler-cranelift = { version = "=4.3.2", path = "../compiler-cranelift", optional = true }
|
||||
wasmer-compiler-llvm = { version = "=4.3.2", path = "../compiler-llvm", optional = true }
|
||||
wasmer-compiler-singlepass = { version = "=4.3.2", path = "../compiler-singlepass", optional = true }
|
||||
wasmer-emscripten = { version = "=4.3.2", path = "../emscripten", optional = true }
|
||||
wasmer-middlewares = { version = "=4.3.2", path = "../middlewares", optional = true }
|
||||
wasmer-types = { version = "=4.3.2", path = "../types" }
|
||||
wasmer-wasix = { path = "../wasix", version="=0.22.0", features = ["host-fs", "host-vnet"], optional = true }
|
||||
wasmer-api = { version = "=4.3.7", path = "../api", default-features = false, package = "wasmer" }
|
||||
wasmer-compiler = { version = "=4.3.7", path = "../compiler", optional = true }
|
||||
wasmer-compiler-cranelift = { version = "=4.3.7", path = "../compiler-cranelift", optional = true }
|
||||
wasmer-compiler-llvm = { version = "=4.3.7", path = "../compiler-llvm", optional = true }
|
||||
wasmer-compiler-singlepass = { version = "=4.3.7", path = "../compiler-singlepass", optional = true }
|
||||
wasmer-emscripten = { version = "=4.3.7", path = "../emscripten", optional = true }
|
||||
wasmer-middlewares = { version = "=4.3.7", path = "../middlewares", optional = true }
|
||||
wasmer-types = { version = "=4.3.7", path = "../types" }
|
||||
wasmer-wasix = { path = "../wasix", version="=0.27.0", features = ["host-fs", "host-vnet"], optional = true }
|
||||
webc = { workspace = true, optional = true }
|
||||
virtual-fs = { version = "0.13.0", path = "../virtual-fs", optional = true, default-features = false, features = ["static-fs"] }
|
||||
virtual-fs = { version = "0.16.0", path = "../virtual-fs", optional = true, default-features = false, features = ["static-fs"] }
|
||||
enumset.workspace = true
|
||||
cfg-if = "1.0"
|
||||
lazy_static = "1.4"
|
||||
@ -42,7 +42,7 @@ libc.workspace = true
|
||||
thiserror = "1"
|
||||
typetag = { version = "0.1", optional = true }
|
||||
paste = "1.0"
|
||||
tokio = { version = "1", features = [ "rt", "rt-multi-thread", "io-util", "sync", "macros"], default_features = false }
|
||||
tokio = { version = "1", features = [ "rt", "rt-multi-thread", "io-util", "sync", "macros"], default-features = false }
|
||||
tracing = { version = "0.1" }
|
||||
tracing-subscriber = { version = "0.3", features = [
|
||||
"env-filter",
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "wasmer-capi-examples-runner"
|
||||
version = "4.3.2"
|
||||
version = "4.3.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "wasmer-capi-examples-runner"
|
||||
|
@ -375,30 +375,30 @@ fn test_run() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn print_wasmer_root_to_stdout(config: &Config) {
|
||||
println!("print_wasmer_root_to_stdout");
|
||||
// #[cfg(test)]
|
||||
// fn print_wasmer_root_to_stdout(config: &Config) {
|
||||
// println!("print_wasmer_root_to_stdout");
|
||||
|
||||
use walkdir::WalkDir;
|
||||
// use walkdir::WalkDir;
|
||||
|
||||
for entry in WalkDir::new(&config.wasmer_dir)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
let f_name = String::from(entry.path().canonicalize().unwrap().to_string_lossy());
|
||||
println!("{f_name}");
|
||||
}
|
||||
// for entry in WalkDir::new(&config.wasmer_dir)
|
||||
// .into_iter()
|
||||
// .filter_map(Result::ok)
|
||||
// {
|
||||
// let f_name = String::from(entry.path().canonicalize().unwrap().to_string_lossy());
|
||||
// println!("{f_name}");
|
||||
// }
|
||||
|
||||
for entry in WalkDir::new(&config.root_dir)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
let f_name = String::from(entry.path().canonicalize().unwrap().to_string_lossy());
|
||||
println!("{f_name}");
|
||||
}
|
||||
// for entry in WalkDir::new(&config.root_dir)
|
||||
// .into_iter()
|
||||
// .filter_map(Result::ok)
|
||||
// {
|
||||
// let f_name = String::from(entry.path().canonicalize().unwrap().to_string_lossy());
|
||||
// println!("{f_name}");
|
||||
// }
|
||||
|
||||
println!("printed");
|
||||
}
|
||||
// println!("printed");
|
||||
// }
|
||||
|
||||
#[cfg(test)]
|
||||
fn fixup_symlinks(
|
||||
|
@ -54,7 +54,7 @@ use std::ptr::{self, NonNull};
|
||||
use std::slice;
|
||||
|
||||
thread_local! {
|
||||
static LAST_ERROR: RefCell<Option<String>> = RefCell::new(None);
|
||||
static LAST_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Rust function to register a new error.
|
||||
|
@ -253,7 +253,7 @@ pub unsafe extern "C" fn wasm_module_exports(
|
||||
/// "(module\n"
|
||||
/// " (import \"ns\" \"function\" (func))\n"
|
||||
/// " (import \"ns\" \"global\" (global f32))\n"
|
||||
/// " (import \"ns\" \"table\" (table 1 2 anyfunc))\n"
|
||||
/// " (import \"ns\" \"table\" (table 1 2 funcref))\n"
|
||||
/// " (import \"ns\" \"memory\" (memory 3 4)))"
|
||||
/// );
|
||||
/// wasm_byte_vec_t wasm;
|
||||
@ -668,7 +668,7 @@ mod tests {
|
||||
"(module\n"
|
||||
" (import \"ns\" \"function\" (func))\n"
|
||||
" (import \"ns\" \"global\" (global f32))\n"
|
||||
" (import \"ns\" \"table\" (table 1 2 anyfunc))\n"
|
||||
" (import \"ns\" \"table\" (table 1 2 funcref))\n"
|
||||
" (import \"ns\" \"memory\" (memory 3 4)))"
|
||||
);
|
||||
wasm_byte_vec_t wasm;
|
||||
|
@ -544,10 +544,32 @@ unsafe fn wasi_get_imports_inner(
|
||||
) -> Option<()> {
|
||||
let wasi_env = wasi_env?;
|
||||
let store = &mut wasi_env.store;
|
||||
let mut store_mut = store.store_mut();
|
||||
let module = module?;
|
||||
|
||||
let import_object = c_try!(wasi_env.inner.import_object(&mut store_mut, &module.inner));
|
||||
let mut import_object = c_try!(wasi_env
|
||||
.inner
|
||||
.import_object(&mut store.store_mut(), &module.inner));
|
||||
|
||||
let shared_memory = module.inner.imports().memories().next().map(|a| *a.ty());
|
||||
|
||||
let spawn_type = match shared_memory {
|
||||
Some(ty) => wasmer_wasix::runtime::SpawnMemoryType::CreateMemoryOfType(ty),
|
||||
None => wasmer_wasix::runtime::SpawnMemoryType::CreateMemory,
|
||||
};
|
||||
|
||||
let tasks = wasi_env
|
||||
.inner
|
||||
.data(&store.store())
|
||||
.runtime
|
||||
.task_manager()
|
||||
.clone();
|
||||
let memory = tasks
|
||||
.build_memory(&mut store.store_mut(), spawn_type)
|
||||
.unwrap();
|
||||
|
||||
if let Some(memory) = memory {
|
||||
import_object.define("env", "memory", memory);
|
||||
}
|
||||
|
||||
imports_set_buffer(store, &module.inner, import_object, imports)?;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
(module
|
||||
(func (export "func") (param i32 f64 f32) (result i32) (unreachable))
|
||||
(global (export "global") f64 (f64.const 0))
|
||||
(table (export "table") 0 50 anyfunc)
|
||||
(table (export "table") 0 50 funcref)
|
||||
(memory (export "memory") 1)
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "wasmer-c-api-test-runner"
|
||||
version = "4.3.2"
|
||||
version = "4.3.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "wasmer-c-api-test-runner"
|
||||
|
@ -316,46 +316,46 @@ fn test_ok() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn print_wasmer_root_to_stdout(config: &Config) {
|
||||
println!("print_wasmer_root_to_stdout");
|
||||
// #[cfg(test)]
|
||||
// fn print_wasmer_root_to_stdout(config: &Config) {
|
||||
// println!("print_wasmer_root_to_stdout");
|
||||
|
||||
use walkdir::WalkDir;
|
||||
// use walkdir::WalkDir;
|
||||
|
||||
println!(
|
||||
"wasmer dir: {}",
|
||||
std::path::Path::new(&config.wasmer_dir)
|
||||
.canonicalize()
|
||||
.unwrap()
|
||||
.display()
|
||||
);
|
||||
// println!(
|
||||
// "wasmer dir: {}",
|
||||
// std::path::Path::new(&config.wasmer_dir)
|
||||
// .canonicalize()
|
||||
// .unwrap()
|
||||
// .display()
|
||||
// );
|
||||
|
||||
for entry in WalkDir::new(&config.wasmer_dir)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
let f_name = String::from(entry.path().canonicalize().unwrap().to_string_lossy());
|
||||
println!("{f_name}");
|
||||
}
|
||||
// for entry in WalkDir::new(&config.wasmer_dir)
|
||||
// .into_iter()
|
||||
// .filter_map(Result::ok)
|
||||
// {
|
||||
// let f_name = String::from(entry.path().canonicalize().unwrap().to_string_lossy());
|
||||
// println!("{f_name}");
|
||||
// }
|
||||
|
||||
println!(
|
||||
"root dir: {}",
|
||||
std::path::Path::new(&config.root_dir)
|
||||
.canonicalize()
|
||||
.unwrap()
|
||||
.display()
|
||||
);
|
||||
// println!(
|
||||
// "root dir: {}",
|
||||
// std::path::Path::new(&config.root_dir)
|
||||
// .canonicalize()
|
||||
// .unwrap()
|
||||
// .display()
|
||||
// );
|
||||
|
||||
for entry in WalkDir::new(&config.root_dir)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
let f_name = String::from(entry.path().canonicalize().unwrap().to_string_lossy());
|
||||
println!("{f_name}");
|
||||
}
|
||||
// for entry in WalkDir::new(&config.root_dir)
|
||||
// .into_iter()
|
||||
// .filter_map(Result::ok)
|
||||
// {
|
||||
// let f_name = String::from(entry.path().canonicalize().unwrap().to_string_lossy());
|
||||
// println!("{f_name}");
|
||||
// }
|
||||
|
||||
println!("printed");
|
||||
}
|
||||
// println!("printed");
|
||||
// }
|
||||
|
||||
#[cfg(test)]
|
||||
fn fixup_symlinks(
|
||||
|
6
lib/cache/Cargo.toml
vendored
6
lib/cache/Cargo.toml
vendored
@ -13,7 +13,7 @@ rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
wasmer = { path = "../api", version = "=4.3.2", default-features = false }
|
||||
wasmer = { path = "../api", version = "=4.3.7", default-features = false }
|
||||
hex = "0.4"
|
||||
thiserror = "1"
|
||||
blake3 = "1.0"
|
||||
@ -26,8 +26,8 @@ clap_derive = { version = "=4.4.7" }
|
||||
clap_lex = { version = "=0.6.0" }
|
||||
tempfile = "3.6.0"
|
||||
rand = "0.8.3"
|
||||
wasmer = { path = "../api", version = "=4.3.2", default-features = false, features = ["sys", "cranelift"] }
|
||||
wasmer-compiler-singlepass = { path = "../compiler-singlepass", version = "=4.3.2" }
|
||||
wasmer = { path = "../api", version = "=4.3.7", default-features = false, features = ["sys", "cranelift"] }
|
||||
wasmer-compiler-singlepass = { path = "../compiler-singlepass", version = "=4.3.7" }
|
||||
|
||||
[features]
|
||||
default = ["filesystem"]
|
||||
|
@ -20,8 +20,8 @@ path = "src/bin/wasmer_compiler.rs"
|
||||
doc = false
|
||||
|
||||
[dependencies]
|
||||
wasmer-compiler = { version = "=4.3.2", path = "../compiler", features = ["compiler"] }
|
||||
wasmer-types = { version = "=4.3.2", path = "../types" }
|
||||
wasmer-compiler = { version = "=4.3.7", path = "../compiler", features = ["compiler"] }
|
||||
wasmer-types = { version = "=4.3.7", path = "../types" }
|
||||
is-terminal = "0.4.7"
|
||||
colored = "2.0"
|
||||
anyhow = "1.0"
|
||||
@ -36,16 +36,16 @@ log = { version = "0.4", optional = true }
|
||||
target-lexicon = { version = "0.12", features = ["std"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
wasmer-compiler-singlepass = { version = "=4.3.2", path = "../compiler-singlepass", optional = true }
|
||||
wasmer-compiler-cranelift = { version = "=4.3.2", path = "../compiler-cranelift", optional = true }
|
||||
clap = { version = "4.3.2", features = ["derive", "env"] }
|
||||
wasmer-compiler-singlepass = { version = "=4.3.7", path = "../compiler-singlepass", optional = true }
|
||||
wasmer-compiler-cranelift = { version = "=4.3.7", path = "../compiler-cranelift", optional = true }
|
||||
clap = { version = "4.3.7", features = ["derive", "env"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasmer-compiler-singlepass = { version = "=4.3.2", path = "../compiler-singlepass", optional = true, default-features = false, features = ["wasm"] }
|
||||
wasmer-compiler-cranelift = { version = "=4.3.2", path = "../compiler-cranelift", optional = true, default-features = false, features = ["wasm"] }
|
||||
wasmer-compiler-singlepass = { version = "=4.3.7", path = "../compiler-singlepass", optional = true, default-features = false, features = ["wasm"] }
|
||||
wasmer-compiler-cranelift = { version = "=4.3.7", path = "../compiler-cranelift", optional = true, default-features = false, features = ["wasm"] }
|
||||
# NOTE: Must use different features for clap because the "color" feature does not
|
||||
# work on wasi, due to the anstream dependency not compiling.
|
||||
clap = { version = "4.3.2", default-features = false, features = [
|
||||
clap = { version = "4.3.7", default-features = false, features = [
|
||||
"std",
|
||||
"help",
|
||||
"usage",
|
||||
|
@ -82,7 +82,7 @@ impl Compile {
|
||||
}
|
||||
let tunables = self.store.get_tunables_for_target(&target)?;
|
||||
|
||||
println!("Compiler: {}", compiler_type.to_string());
|
||||
println!("Compiler: {}", compiler_type);
|
||||
println!("Target: {}", target.triple());
|
||||
|
||||
// compile and save the artifact (without using module from api)
|
||||
|
@ -348,14 +348,18 @@ impl CompilerType {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for CompilerType {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Self::Singlepass => "singlepass".to_string(),
|
||||
Self::Cranelift => "cranelift".to_string(),
|
||||
Self::LLVM => "llvm".to_string(),
|
||||
Self::Headless => "headless".to_string(),
|
||||
}
|
||||
impl std::fmt::Display for CompilerType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Singlepass => "singlepass",
|
||||
Self::Cranelift => "cranelift",
|
||||
Self::LLVM => "llvm",
|
||||
Self::Headless => "headless",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,16 +109,16 @@ enable-serde = [
|
||||
[dependencies]
|
||||
# Repo-local dependencies.
|
||||
|
||||
wasmer = { version = "=4.3.2", path = "../api", default-features = false }
|
||||
wasmer-compiler = { version = "=4.3.2", path = "../compiler", features = [
|
||||
wasmer = { version = "=4.3.7", path = "../api", default-features = false }
|
||||
wasmer-compiler = { version = "=4.3.7", path = "../compiler", features = [
|
||||
"compiler",
|
||||
], optional = true }
|
||||
wasmer-compiler-cranelift = { version = "=4.3.2", path = "../compiler-cranelift", optional = true }
|
||||
wasmer-compiler-singlepass = { version = "=4.3.2", path = "../compiler-singlepass", optional = true }
|
||||
wasmer-compiler-llvm = { version = "=4.3.2", path = "../compiler-llvm", optional = true }
|
||||
wasmer-emscripten = { version = "=4.3.2", path = "../emscripten" }
|
||||
wasmer-vm = { version = "=4.3.2", path = "../vm", optional = true }
|
||||
wasmer-wasix = { path = "../wasix", version = "=0.22.0", features = [
|
||||
wasmer-compiler-cranelift = { version = "=4.3.7", path = "../compiler-cranelift", optional = true }
|
||||
wasmer-compiler-singlepass = { version = "=4.3.7", path = "../compiler-singlepass", optional = true }
|
||||
wasmer-compiler-llvm = { version = "=4.3.7", path = "../compiler-llvm", optional = true }
|
||||
wasmer-emscripten = { version = "=4.3.7", path = "../emscripten" }
|
||||
wasmer-vm = { version = "=4.3.7", path = "../vm", optional = true }
|
||||
wasmer-wasix = { path = "../wasix", version = "=0.27.0", features = [
|
||||
"logging",
|
||||
"webc_runner_rt_wcgi",
|
||||
"webc_runner_rt_dcgi",
|
||||
@ -127,25 +127,25 @@ wasmer-wasix = { path = "../wasix", version = "=0.22.0", features = [
|
||||
"host-fs",
|
||||
"ctrlc",
|
||||
] }
|
||||
wasmer-wast = { version = "=4.3.2", path = "../../tests/lib/wast", optional = true }
|
||||
wasmer-types = { version = "=4.3.2", path = "../types", features = [
|
||||
wasmer-wast = { version = "=4.3.7", path = "../../tests/lib/wast", optional = true }
|
||||
wasmer-types = { version = "=4.3.7", path = "../types", features = [
|
||||
"enable-serde",
|
||||
] }
|
||||
wasmer-registry = { version = "=5.14.0", path = "../registry", features = [
|
||||
wasmer-registry = { version = "=5.19.0", path = "../registry", features = [
|
||||
"build-package",
|
||||
"clap",
|
||||
] }
|
||||
wasmer-object = { version = "=4.3.2", path = "../object", optional = true }
|
||||
virtual-fs = { version = "0.13.0", path = "../virtual-fs", default-features = false, features = [
|
||||
wasmer-object = { version = "=4.3.7", path = "../object", optional = true }
|
||||
virtual-fs = { version = "0.16.0", path = "../virtual-fs", default-features = false, features = [
|
||||
"host-fs",
|
||||
] }
|
||||
virtual-net = { version = "0.6.7", path = "../virtual-net" }
|
||||
virtual-net = { version = "0.8.0", path = "../virtual-net" }
|
||||
virtual-mio = { version = "0.3.1", path = "../virtual-io" }
|
||||
|
||||
# Wasmer-owned dependencies.
|
||||
|
||||
webc = { workspace = true }
|
||||
wasmer-api = { version = "=0.0.30", path = "../backend-api" }
|
||||
wasmer-api = { version = "=0.0.34", path = "../backend-api" }
|
||||
edge-schema.workspace = true
|
||||
edge-util = { version = "=0.1.0" }
|
||||
lazy_static = "1.4.0"
|
||||
@ -160,11 +160,10 @@ time01 = { package = "time", version = "0.1.45", optional = true }
|
||||
|
||||
# Third-party dependencies.
|
||||
|
||||
http.workspace = true
|
||||
is-terminal = "0.4.7"
|
||||
colored = "2.0"
|
||||
anyhow = "1.0"
|
||||
|
||||
# For the inspect subcommand
|
||||
bytesize = "1.0"
|
||||
cfg-if = "1.0"
|
||||
tempfile = "3.6.0"
|
||||
@ -172,7 +171,7 @@ serde = { version = "1.0.147", features = ["derive"] }
|
||||
dirs = "4.0"
|
||||
serde_json = { version = "1.0" }
|
||||
target-lexicon = { version = "0.12", features = ["std"] }
|
||||
wasmer-config = { version = "0.4.0", path = "../config" }
|
||||
wasmer-config = { version = "0.8.0", path = "../config" }
|
||||
indexmap = "1.9.2"
|
||||
walkdir = "2.3.2"
|
||||
regex = "1.6.0"
|
||||
@ -206,8 +205,9 @@ once_cell = "1.17.1"
|
||||
indicatif = "0.17.5"
|
||||
opener = "0.6.1"
|
||||
normpath = "=1.1.1"
|
||||
hyper = { version = "0.14.27", features = ["server"] }
|
||||
http = "0.2.9"
|
||||
hyper = { workspace = true, features = ["server"] }
|
||||
hyper-util = { version = "0.1.5", features = ["tokio"] }
|
||||
http-body-util = "0.1.1"
|
||||
futures = "0.3.29"
|
||||
humantime = "2.1.0"
|
||||
interfaces = { version = "0.0.9", optional = true }
|
||||
@ -221,22 +221,25 @@ comfy-table = "7.0.1"
|
||||
# Used by tuntap and connect
|
||||
futures-util = "0.3"
|
||||
mio = { version = "0.8", optional = true }
|
||||
tokio-tungstenite = { version = "0.20.1", features = [
|
||||
tokio-tungstenite = { version = "0.21.0", features = [
|
||||
"rustls-tls-webpki-roots",
|
||||
"stream",
|
||||
], optional = true }
|
||||
mac_address = { version = "1.1.5", optional = true }
|
||||
tun-tap = { version = "0.1.3", features = ["tokio"], optional = true }
|
||||
tun-tap = { version = "0.1.4", features = ["tokio"], optional = true }
|
||||
|
||||
clap_complete = "4.5.2"
|
||||
clap_mangen = "0.2.20"
|
||||
zip = { version = "2.1.3", default-features = false, features = ["deflate"] }
|
||||
console = "0.15.8"
|
||||
dotenvy = "0.15.7"
|
||||
|
||||
# NOTE: Must use different features for clap because the "color" feature does not
|
||||
# work on wasi due to the anstream dependency not compiling.
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
clap = { version = "4.3.2", features = ["derive", "env"] }
|
||||
clap = { version = "4.3.7", features = ["derive", "env"] }
|
||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||
clap = { version = "4.3.2", default-features = false, features = [
|
||||
clap = { version = "4.3.7", default-features = false, features = [
|
||||
"std",
|
||||
"help",
|
||||
"usage",
|
||||
@ -247,7 +250,7 @@ clap = { version = "4.3.2", default-features = false, features = [
|
||||
] }
|
||||
|
||||
[target.'cfg(not(target_arch = "riscv64"))'.dependencies]
|
||||
reqwest = { version = "^0.11", default-features = false, features = [
|
||||
reqwest = { workspace = true, default-features = false, features = [
|
||||
"rustls-tls",
|
||||
"json",
|
||||
"multipart",
|
||||
@ -255,7 +258,7 @@ reqwest = { version = "^0.11", default-features = false, features = [
|
||||
] }
|
||||
|
||||
[target.'cfg(target_arch = "riscv64")'.dependencies]
|
||||
reqwest = { version = "^0.11", default-features = false, features = [
|
||||
reqwest = { workspace = true, default-features = false, features = [
|
||||
"native-tls",
|
||||
"json",
|
||||
"multipart",
|
||||
|
@ -14,13 +14,17 @@ use colored::Colorize;
|
||||
use dialoguer::{theme::ColorfulTheme, Confirm, Select};
|
||||
use futures::stream::TryStreamExt;
|
||||
use is_terminal::IsTerminal;
|
||||
use wasmer_api::{types::AppTemplate, WasmerClient};
|
||||
use wasmer_api::{
|
||||
types::{AppTemplate, TemplateLanguage},
|
||||
WasmerClient,
|
||||
};
|
||||
use wasmer_config::{app::AppConfigV1, package::PackageSource};
|
||||
|
||||
use super::{deploy::CmdAppDeploy, util::login_user};
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
opts::{ApiOpts, ItemFormatOpts, WasmerEnv},
|
||||
config::WasmerEnv,
|
||||
opts::ItemFormatOpts,
|
||||
utils::{load_package_manifest, prompts::PackageCheckMode},
|
||||
};
|
||||
|
||||
@ -32,13 +36,17 @@ async fn write_app_config(app_config: &AppConfigV1, dir: Option<PathBuf>) -> any
|
||||
None => std::env::current_dir()?,
|
||||
};
|
||||
|
||||
tokio::fs::create_dir_all(&app_dir).await?;
|
||||
|
||||
let app_config_path = app_dir.join(AppConfigV1::CANONICAL_FILE_NAME);
|
||||
std::fs::write(&app_config_path, raw_app_config).with_context(|| {
|
||||
format!(
|
||||
"could not write app config to '{}'",
|
||||
app_config_path.display()
|
||||
)
|
||||
})
|
||||
tokio::fs::write(&app_config_path, raw_app_config)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"could not write app config to '{}'",
|
||||
app_config_path.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new Edge app.
|
||||
@ -104,10 +112,6 @@ pub struct CmdAppCreate {
|
||||
pub no_wait: bool,
|
||||
|
||||
// Common args.
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub api: ApiOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub env: WasmerEnv,
|
||||
|
||||
@ -141,6 +145,8 @@ impl CmdAppCreate {
|
||||
health_checks: None,
|
||||
debug: None,
|
||||
scaling: None,
|
||||
locality: None,
|
||||
redirect: None,
|
||||
extra: HashMap::new(),
|
||||
}
|
||||
}
|
||||
@ -191,6 +197,34 @@ impl CmdAppCreate {
|
||||
crate::utils::prompts::prompt_for_namespace("Who should own this app?", None, user.as_ref())
|
||||
}
|
||||
|
||||
async fn get_output_dir(&self, app_name: &str) -> anyhow::Result<PathBuf> {
|
||||
let mut output_path = if let Some(path) = &self.app_dir_path {
|
||||
path.clone()
|
||||
} else {
|
||||
PathBuf::from(".").canonicalize()?
|
||||
};
|
||||
|
||||
if output_path.is_dir() && output_path.read_dir()?.next().is_some() {
|
||||
if self.non_interactive {
|
||||
if !self.quiet {
|
||||
eprintln!("The current directory is not empty.");
|
||||
eprintln!("Use the `--dir` flag to specify another directory, or remove files from the currently selected one.")
|
||||
}
|
||||
anyhow::bail!("Stopping as the directory is not empty")
|
||||
} else {
|
||||
let theme = ColorfulTheme::default();
|
||||
let raw: String = dialoguer::Input::with_theme(&theme)
|
||||
.with_prompt("Select the directory to save the app in")
|
||||
.with_initial_text(app_name)
|
||||
.interact_text()
|
||||
.context("could not read user input")?;
|
||||
output_path = PathBuf::from_str(&raw)?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
async fn create_from_local_manifest(
|
||||
&self,
|
||||
owner: &str,
|
||||
@ -230,7 +264,7 @@ impl CmdAppCreate {
|
||||
if self.use_local_manifest || ask_confirmation()? {
|
||||
let app_config = self.get_app_config(owner, app_name, ".");
|
||||
write_app_config(&app_config, self.app_dir_path.clone()).await?;
|
||||
self.try_deploy(owner, app_name).await?;
|
||||
self.try_deploy(owner, app_name, None).await?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@ -247,10 +281,12 @@ impl CmdAppCreate {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let output_path = self.get_output_dir(app_name).await?;
|
||||
|
||||
if let Some(pkg) = &self.package {
|
||||
let app_config = self.get_app_config(owner, app_name, pkg);
|
||||
write_app_config(&app_config, self.app_dir_path.clone()).await?;
|
||||
self.try_deploy(owner, app_name).await?;
|
||||
write_app_config(&app_config, Some(output_path.clone())).await?;
|
||||
self.try_deploy(owner, app_name, Some(&output_path)).await?;
|
||||
return Ok(true);
|
||||
} else if !self.non_interactive {
|
||||
let (package_id, _) = crate::utils::prompts::prompt_for_package(
|
||||
@ -266,8 +302,8 @@ impl CmdAppCreate {
|
||||
.await?;
|
||||
|
||||
let app_config = self.get_app_config(owner, app_name, &package_id.to_string());
|
||||
write_app_config(&app_config, self.app_dir_path.clone()).await?;
|
||||
self.try_deploy(owner, app_name).await?;
|
||||
write_app_config(&app_config, Some(output_path.clone())).await?;
|
||||
self.try_deploy(owner, app_name, Some(&output_path)).await?;
|
||||
return Ok(true);
|
||||
} else {
|
||||
eprintln!(
|
||||
@ -279,31 +315,12 @@ impl CmdAppCreate {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Load cached templates from a file.
|
||||
///
|
||||
/// Returns an error if the cache file is older than the max age.
|
||||
fn load_cached_templates(
|
||||
path: &Path,
|
||||
) -> Result<(Vec<AppTemplate>, std::time::Duration), anyhow::Error> {
|
||||
let modified = path.metadata()?.modified()?;
|
||||
let age = modified.elapsed()?;
|
||||
|
||||
let data = std::fs::read_to_string(path)?;
|
||||
match serde_json::from_str::<Vec<AppTemplate>>(&data) {
|
||||
Ok(v) => Ok((v, age)),
|
||||
Err(err) => {
|
||||
std::fs::remove_file(path).ok();
|
||||
Err(err).context("could not deserialize cached file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn persist_template_cache(path: &Path, templates: &[AppTemplate]) -> Result<(), anyhow::Error> {
|
||||
fn persist_in_cache<S: serde::Serialize>(path: &Path, data: &S) -> Result<(), anyhow::Error> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).context("could not create cache dir")?;
|
||||
}
|
||||
|
||||
let data = serde_json::to_vec(templates)?;
|
||||
let data = serde_json::to_vec(data)?;
|
||||
|
||||
std::fs::write(path, data)?;
|
||||
tracing::trace!(path=%path.display(), "persisted app template cache");
|
||||
@ -317,14 +334,15 @@ impl CmdAppCreate {
|
||||
async fn fetch_templates_cached(
|
||||
client: &WasmerClient,
|
||||
cache_dir: &Path,
|
||||
language: &str,
|
||||
) -> Result<Vec<AppTemplate>, anyhow::Error> {
|
||||
const MAX_CACHE_AGE: Duration = Duration::from_secs(60 * 60);
|
||||
const MAX_COUNT: usize = 100;
|
||||
const CACHE_FILENAME: &str = "app_templates.json";
|
||||
let cache_filename = format!("app_templates_{language}.json");
|
||||
|
||||
let cache_path = cache_dir.join(CACHE_FILENAME);
|
||||
let cache_path = cache_dir.join(cache_filename);
|
||||
|
||||
let cached_items = match Self::load_cached_templates(&cache_path) {
|
||||
let cached_items = match Self::load_cached::<Vec<AppTemplate>>(&cache_path) {
|
||||
Ok((items, age)) => {
|
||||
if age <= MAX_CACHE_AGE {
|
||||
return Ok(items);
|
||||
@ -340,10 +358,96 @@ impl CmdAppCreate {
|
||||
// Either no cache present, or cache has exceeded max age.
|
||||
// Fetch the first page.
|
||||
// If first item matches, then no need to re-fetch.
|
||||
let mut stream = Box::pin(wasmer_api::query::fetch_all_app_templates(
|
||||
//
|
||||
let stream = wasmer_api::query::fetch_all_app_templates_from_language(
|
||||
client,
|
||||
10,
|
||||
Some(wasmer_api::types::AppTemplatesSortBy::Newest),
|
||||
language.to_string(),
|
||||
);
|
||||
|
||||
futures_util::pin_mut!(stream);
|
||||
|
||||
let first_page = match stream.try_next().await? {
|
||||
Some(items) => items,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
if let (Some(a), Some(b)) = (cached_items.first(), first_page.first()) {
|
||||
if a == b {
|
||||
// Cached items are up to date, no need to query more.
|
||||
return Ok(cached_items);
|
||||
}
|
||||
}
|
||||
|
||||
let mut items = first_page;
|
||||
while let Some(next) = stream.try_next().await? {
|
||||
items.extend(next);
|
||||
|
||||
if items.len() >= MAX_COUNT {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Persist to cache.
|
||||
if let Err(err) = Self::persist_in_cache(&cache_path, &items) {
|
||||
tracing::trace!(error = &*err, "could not persist template cache");
|
||||
}
|
||||
|
||||
// TODO: sort items by popularity!
|
||||
// Since we can't rely on backend sorting because of the cache
|
||||
// preservation logic, the backend needs to add a popluarity field.
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// Load cached data from a file.
|
||||
///
|
||||
/// Returns an error if the cache file is older than the max age.
|
||||
fn load_cached<D: serde::de::DeserializeOwned>(
|
||||
path: &Path,
|
||||
) -> Result<(D, std::time::Duration), anyhow::Error> {
|
||||
let modified = path.metadata()?.modified()?;
|
||||
let age = modified.elapsed()?;
|
||||
|
||||
let data = std::fs::read_to_string(path)?;
|
||||
match serde_json::from_str::<D>(data.as_str()) {
|
||||
Ok(v) => Ok((v, age)),
|
||||
Err(err) => {
|
||||
std::fs::remove_file(path).ok();
|
||||
Err(err).context("could not deserialize cached file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_template_languages_cached(
|
||||
client: &WasmerClient,
|
||||
cache_dir: &Path,
|
||||
) -> anyhow::Result<Vec<TemplateLanguage>> {
|
||||
const MAX_CACHE_AGE: Duration = Duration::from_secs(60 * 60);
|
||||
const MAX_COUNT: usize = 100;
|
||||
const CACHE_FILENAME: &str = "app_languages.json";
|
||||
|
||||
let cache_path = cache_dir.join(CACHE_FILENAME);
|
||||
|
||||
let cached_items = match Self::load_cached::<Vec<TemplateLanguage>>(&cache_path) {
|
||||
Ok((items, age)) => {
|
||||
if age <= MAX_CACHE_AGE {
|
||||
return Ok(items);
|
||||
}
|
||||
items
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::trace!(error = &*e, "could not load templates from local cache");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
//
|
||||
// Either no cache present, or cache has exceeded max age.
|
||||
// Fetch the first page.
|
||||
// If first item matches, then no need to re-fetch.
|
||||
let mut stream = Box::pin(wasmer_api::query::fetch_all_app_template_languages(
|
||||
client, None,
|
||||
));
|
||||
|
||||
let first_page = match stream.try_next().await? {
|
||||
@ -368,7 +472,7 @@ impl CmdAppCreate {
|
||||
}
|
||||
|
||||
// Persist to cache.
|
||||
if let Err(err) = Self::persist_template_cache(&cache_path, &items) {
|
||||
if let Err(err) = Self::persist_in_cache(&cache_path, &items) {
|
||||
tracing::trace!(error = &*err, "could not persist template cache");
|
||||
}
|
||||
|
||||
@ -396,29 +500,50 @@ impl CmdAppCreate {
|
||||
anyhow::bail!("No template selected")
|
||||
}
|
||||
|
||||
let templates = Self::fetch_templates_cached(client, &self.env.cache_dir).await?;
|
||||
|
||||
let theme = ColorfulTheme::default();
|
||||
let registry = self
|
||||
.env
|
||||
.registry_public_url()?
|
||||
.host_str()
|
||||
.unwrap_or("unknown_registry")
|
||||
.replace('.', "_");
|
||||
let cache_dir = self.env.cache_dir().join("templates").join(registry);
|
||||
|
||||
let languages = Self::fetch_template_languages_cached(client, &cache_dir).await?;
|
||||
|
||||
let items = languages.iter().map(|t| t.name.clone()).collect::<Vec<_>>();
|
||||
|
||||
// Note: this should really use `dialoger::FuzzySelect`, but that
|
||||
// breaks the formatting.
|
||||
let dialog = dialoguer::Select::with_theme(&theme)
|
||||
.with_prompt(format!("Select a language ({} available)", items.len()))
|
||||
.items(&items)
|
||||
.max_length(10)
|
||||
.clear(true)
|
||||
.report(true)
|
||||
.default(0);
|
||||
|
||||
let selection = dialog.interact()?;
|
||||
|
||||
let selected_language = languages
|
||||
.get(selection)
|
||||
.ok_or(anyhow::anyhow!("Invalid selection!"))?;
|
||||
|
||||
let templates =
|
||||
Self::fetch_templates_cached(client, &cache_dir, &selected_language.slug).await?;
|
||||
|
||||
let items = templates
|
||||
.iter()
|
||||
.map(|t| {
|
||||
format!(
|
||||
"{}{}\n {} {}",
|
||||
"{} - {} {}",
|
||||
t.name.bold(),
|
||||
if t.language.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {}", t.language.dimmed())
|
||||
},
|
||||
"demo:".bold().dimmed(),
|
||||
t.demo_url.dimmed()
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Note: this should really use `dialoger::FuzzySelect`, but that
|
||||
// breaks the formatting.
|
||||
let dialog = dialoguer::Select::with_theme(&theme)
|
||||
.with_prompt(format!("Select a template ({} available)", items.len()))
|
||||
.items(&items)
|
||||
@ -435,7 +560,7 @@ impl CmdAppCreate {
|
||||
|
||||
if !self.quiet {
|
||||
eprintln!(
|
||||
"{} {} {} {} ({} {})",
|
||||
"{} {} {} {} - {} {}",
|
||||
"✔".green().bold(),
|
||||
"Selected template".bold(),
|
||||
"·".dimmed(),
|
||||
@ -474,20 +599,7 @@ impl CmdAppCreate {
|
||||
|
||||
tracing::info!("Downloading template from url {url}");
|
||||
|
||||
let output_path = if let Some(path) = &self.app_dir_path {
|
||||
path.clone()
|
||||
} else {
|
||||
PathBuf::from(".").canonicalize()?
|
||||
};
|
||||
|
||||
if output_path.is_dir() && output_path.read_dir()?.next().is_some() {
|
||||
if !self.quiet {
|
||||
eprintln!("The current directory is not empty.");
|
||||
eprintln!("Use the `--dir` flag to specify another directory, or remove files from the currently selected one.")
|
||||
}
|
||||
anyhow::bail!("Stopping as the directory is not empty")
|
||||
}
|
||||
|
||||
let output_path = self.get_output_dir(app_name).await?;
|
||||
let pb = indicatif::ProgressBar::new_spinner();
|
||||
|
||||
pb.enable_steady_tick(std::time::Duration::from_millis(500));
|
||||
@ -598,13 +710,18 @@ the app:\n"
|
||||
format!("{bin_name} deploy").bold()
|
||||
)
|
||||
} else {
|
||||
self.try_deploy(owner, app_name).await?;
|
||||
self.try_deploy(owner, app_name, Some(&output_path)).await?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn try_deploy(&self, owner: &str, app_name: &str) -> anyhow::Result<()> {
|
||||
async fn try_deploy(
|
||||
&self,
|
||||
owner: &str,
|
||||
app_name: &str,
|
||||
path: Option<&Path>,
|
||||
) -> anyhow::Result<()> {
|
||||
let interactive = !self.non_interactive;
|
||||
let theme = dialoguer::theme::ColorfulTheme::default();
|
||||
|
||||
@ -616,7 +733,6 @@ the app:\n"
|
||||
{
|
||||
let cmd_deploy = CmdAppDeploy {
|
||||
quiet: false,
|
||||
api: self.api.clone(),
|
||||
env: self.env.clone(),
|
||||
fmt: ItemFormatOpts {
|
||||
format: self.fmt.format,
|
||||
@ -625,6 +741,7 @@ the app:\n"
|
||||
non_interactive: self.non_interactive,
|
||||
publish_package: true,
|
||||
dir: self.app_dir_path.clone(),
|
||||
path: path.map(|v| v.to_path_buf()),
|
||||
no_wait: self.no_wait,
|
||||
no_default: false,
|
||||
no_persist_id: false,
|
||||
@ -652,7 +769,6 @@ impl AsyncCliCommand for CmdAppCreate {
|
||||
} else {
|
||||
Some(
|
||||
login_user(
|
||||
&self.api,
|
||||
&self.env,
|
||||
!self.non_interactive,
|
||||
"retrieve informations about the owner of the app",
|
||||
|
@ -4,13 +4,13 @@ use dialoguer::Confirm;
|
||||
use is_terminal::IsTerminal;
|
||||
|
||||
use super::util::AppIdentOpts;
|
||||
use crate::{commands::AsyncCliCommand, opts::ApiOpts};
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv};
|
||||
|
||||
/// Delete an existing Edge app
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppDelete {
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
|
||||
#[clap(long, default_value_t = !std::io::stdin().is_terminal())]
|
||||
non_interactive: bool,
|
||||
@ -25,7 +25,7 @@ impl AsyncCliCommand for CmdAppDelete {
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let interactive = !self.non_interactive;
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
|
||||
eprintln!("Looking up the app...");
|
||||
let (_ident, app) = self.ident.load_app(&client).await?;
|
||||
|
@ -1,7 +1,8 @@
|
||||
use super::{util::login_user, AsyncCliCommand};
|
||||
use crate::{
|
||||
commands::{app::create::CmdAppCreate, package::publish::PackagePublish, PublishWait},
|
||||
opts::{ApiOpts, ItemFormatOpts, WasmerEnv},
|
||||
config::WasmerEnv,
|
||||
opts::ItemFormatOpts,
|
||||
utils::load_package_manifest,
|
||||
};
|
||||
use anyhow::Context;
|
||||
@ -19,12 +20,14 @@ use wasmer_config::{
|
||||
package::{PackageIdent, PackageSource},
|
||||
};
|
||||
|
||||
// TODO: apparently edge-util uses a different version of the http crate, which makes the
|
||||
// HEADER_APP_VERSION_ID field incompatible with our use here, so it needs to be redeclared.
|
||||
static EDGE_HEADER_APP_VERSION_ID: http::HeaderName =
|
||||
http::HeaderName::from_static("x-edge-app-version-id");
|
||||
|
||||
/// Deploy an app to Wasmer Edge.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppDeploy {
|
||||
#[clap(flatten)]
|
||||
pub api: ApiOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub env: WasmerEnv,
|
||||
|
||||
@ -49,6 +52,10 @@ pub struct CmdAppDeploy {
|
||||
#[clap(long)]
|
||||
pub dir: Option<PathBuf>,
|
||||
|
||||
/// The path to the `app.yaml` file.
|
||||
#[clap(long, conflicts_with = "dir")]
|
||||
pub path: Option<PathBuf>,
|
||||
|
||||
/// Do not wait for the app to become reachable.
|
||||
#[clap(long)]
|
||||
pub no_wait: bool,
|
||||
@ -147,7 +154,6 @@ impl CmdAppDeploy {
|
||||
package_namespace: Some(owner),
|
||||
non_interactive: self.non_interactive,
|
||||
bump: self.bump,
|
||||
api: self.api.clone(),
|
||||
};
|
||||
|
||||
publish_cmd
|
||||
@ -158,19 +164,24 @@ impl CmdAppDeploy {
|
||||
async fn get_owner(
|
||||
&self,
|
||||
client: &WasmerClient,
|
||||
app: &serde_yaml::Value,
|
||||
app_config_path: &PathBuf,
|
||||
) -> anyhow::Result<(String, String)> {
|
||||
let r_ret = serde_yaml::to_string(&app)?;
|
||||
|
||||
app: &mut serde_yaml::Value,
|
||||
maybe_edge_app: Option<&DeployApp>,
|
||||
) -> anyhow::Result<String> {
|
||||
if let Some(owner) = &self.owner {
|
||||
return Ok((owner.clone(), r_ret));
|
||||
return Ok(owner.clone());
|
||||
}
|
||||
|
||||
if let Some(serde_yaml::Value::String(owner)) = &app.get("owner") {
|
||||
return Ok((owner.clone(), r_ret));
|
||||
return Ok(owner.clone());
|
||||
}
|
||||
|
||||
if let Some(edge_app) = maybe_edge_app {
|
||||
app.as_mapping_mut()
|
||||
.unwrap()
|
||||
.insert("owner".into(), edge_app.owner.global_name.clone().into());
|
||||
return Ok(edge_app.owner.global_name.clone());
|
||||
};
|
||||
|
||||
if self.non_interactive {
|
||||
// if not interactive we can't prompt the user to choose the owner of the app.
|
||||
anyhow::bail!("No owner specified: use --owner XXX");
|
||||
@ -183,12 +194,11 @@ impl CmdAppDeploy {
|
||||
Some(&user),
|
||||
)?;
|
||||
|
||||
let new_raw_config = format!("owner: {owner}\n{r_ret}");
|
||||
app.as_mapping_mut()
|
||||
.unwrap()
|
||||
.insert("owner".into(), owner.clone().into());
|
||||
|
||||
std::fs::write(app_config_path, &new_raw_config)
|
||||
.with_context(|| format!("Could not write file: '{}'", app_config_path.display()))?;
|
||||
|
||||
Ok((owner.clone(), new_raw_config))
|
||||
Ok(owner.clone())
|
||||
}
|
||||
async fn create(&self) -> anyhow::Result<()> {
|
||||
eprintln!("It seems you are trying to create a new app!");
|
||||
@ -202,7 +212,6 @@ impl CmdAppDeploy {
|
||||
owner: self.owner.clone(),
|
||||
app_name: self.app_name.clone(),
|
||||
no_wait: self.no_wait,
|
||||
api: self.api.clone(),
|
||||
env: self.env.clone(),
|
||||
fmt: ItemFormatOpts {
|
||||
format: self.fmt.format,
|
||||
@ -223,10 +232,14 @@ impl AsyncCliCommand for CmdAppDeploy {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
let client =
|
||||
login_user(&self.api, &self.env, !self.non_interactive, "deploy an app").await?;
|
||||
let client = login_user(&self.env, !self.non_interactive, "deploy an app").await?;
|
||||
|
||||
let base_dir_path = self.dir.clone().unwrap_or_else(|| {
|
||||
self.path
|
||||
.clone()
|
||||
.unwrap_or_else(|| std::env::current_dir().unwrap())
|
||||
});
|
||||
|
||||
let base_dir_path = self.dir.clone().unwrap_or(std::env::current_dir()?);
|
||||
let (app_config_path, base_dir_path) = {
|
||||
if base_dir_path.is_file() {
|
||||
(
|
||||
@ -247,8 +260,12 @@ impl AsyncCliCommand for CmdAppDeploy {
|
||||
|| self.package.is_some()
|
||||
|| self.use_local_manifest
|
||||
{
|
||||
// Create already points back to deploy.
|
||||
return self.create().await;
|
||||
if !self.non_interactive {
|
||||
// Create already points back to deploy.
|
||||
return self.create().await;
|
||||
} else {
|
||||
anyhow::bail!("No app configuration was found in {}. Create an app before deploying or re-run in interactive mode!", app_config_path.display());
|
||||
}
|
||||
}
|
||||
|
||||
assert!(app_config_path.is_file());
|
||||
@ -257,14 +274,60 @@ impl AsyncCliCommand for CmdAppDeploy {
|
||||
.with_context(|| format!("Could not read file '{}'", &app_config_path.display()))?;
|
||||
|
||||
// We want to allow the user to specify the app name interactively.
|
||||
let app_yaml: serde_yaml::Value = serde_yaml::from_str(&config_str)?;
|
||||
let (owner, mut config_str) = self.get_owner(&client, &app_yaml, &app_config_path).await?;
|
||||
let mut app_yaml: serde_yaml::Value = serde_yaml::from_str(&config_str)?;
|
||||
let maybe_edge_app = if let Some(app_id) = app_yaml.get("app_id").and_then(|s| s.as_str()) {
|
||||
wasmer_api::query::get_app_by_id(&client, app_id.to_owned())
|
||||
.await
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// We want to allow the user to specify the app name interactively.
|
||||
let app_yaml: serde_yaml::Value = serde_yaml::from_str(&config_str)?;
|
||||
let mut owner = self
|
||||
.get_owner(&client, &mut app_yaml, maybe_edge_app.as_ref())
|
||||
.await?;
|
||||
|
||||
if !wasmer_api::query::viewer_can_deploy_to_namespace(&client, &owner).await? {
|
||||
eprintln!("It seems you don't have access to {}", owner.bold());
|
||||
if self.non_interactive {
|
||||
anyhow::bail!("Please, change the owner before deploying or check your current user with `{} whoami`.", std::env::args().next().unwrap_or("wasmer".into()));
|
||||
} else {
|
||||
let user = wasmer_api::query::current_user_with_namespaces(&client, None).await?;
|
||||
owner = crate::utils::prompts::prompt_for_namespace(
|
||||
"Who should own this app?",
|
||||
None,
|
||||
Some(&user),
|
||||
)?;
|
||||
|
||||
app_yaml
|
||||
.as_mapping_mut()
|
||||
.unwrap()
|
||||
.insert("owner".into(), owner.clone().into());
|
||||
|
||||
if app_yaml.get("app_id").is_some() {
|
||||
app_yaml.as_mapping_mut().unwrap().remove("app_id");
|
||||
}
|
||||
|
||||
if app_yaml.get("name").is_some() {
|
||||
app_yaml.as_mapping_mut().unwrap().remove("name");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if app_yaml.get("name").is_none() && self.app_name.is_some() {
|
||||
config_str = format!("{}\nname: {}", config_str, self.app_name.as_ref().unwrap());
|
||||
app_yaml.as_mapping_mut().unwrap().insert(
|
||||
"name".into(),
|
||||
self.app_name.as_ref().unwrap().to_string().into(),
|
||||
);
|
||||
} else if app_yaml.get("name").is_none() && maybe_edge_app.is_some() {
|
||||
app_yaml.as_mapping_mut().unwrap().insert(
|
||||
"name".into(),
|
||||
maybe_edge_app
|
||||
.as_ref()
|
||||
.map(|v| v.name.to_string())
|
||||
.unwrap()
|
||||
.into(),
|
||||
);
|
||||
} else if app_yaml.get("name").is_none() {
|
||||
if !self.non_interactive {
|
||||
let default_name = std::env::current_dir().ok().and_then(|dir| {
|
||||
@ -276,18 +339,14 @@ impl AsyncCliCommand for CmdAppDeploy {
|
||||
"Enter the name of the app",
|
||||
default_name.as_deref(),
|
||||
&owner,
|
||||
self.api.client().ok().as_ref(),
|
||||
self.env.client().ok().as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
std::fs::write(
|
||||
&app_config_path,
|
||||
format!("{}name: {}", config_str, app_name),
|
||||
)?;
|
||||
|
||||
config_str = std::fs::read_to_string(&app_config_path).with_context(|| {
|
||||
format!("Could not read file '{}'", &app_config_path.display())
|
||||
})?;
|
||||
app_yaml
|
||||
.as_mapping_mut()
|
||||
.unwrap()
|
||||
.insert("name".into(), app_name.into());
|
||||
} else {
|
||||
if !self.quiet {
|
||||
eprintln!("The app.yaml does not specify any app name.");
|
||||
@ -304,7 +363,13 @@ impl AsyncCliCommand for CmdAppDeploy {
|
||||
}
|
||||
}
|
||||
|
||||
let original_app_config: AppConfigV1 = AppConfigV1::parse_yaml(&config_str)?;
|
||||
let original_app_config: AppConfigV1 = serde_yaml::from_value(app_yaml.clone())?;
|
||||
std::fs::write(
|
||||
&app_config_path,
|
||||
serde_yaml::to_string(&original_app_config)?,
|
||||
)
|
||||
.with_context(|| format!("Could not write file: '{}'", app_config_path.display()))?;
|
||||
|
||||
let mut app_config = original_app_config.clone();
|
||||
|
||||
app_config.owner = Some(owner.clone());
|
||||
@ -508,7 +573,7 @@ impl AsyncCliCommand for CmdAppDeploy {
|
||||
// If the config changed, write it back.
|
||||
if new_app_config != app_config {
|
||||
// We want to preserve unknown fields to allow for newer app.yaml
|
||||
// settings without requring new CLI versions, so instead of just
|
||||
// settings without requiring new CLI versions, so instead of just
|
||||
// serializing the new config, we merge it with the old one.
|
||||
let new_merged = crate::utils::merge_yaml_values(
|
||||
&app_config.clone().to_yaml_value()?,
|
||||
@ -631,7 +696,13 @@ pub async fn wait_app(
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
let start = tokio::time::Instant::now();
|
||||
let client = reqwest::Client::new();
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.timeout(Duration::from_secs(90))
|
||||
// Should not follow redirects.
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let check_url = if make_default { &app.url } else { &version.url };
|
||||
|
||||
@ -656,19 +727,37 @@ pub async fn wait_app(
|
||||
|
||||
let request_start = tokio::time::Instant::now();
|
||||
|
||||
tracing::debug!(%check_url, "checking health of app");
|
||||
match client.get(check_url).send().await {
|
||||
Ok(res) => {
|
||||
let header = res
|
||||
.headers()
|
||||
.get(edge_util::headers::HEADER_APP_VERSION_ID)
|
||||
.get(&EDGE_HEADER_APP_VERSION_ID)
|
||||
.and_then(|x| x.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
tracing::debug!(
|
||||
%check_url,
|
||||
status=res.status().as_u16(),
|
||||
app_version_header=%header,
|
||||
"app request response received",
|
||||
);
|
||||
|
||||
if header == version.id.inner() {
|
||||
if !quiet {
|
||||
eprintln!();
|
||||
}
|
||||
eprintln!("{} Deployment complete", "𖥔".yellow().bold());
|
||||
if !(res.status().is_success() || res.status().is_redirection()) {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!(
|
||||
"The app version was deployed correctly, but fails with a non-success status code of {}",
|
||||
res.status()).yellow()
|
||||
);
|
||||
} else {
|
||||
eprintln!("{} Deployment complete", "𖥔".yellow().bold());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@ -683,6 +772,8 @@ pub async fn wait_app(
|
||||
}
|
||||
};
|
||||
|
||||
// Increase the sleep time between requests, up
|
||||
// to a reasonable maximum.
|
||||
let elapsed: u64 = request_start
|
||||
.elapsed()
|
||||
.as_millis()
|
||||
|
@ -3,23 +3,19 @@
|
||||
use wasmer_api::types::DeployApp;
|
||||
|
||||
use super::util::AppIdentOpts;
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
opts::{ApiOpts, ItemFormatOpts},
|
||||
};
|
||||
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ItemFormatOpts};
|
||||
|
||||
/// Retrieve detailed informations about an app
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppGet {
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub api: ApiOpts,
|
||||
pub env: WasmerEnv,
|
||||
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub fmt: ItemFormatOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub ident: AppIdentOpts,
|
||||
}
|
||||
|
||||
@ -28,7 +24,7 @@ impl AsyncCliCommand for CmdAppGet {
|
||||
type Output = DeployApp;
|
||||
|
||||
async fn run_async(self) -> Result<DeployApp, anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
let (_ident, app) = self.ident.load_app(&client).await?;
|
||||
|
||||
println!("{}", self.fmt.format.render(&app));
|
||||
|
@ -1,7 +1,7 @@
|
||||
//! Show short information about an Edge app.
|
||||
|
||||
use super::util::AppIdentOpts;
|
||||
use crate::{commands::AsyncCliCommand, opts::ApiOpts};
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv};
|
||||
|
||||
/// Show short information about an Edge app.
|
||||
///
|
||||
@ -9,7 +9,7 @@ use crate::{commands::AsyncCliCommand, opts::ApiOpts};
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppInfo {
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
#[clap(flatten)]
|
||||
ident: AppIdentOpts,
|
||||
}
|
||||
@ -19,7 +19,7 @@ impl AsyncCliCommand for CmdAppInfo {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
let (_ident, app) = self.ident.load_app(&client).await?;
|
||||
|
||||
let app_url = app.url;
|
||||
|
@ -3,20 +3,18 @@
|
||||
use std::pin::Pin;
|
||||
|
||||
use futures::{Stream, StreamExt};
|
||||
use wasmer_api::types::DeployApp;
|
||||
use wasmer_api::types::{DeployApp, DeployAppsSortBy};
|
||||
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
opts::{ApiOpts, ListFormatOpts},
|
||||
};
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ListFormatOpts};
|
||||
|
||||
/// List apps belonging to a namespace
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppList {
|
||||
#[clap(flatten)]
|
||||
fmt: ListFormatOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
|
||||
/// Get apps in a specific namespace.
|
||||
///
|
||||
@ -36,6 +34,19 @@ pub struct CmdAppList {
|
||||
/// Asks whether to display the next page or not
|
||||
#[clap(long, default_value = "false")]
|
||||
paging_mode: bool,
|
||||
|
||||
/// Sort order for apps.
|
||||
///
|
||||
/// Use: --newest, --oldest or --last-updated
|
||||
#[clap(long, default_value = "last-updated")]
|
||||
sort: AppSort,
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug)]
|
||||
pub enum AppSort {
|
||||
Newest,
|
||||
Oldest,
|
||||
LastUpdated,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@ -43,16 +54,22 @@ impl AsyncCliCommand for CmdAppList {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
|
||||
let sort = match self.sort {
|
||||
AppSort::Newest => DeployAppsSortBy::Newest,
|
||||
AppSort::Oldest => DeployAppsSortBy::Oldest,
|
||||
AppSort::LastUpdated => DeployAppsSortBy::MostActive,
|
||||
};
|
||||
|
||||
let apps_stream: Pin<
|
||||
Box<dyn Stream<Item = Result<Vec<DeployApp>, anyhow::Error>> + Send + Sync>,
|
||||
> = if let Some(ns) = self.namespace.clone() {
|
||||
Box::pin(wasmer_api::query::namespace_apps(&client, ns).await)
|
||||
Box::pin(wasmer_api::query::namespace_apps(&client, ns, sort).await)
|
||||
} else if self.all {
|
||||
Box::pin(wasmer_api::query::user_accessible_apps(&client).await?)
|
||||
Box::pin(wasmer_api::query::user_accessible_apps(&client, sort).await?)
|
||||
} else {
|
||||
Box::pin(wasmer_api::query::user_apps(&client).await)
|
||||
Box::pin(wasmer_api::query::user_apps(&client, sort).await)
|
||||
};
|
||||
|
||||
let mut apps_stream = std::pin::pin!(apps_stream);
|
||||
|
@ -7,10 +7,7 @@ use futures::StreamExt;
|
||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||
use wasmer_api::types::{Log, LogStream};
|
||||
|
||||
use crate::{
|
||||
opts::{ApiOpts, ListFormatOpts},
|
||||
utils::render::CliRender,
|
||||
};
|
||||
use crate::{config::WasmerEnv, opts::ListFormatOpts, utils::render::CliRender};
|
||||
|
||||
use super::util::AppIdentOpts;
|
||||
|
||||
@ -24,7 +21,8 @@ pub enum LogStreamArg {
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppLogs {
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
|
||||
#[clap(flatten)]
|
||||
fmt: ListFormatOpts,
|
||||
|
||||
@ -39,7 +37,7 @@ pub struct CmdAppLogs {
|
||||
/// * Unix timestamp (`1136196245`)
|
||||
/// * Relative time (`10m` / `-1h`, `1d1h30s`)
|
||||
// TODO: should default to trailing logs once trailing is implemented.
|
||||
#[clap(long, value_parser = parse_timestamp_or_relative_time)]
|
||||
#[clap(long, value_parser = parse_timestamp_or_relative_time, conflicts_with = "request_id")]
|
||||
from: Option<OffsetDateTime>,
|
||||
|
||||
/// The date of the latest log entry.
|
||||
@ -50,7 +48,7 @@ pub struct CmdAppLogs {
|
||||
/// * Simple date (`2022-11-11`)
|
||||
/// * Unix timestamp (`1136196245`)
|
||||
/// * Relative time (`10m` / `1h`, `1d1h30s`)
|
||||
#[clap(long, value_parser = parse_timestamp_or_relative_time)]
|
||||
#[clap(long, value_parser = parse_timestamp_or_relative_time, conflicts_with = "request_id")]
|
||||
until: Option<OffsetDateTime>,
|
||||
|
||||
/// Maximum log lines to fetch.
|
||||
@ -58,6 +56,7 @@ pub struct CmdAppLogs {
|
||||
#[clap(long, default_value = "1000")]
|
||||
max: usize,
|
||||
|
||||
/// Continuously watch for new logs and display them in real-time.
|
||||
#[clap(long, default_value = "false")]
|
||||
watch: bool,
|
||||
|
||||
@ -68,6 +67,14 @@ pub struct CmdAppLogs {
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub ident: AppIdentOpts,
|
||||
|
||||
/// The identifier of the request to show logs related to
|
||||
#[clap(long)]
|
||||
pub request_id: Option<String>,
|
||||
|
||||
/// The identifier of the app instance to show logs related to
|
||||
#[clap(long, conflicts_with = "request_id", value_delimiter = ' ', num_args = 1..)]
|
||||
pub instance_id: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@ -75,7 +82,7 @@ impl crate::commands::AsyncCliCommand for CmdAppLogs {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
|
||||
let (_ident, app) = self.ident.load_app(&client).await?;
|
||||
|
||||
@ -116,38 +123,110 @@ impl crate::commands::AsyncCliCommand for CmdAppLogs {
|
||||
(false, true) => &[LogStream::Stderr][..],
|
||||
});
|
||||
|
||||
let logs_stream = wasmer_api::query::get_app_logs_paginated(
|
||||
&client,
|
||||
app.name.clone(),
|
||||
app.owner.global_name.to_string(),
|
||||
None, // keep version None since we want logs from all versions atm
|
||||
from,
|
||||
self.until,
|
||||
self.watch,
|
||||
Some(streams),
|
||||
)
|
||||
.await;
|
||||
// Code duplication to avoid a dependency to `OR` streams.
|
||||
if let Some(instance_id) = &self.instance_id {
|
||||
let logs_stream = wasmer_api::query::get_app_logs_paginated_filter_instance(
|
||||
&client,
|
||||
app.name.clone(),
|
||||
app.owner.global_name.to_string(),
|
||||
None, // keep version None since we want logs from all versions atm
|
||||
from,
|
||||
self.until,
|
||||
self.watch,
|
||||
Some(streams),
|
||||
instance_id.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut logs_stream = std::pin::pin!(logs_stream);
|
||||
let mut logs_stream = std::pin::pin!(logs_stream);
|
||||
let mut rem = self.max;
|
||||
|
||||
let mut rem = self.max;
|
||||
while let Some(logs) = logs_stream.next().await {
|
||||
let mut logs = logs?;
|
||||
|
||||
while let Some(logs) = logs_stream.next().await {
|
||||
let mut logs = logs?;
|
||||
let limit = std::cmp::min(logs.len(), rem);
|
||||
|
||||
let limit = std::cmp::min(logs.len(), rem);
|
||||
let logs: Vec<_> = logs.drain(..limit).collect();
|
||||
|
||||
let logs: Vec<_> = logs.drain(..limit).collect();
|
||||
if !logs.is_empty() {
|
||||
let rendered = self.fmt.format.render(&logs);
|
||||
println!("{rendered}");
|
||||
|
||||
if !logs.is_empty() {
|
||||
let rendered = self.fmt.format.render(&logs);
|
||||
println!("{rendered}");
|
||||
rem -= limit;
|
||||
}
|
||||
|
||||
rem -= limit;
|
||||
if !self.watch || rem == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if let Some(request_id) = &self.request_id {
|
||||
let logs_stream = wasmer_api::query::get_app_logs_paginated_filter_request(
|
||||
&client,
|
||||
app.name.clone(),
|
||||
app.owner.global_name.to_string(),
|
||||
None, // keep version None since we want logs from all versions atm
|
||||
from,
|
||||
self.until,
|
||||
self.watch,
|
||||
Some(streams),
|
||||
request_id.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if !self.watch || rem == 0 {
|
||||
break;
|
||||
let mut logs_stream = std::pin::pin!(logs_stream);
|
||||
let mut rem = self.max;
|
||||
|
||||
while let Some(logs) = logs_stream.next().await {
|
||||
let mut logs = logs?;
|
||||
|
||||
let limit = std::cmp::min(logs.len(), rem);
|
||||
|
||||
let logs: Vec<_> = logs.drain(..limit).collect();
|
||||
|
||||
if !logs.is_empty() {
|
||||
let rendered = self.fmt.format.render(&logs);
|
||||
println!("{rendered}");
|
||||
|
||||
rem -= limit;
|
||||
}
|
||||
|
||||
if !self.watch || rem == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let logs_stream = wasmer_api::query::get_app_logs_paginated(
|
||||
&client,
|
||||
app.name.clone(),
|
||||
app.owner.global_name.to_string(),
|
||||
None, // keep version None since we want logs from all versions atm
|
||||
from,
|
||||
self.until,
|
||||
self.watch,
|
||||
Some(streams),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut logs_stream = std::pin::pin!(logs_stream);
|
||||
let mut rem = self.max;
|
||||
|
||||
while let Some(logs) = logs_stream.next().await {
|
||||
let mut logs = logs?;
|
||||
|
||||
let limit = std::cmp::min(logs.len(), rem);
|
||||
|
||||
let logs: Vec<_> = logs.drain(..limit).collect();
|
||||
|
||||
if !logs.is_empty() {
|
||||
let rendered = self.fmt.format.render(&logs);
|
||||
println!("{rendered}");
|
||||
|
||||
rem -= limit;
|
||||
}
|
||||
|
||||
if !self.watch || rem == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,10 @@ pub mod info;
|
||||
pub mod list;
|
||||
pub mod logs;
|
||||
pub mod purge_cache;
|
||||
pub mod regions;
|
||||
pub mod secrets;
|
||||
pub mod version;
|
||||
pub mod volumes;
|
||||
|
||||
mod util;
|
||||
|
||||
@ -27,6 +30,12 @@ pub enum CmdApp {
|
||||
Delete(delete::CmdAppDelete),
|
||||
#[clap(subcommand)]
|
||||
Version(version::CmdAppVersion),
|
||||
#[clap(subcommand, alias = "secrets")]
|
||||
Secret(secrets::CmdAppSecrets),
|
||||
#[clap(subcommand, alias = "regions")]
|
||||
Region(regions::CmdAppRegions),
|
||||
#[clap(subcommand, alias = "volumes")]
|
||||
Volume(volumes::CmdAppVolumes),
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@ -53,6 +62,9 @@ impl AsyncCliCommand for CmdApp {
|
||||
Self::Version(cmd) => cmd.run_async().await,
|
||||
Self::Deploy(cmd) => cmd.run_async().await,
|
||||
Self::PurgeCache(cmd) => cmd.run_async().await,
|
||||
Self::Secret(cmd) => cmd.run_async().await,
|
||||
Self::Region(cmd) => cmd.run_async().await,
|
||||
Self::Volume(cmd) => cmd.run_async().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,7 @@
|
||||
//! Get information about an edge app.
|
||||
|
||||
use super::util::AppIdentOpts;
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
opts::{ApiOpts, ItemFormatOpts},
|
||||
};
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ItemFormatOpts};
|
||||
|
||||
/// Purge caches for applications.
|
||||
///
|
||||
@ -15,14 +12,12 @@ use crate::{
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppPurgeCache {
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub api: ApiOpts,
|
||||
pub env: WasmerEnv,
|
||||
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub fmt: ItemFormatOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub ident: AppIdentOpts,
|
||||
}
|
||||
|
||||
@ -31,7 +26,7 @@ impl AsyncCliCommand for CmdAppPurgeCache {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
let (_ident, app) = self.ident.load_app(&client).await?;
|
||||
|
||||
let version_id = app.active_version.id;
|
||||
|
35
lib/cli/src/commands/app/regions/list.rs
Normal file
35
lib/cli/src/commands/app/regions/list.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ListFormatOpts};
|
||||
use is_terminal::IsTerminal;
|
||||
|
||||
/// List available Edge regions.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppRegionsList {
|
||||
/* --- Common flags --- */
|
||||
#[clap(flatten)]
|
||||
pub env: WasmerEnv,
|
||||
|
||||
/// Don't print any message.
|
||||
#[clap(long)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Do not prompt for user input.
|
||||
#[clap(long, default_value_t = !std::io::stdin().is_terminal())]
|
||||
pub non_interactive: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub fmt: ListFormatOpts,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAppRegionsList {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
let client = self.env.client()?;
|
||||
let regions = wasmer_api::query::get_all_app_regions(&client).await?;
|
||||
|
||||
println!("{}", self.fmt.format.render(regions.as_slice()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
24
lib/cli/src/commands/app/regions/mod.rs
Normal file
24
lib/cli/src/commands/app/regions/mod.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use crate::commands::AsyncCliCommand;
|
||||
|
||||
pub mod list;
|
||||
mod utils;
|
||||
|
||||
/// Informations about available Edge regioins.
|
||||
#[derive(Debug, clap::Parser)]
|
||||
pub enum CmdAppRegions {
|
||||
List(list::CmdAppRegionsList),
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAppRegions {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
match self {
|
||||
Self::List(c) => {
|
||||
c.run_async().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
lib/cli/src/commands/app/regions/utils/mod.rs
Normal file
1
lib/cli/src/commands/app/regions/utils/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub(crate) mod render;
|
55
lib/cli/src/commands/app/regions/utils/render.rs
Normal file
55
lib/cli/src/commands/app/regions/utils/render.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use crate::utils::render::CliRender;
|
||||
use comfy_table::{Cell, Table};
|
||||
use wasmer_api::types::AppRegion;
|
||||
|
||||
impl CliRender for AppRegion {
|
||||
fn render_item_table(&self) -> String {
|
||||
let mut table = Table::new();
|
||||
let AppRegion {
|
||||
name,
|
||||
city,
|
||||
country,
|
||||
..
|
||||
}: &AppRegion = self;
|
||||
|
||||
table.load_preset(comfy_table::presets::NOTHING);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
|
||||
table.add_rows([
|
||||
vec![
|
||||
Cell::new("Name".to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
Cell::new("City".to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
Cell::new("Country code".to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
],
|
||||
vec![
|
||||
Cell::new(name.to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
Cell::new(city.to_string()),
|
||||
Cell::new(country.to_string()),
|
||||
],
|
||||
]);
|
||||
table.to_string()
|
||||
}
|
||||
|
||||
fn render_list_table(items: &[Self]) -> String {
|
||||
if items.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let mut table = Table::new();
|
||||
table.load_preset(comfy_table::presets::NOTHING);
|
||||
//table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
|
||||
table.set_header(vec![
|
||||
Cell::new("Name".to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
Cell::new("City".to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
Cell::new("Country code".to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
]);
|
||||
table.add_rows(items.iter().map(|s| {
|
||||
vec![
|
||||
Cell::new(s.name.to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
Cell::new(s.city.to_string()),
|
||||
Cell::new(s.country.to_string()),
|
||||
]
|
||||
}));
|
||||
table.to_string()
|
||||
}
|
||||
}
|
228
lib/cli/src/commands/app/secrets/create.rs
Normal file
228
lib/cli/src/commands/app/secrets/create.rs
Normal file
@ -0,0 +1,228 @@
|
||||
use super::utils::Secret;
|
||||
use crate::{
|
||||
commands::{app::util::AppIdentFlag, AsyncCliCommand},
|
||||
config::WasmerEnv,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use colored::Colorize;
|
||||
use dialoguer::theme::ColorfulTheme;
|
||||
use is_terminal::IsTerminal;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use wasmer_api::WasmerClient;
|
||||
|
||||
/// Create a new app secret.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppSecretsCreate {
|
||||
/* --- Common flags --- */
|
||||
#[clap(flatten)]
|
||||
pub env: WasmerEnv,
|
||||
|
||||
/// Don't print any message.
|
||||
#[clap(long)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Do not prompt for user input.
|
||||
#[clap(long, default_value_t = !std::io::stdin().is_terminal())]
|
||||
pub non_interactive: bool,
|
||||
|
||||
/* --- Flags --- */
|
||||
/// The path to the directory where the config file for the application will be written to.
|
||||
#[clap(long = "app-dir", conflicts_with = "app")]
|
||||
pub app_dir_path: Option<PathBuf>,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub app_id: AppIdentFlag,
|
||||
|
||||
/// Path to a file with secrets stored in JSON format to create secrets from.
|
||||
#[clap(long, name = "from-file", conflicts_with = "name")]
|
||||
pub from_file: Option<PathBuf>,
|
||||
|
||||
/// Whether or not to redeploy the app after creating the secrets.
|
||||
#[clap(long)]
|
||||
pub redeploy: bool,
|
||||
|
||||
/* --- Parameters --- */
|
||||
/// The name of the secret to create.
|
||||
#[clap(name = "name")]
|
||||
pub secret_name: Option<String>,
|
||||
|
||||
/// The value of the secret to create.
|
||||
#[clap(name = "value")]
|
||||
pub secret_value: Option<String>,
|
||||
}
|
||||
|
||||
impl CmdAppSecretsCreate {
|
||||
fn get_secret_name(&self) -> anyhow::Result<String> {
|
||||
if let Some(name) = &self.secret_name {
|
||||
return Ok(name.clone());
|
||||
}
|
||||
|
||||
if self.non_interactive {
|
||||
anyhow::bail!("No secret name given. Provide one as a positional argument.")
|
||||
} else {
|
||||
let theme = ColorfulTheme::default();
|
||||
Ok(dialoguer::Input::with_theme(&theme)
|
||||
.with_prompt("Enter the name of the secret")
|
||||
.interact_text()?)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_secret_value(&self) -> anyhow::Result<String> {
|
||||
if let Some(value) = &self.secret_value {
|
||||
return Ok(value.clone());
|
||||
}
|
||||
|
||||
if self.non_interactive {
|
||||
anyhow::bail!("No secret value given. Provide one as a positional argument.")
|
||||
} else {
|
||||
let theme = ColorfulTheme::default();
|
||||
Ok(dialoguer::Input::with_theme(&theme)
|
||||
.with_prompt("Enter the value of the secret")
|
||||
.interact_text()?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a list of secrets, checks if the given secrets already exist for the given app and
|
||||
/// returns a list of secrets that must be upserted.
|
||||
async fn filter_secrets(
|
||||
&self,
|
||||
client: &WasmerClient,
|
||||
app_id: &str,
|
||||
secrets: Vec<Secret>,
|
||||
) -> anyhow::Result<Vec<Secret>> {
|
||||
let names = secrets.iter().map(|s| &s.name);
|
||||
let app_secrets =
|
||||
wasmer_api::query::get_all_app_secrets_filtered(client, app_id, names).await?;
|
||||
let mut sset = HashSet::<String>::from_iter(app_secrets.iter().map(|s| s.name.clone()));
|
||||
let mut ret = HashMap::new();
|
||||
|
||||
for secret in secrets {
|
||||
if sset.contains(&secret.name) {
|
||||
if self.non_interactive {
|
||||
anyhow::bail!("Cannot create secret '{}' as it already exists. Use the `update` command instead.", secret.name.bold());
|
||||
} else {
|
||||
if ret.contains_key(&secret.name) {
|
||||
eprintln!(
|
||||
"Secret '{}' appears twice in the input file.",
|
||||
secret.name.bold()
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"Secret '{}' already exists for the selected app.",
|
||||
secret.name.bold()
|
||||
);
|
||||
}
|
||||
let theme = ColorfulTheme::default();
|
||||
let res = dialoguer::Confirm::with_theme(&theme)
|
||||
.with_prompt("Do you want to update it?")
|
||||
.interact()?;
|
||||
|
||||
if !res {
|
||||
eprintln!("Cannot create secret '{}' as it already exists. Use the `update` command instead.", secret.name.bold());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sset.insert(secret.name.clone());
|
||||
ret.insert(secret.name, secret.value);
|
||||
}
|
||||
|
||||
Ok(ret
|
||||
.into_iter()
|
||||
.map(|(name, value)| Secret { name, value })
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn create(
|
||||
&self,
|
||||
client: &WasmerClient,
|
||||
app_id: &str,
|
||||
secrets: Vec<Secret>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let res = wasmer_api::query::upsert_app_secrets(
|
||||
client,
|
||||
app_id,
|
||||
secrets.iter().map(|s| (s.name.as_str(), s.value.as_str())),
|
||||
)
|
||||
.await?;
|
||||
let res = res.context(
|
||||
"Backend did not return any payload to confirm the successful creation of the secret!",
|
||||
)?;
|
||||
|
||||
if !res.success {
|
||||
anyhow::bail!("Secret creation failed!")
|
||||
} else {
|
||||
if !self.quiet {
|
||||
eprintln!("Succesfully created secret(s):");
|
||||
for secret in &secrets {
|
||||
eprintln!("{}", secret.name.bold());
|
||||
}
|
||||
|
||||
let should_redeploy = self.redeploy || {
|
||||
if !self.non_interactive && self.from_file.is_some() {
|
||||
let theme = ColorfulTheme::default();
|
||||
dialoguer::Confirm::with_theme(&theme)
|
||||
.with_prompt("Do you want to redeploy your app?")
|
||||
.interact()?
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if should_redeploy {
|
||||
wasmer_api::query::redeploy_app_by_id(client, app_id).await?;
|
||||
eprintln!("{} Deployment complete", "𖥔".yellow().bold());
|
||||
} else {
|
||||
eprintln!(
|
||||
"{}: In order for secrets to appear in your app, re-deploy it.",
|
||||
"Info".bold()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_from_file(
|
||||
&self,
|
||||
client: &WasmerClient,
|
||||
path: &Path,
|
||||
app_id: &str,
|
||||
) -> anyhow::Result<(), anyhow::Error> {
|
||||
let secrets = super::utils::read_secrets_from_file(path).await?;
|
||||
|
||||
let secrets = self.filter_secrets(client, app_id, secrets).await?;
|
||||
self.create(client, app_id, secrets).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAppSecretsCreate {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
let client = self.env.client()?;
|
||||
let app_id = super::utils::get_app_id(
|
||||
&client,
|
||||
self.app_id.app.as_ref(),
|
||||
self.app_dir_path.as_ref(),
|
||||
self.quiet,
|
||||
self.non_interactive,
|
||||
)
|
||||
.await?;
|
||||
if let Some(file) = &self.from_file {
|
||||
self.create_from_file(&client, file, &app_id).await
|
||||
} else {
|
||||
let name = self.get_secret_name()?;
|
||||
let value = self.get_secret_value()?;
|
||||
let secret = Secret { name, value };
|
||||
self.create(&client, &app_id, vec![secret]).await
|
||||
}
|
||||
}
|
||||
}
|
165
lib/cli/src/commands/app/secrets/delete.rs
Normal file
165
lib/cli/src/commands/app/secrets/delete.rs
Normal file
@ -0,0 +1,165 @@
|
||||
use crate::{
|
||||
commands::{app::util::AppIdentFlag, AsyncCliCommand},
|
||||
config::WasmerEnv,
|
||||
};
|
||||
use colored::Colorize;
|
||||
use dialoguer::theme::ColorfulTheme;
|
||||
use is_terminal::IsTerminal;
|
||||
use std::path::{Path, PathBuf};
|
||||
use wasmer_api::WasmerClient;
|
||||
|
||||
use super::utils::{self, get_secrets};
|
||||
|
||||
/// Delete an existing app secret.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppSecretsDelete {
|
||||
/* --- Common flags --- */
|
||||
#[clap(flatten)]
|
||||
pub env: WasmerEnv,
|
||||
|
||||
/// Don't print any message.
|
||||
#[clap(long)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Do not prompt for user input.
|
||||
#[clap(long, default_value_t = !std::io::stdin().is_terminal())]
|
||||
pub non_interactive: bool,
|
||||
|
||||
/* --- Flags --- */
|
||||
#[clap(flatten)]
|
||||
pub app_id: AppIdentFlag,
|
||||
|
||||
/// The path to the directory where the config file for the application will be written to.
|
||||
#[clap(long = "app-dir", conflicts_with = "app")]
|
||||
pub app_dir_path: Option<PathBuf>,
|
||||
|
||||
/// Path to a file with secrets stored in JSON format to delete secrets from.
|
||||
#[clap(
|
||||
long,
|
||||
name = "from-file",
|
||||
conflicts_with = "name",
|
||||
conflicts_with = "all"
|
||||
)]
|
||||
pub from_file: Option<PathBuf>,
|
||||
|
||||
/// Delete all the secrets related to an app.
|
||||
#[clap(long, conflicts_with = "name")]
|
||||
pub all: bool,
|
||||
|
||||
/// Delete the secret(s) without asking for confirmation.
|
||||
#[clap(long)]
|
||||
pub force: bool,
|
||||
|
||||
/* --- Parameters --- */
|
||||
/// The name of the secret to delete.
|
||||
#[clap(name = "name")]
|
||||
pub secret_name: Option<String>,
|
||||
}
|
||||
|
||||
impl CmdAppSecretsDelete {
|
||||
fn get_secret_name(&self) -> anyhow::Result<String> {
|
||||
if let Some(name) = &self.secret_name {
|
||||
return Ok(name.clone());
|
||||
}
|
||||
|
||||
if self.non_interactive {
|
||||
anyhow::bail!("No secret name given. Provide one as a positional argument.")
|
||||
} else {
|
||||
let theme = ColorfulTheme::default();
|
||||
Ok(dialoguer::Input::with_theme(&theme)
|
||||
.with_prompt("Enter the name of the secret")
|
||||
.interact_text()?)
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete(
|
||||
&self,
|
||||
client: &WasmerClient,
|
||||
app_id: &str,
|
||||
secret_name: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let secret = utils::get_secret_by_name(client, app_id, secret_name).await?;
|
||||
|
||||
if let Some(secret) = secret {
|
||||
if !self.non_interactive && !self.force {
|
||||
let theme = ColorfulTheme::default();
|
||||
let res = dialoguer::Confirm::with_theme(&theme)
|
||||
.with_prompt(format!("Delete secret '{}'?", secret_name.bold()))
|
||||
.interact()?;
|
||||
if !res {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let res = wasmer_api::query::delete_app_secret(client, secret.id.into_inner()).await?;
|
||||
|
||||
match res {
|
||||
Some(res) if !res.success => {
|
||||
anyhow::bail!("Error deleting secret '{}'", secret.name.bold())
|
||||
}
|
||||
Some(_) => {
|
||||
if !self.quiet {
|
||||
eprintln!("Correctly deleted secret '{}'", secret.name.bold());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
anyhow::bail!("Error deleting secret '{}'", secret.name.bold())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !self.quiet {
|
||||
eprintln!("No secret with name '{}' found", secret_name.bold());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_from_file(
|
||||
&self,
|
||||
client: &WasmerClient,
|
||||
path: &Path,
|
||||
app_id: String,
|
||||
) -> anyhow::Result<()> {
|
||||
let secrets = super::utils::read_secrets_from_file(path).await?;
|
||||
|
||||
for secret in secrets {
|
||||
self.delete(client, &app_id, &secret.name).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAppSecretsDelete {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
let client = self.env.client()?;
|
||||
let app_id = super::utils::get_app_id(
|
||||
&client,
|
||||
self.app_id.app.as_ref(),
|
||||
self.app_dir_path.as_ref(),
|
||||
self.quiet,
|
||||
self.non_interactive,
|
||||
)
|
||||
.await?;
|
||||
if let Some(file) = &self.from_file {
|
||||
self.delete_from_file(&client, file, app_id).await
|
||||
} else if self.all {
|
||||
if self.non_interactive && !self.force {
|
||||
anyhow::bail!("Refusing to delete all secrets in non-interactive mode without the `--force` flag.")
|
||||
}
|
||||
let secrets = get_secrets(&client, &app_id).await?;
|
||||
for secret in secrets {
|
||||
self.delete(&client, &app_id, &secret.name).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
let name = self.get_secret_name()?;
|
||||
self.delete(&client, &app_id, &name).await
|
||||
}
|
||||
}
|
||||
}
|
63
lib/cli/src/commands/app/secrets/list.rs
Normal file
63
lib/cli/src/commands/app/secrets/list.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use super::utils::{get_secrets, BackendSecretWrapper};
|
||||
use crate::{
|
||||
commands::{app::util::AppIdentFlag, AsyncCliCommand},
|
||||
config::WasmerEnv,
|
||||
opts::ListFormatOpts,
|
||||
};
|
||||
use is_terminal::IsTerminal;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Retrieve the value of an existing app secret.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppSecretsList {
|
||||
/* --- Common flags --- */
|
||||
#[clap(flatten)]
|
||||
pub env: WasmerEnv,
|
||||
|
||||
/// Don't print any message.
|
||||
#[clap(long)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Do not prompt for user input.
|
||||
#[clap(long, default_value_t = !std::io::stdin().is_terminal())]
|
||||
pub non_interactive: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub fmt: ListFormatOpts,
|
||||
|
||||
/* --- Flags --- */
|
||||
/// The identifier of the app to list secrets of.
|
||||
#[clap(flatten)]
|
||||
pub app_id: AppIdentFlag,
|
||||
|
||||
/// The path to the directory where the config file for the application will be written to.
|
||||
#[clap(long = "app-dir", conflicts_with = "app")]
|
||||
pub app_dir_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAppSecretsList {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
let client = self.env.client()?;
|
||||
let app_id = super::utils::get_app_id(
|
||||
&client,
|
||||
self.app_id.app.as_ref(),
|
||||
self.app_dir_path.as_ref(),
|
||||
self.quiet,
|
||||
self.non_interactive,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let secrets: Vec<BackendSecretWrapper> = get_secrets(&client, &app_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect();
|
||||
|
||||
println!("{}", self.fmt.format.render(secrets.as_slice()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
49
lib/cli/src/commands/app/secrets/mod.rs
Normal file
49
lib/cli/src/commands/app/secrets/mod.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use crate::commands::AsyncCliCommand;
|
||||
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod list;
|
||||
pub mod reveal;
|
||||
pub mod update;
|
||||
mod utils;
|
||||
|
||||
/// Manage and reveal secrets related to Edge apps.
|
||||
#[derive(Debug, clap::Parser)]
|
||||
pub enum CmdAppSecrets {
|
||||
Create(create::CmdAppSecretsCreate),
|
||||
Delete(delete::CmdAppSecretsDelete),
|
||||
Reveal(reveal::CmdAppSecretsReveal),
|
||||
List(list::CmdAppSecretsList),
|
||||
Update(update::CmdAppSecretsUpdate),
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAppSecrets {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
match self {
|
||||
CmdAppSecrets::Create(c) => {
|
||||
c.run_async().await?;
|
||||
Ok(())
|
||||
}
|
||||
CmdAppSecrets::Delete(c) => {
|
||||
c.run_async().await?;
|
||||
Ok(())
|
||||
}
|
||||
CmdAppSecrets::Reveal(c) => {
|
||||
c.run_async().await?;
|
||||
Ok(())
|
||||
}
|
||||
CmdAppSecrets::List(c) => {
|
||||
c.run_async().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
CmdAppSecrets::Update(c) => {
|
||||
c.run_async().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
118
lib/cli/src/commands/app/secrets/reveal.rs
Normal file
118
lib/cli/src/commands/app/secrets/reveal.rs
Normal file
@ -0,0 +1,118 @@
|
||||
use super::utils;
|
||||
use crate::{
|
||||
commands::{app::util::AppIdentFlag, AsyncCliCommand},
|
||||
config::WasmerEnv,
|
||||
opts::ListFormatOpts,
|
||||
utils::render::{ItemFormat, ListFormat},
|
||||
};
|
||||
use dialoguer::theme::ColorfulTheme;
|
||||
use is_terminal::IsTerminal;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Reveal the value of an existing app secret.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppSecretsReveal {
|
||||
/* --- Common flags --- */
|
||||
#[clap(flatten)]
|
||||
pub env: WasmerEnv,
|
||||
|
||||
/// Don't print any message.
|
||||
#[clap(long)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Do not prompt for user input.
|
||||
#[clap(long, default_value_t = !std::io::stdin().is_terminal())]
|
||||
pub non_interactive: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub fmt: Option<ListFormatOpts>,
|
||||
|
||||
/* --- Flags --- */
|
||||
#[clap(flatten)]
|
||||
pub app_id: AppIdentFlag,
|
||||
|
||||
/// The path to the directory where the config file for the application will be written to.
|
||||
#[clap(long = "app-dir", conflicts_with = "app")]
|
||||
pub app_dir_path: Option<PathBuf>,
|
||||
|
||||
/// Reveal all the secrets related to an app.
|
||||
#[clap(long, conflicts_with = "name")]
|
||||
pub all: bool,
|
||||
|
||||
/* --- Parameters --- */
|
||||
/// The name of the secret to get the value of.
|
||||
#[clap(name = "name")]
|
||||
pub secret_name: Option<String>,
|
||||
}
|
||||
|
||||
impl CmdAppSecretsReveal {
|
||||
fn get_secret_name(&self) -> anyhow::Result<String> {
|
||||
if let Some(name) = &self.secret_name {
|
||||
return Ok(name.clone());
|
||||
}
|
||||
|
||||
if self.non_interactive {
|
||||
anyhow::bail!("No secret name given. Provide one as a positional argument.")
|
||||
} else {
|
||||
let theme = ColorfulTheme::default();
|
||||
Ok(dialoguer::Input::with_theme(&theme)
|
||||
.with_prompt("Enter the name of the secret:")
|
||||
.interact_text()?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAppSecretsReveal {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
let client = self.env.client()?;
|
||||
let app_id = super::utils::get_app_id(
|
||||
&client,
|
||||
self.app_id.app.as_ref(),
|
||||
self.app_dir_path.as_ref(),
|
||||
self.quiet,
|
||||
self.non_interactive,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !self.all {
|
||||
let name = self.get_secret_name()?;
|
||||
|
||||
let value = utils::get_secret_value_by_name(&client, &app_id, &name).await?;
|
||||
|
||||
let secret = utils::Secret { name, value };
|
||||
|
||||
if let Some(fmt) = &self.fmt {
|
||||
let fmt = match fmt.format {
|
||||
ListFormat::Json => ItemFormat::Json,
|
||||
ListFormat::Yaml => ItemFormat::Yaml,
|
||||
ListFormat::Table => ItemFormat::Table,
|
||||
ListFormat::ItemTable => {
|
||||
anyhow::bail!("The 'item-table' format is not available for single values.")
|
||||
}
|
||||
};
|
||||
println!("{}", fmt.render(&secret));
|
||||
} else {
|
||||
print!("{}", secret.value);
|
||||
}
|
||||
} else {
|
||||
let secrets: Vec<utils::Secret> = utils::reveal_secrets(&client, &app_id).await?;
|
||||
|
||||
if let Some(fmt) = &self.fmt {
|
||||
println!("{}", fmt.format.render(secrets.as_slice()));
|
||||
} else {
|
||||
for secret in secrets {
|
||||
println!(
|
||||
"{}=\"{}\"",
|
||||
secret.name,
|
||||
utils::render::sanitize_value(&secret.value)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
223
lib/cli/src/commands/app/secrets/update.rs
Normal file
223
lib/cli/src/commands/app/secrets/update.rs
Normal file
@ -0,0 +1,223 @@
|
||||
use super::utils::Secret;
|
||||
use crate::{
|
||||
commands::{app::util::AppIdentFlag, AsyncCliCommand},
|
||||
config::WasmerEnv,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use colored::Colorize;
|
||||
use dialoguer::theme::ColorfulTheme;
|
||||
use is_terminal::IsTerminal;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use wasmer_api::WasmerClient;
|
||||
|
||||
/// Update an existing app secret.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppSecretsUpdate {
|
||||
/* --- Common args --- */
|
||||
#[clap(flatten)]
|
||||
pub env: WasmerEnv,
|
||||
|
||||
/// Don't print any message.
|
||||
#[clap(long)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Do not prompt for user input.
|
||||
#[clap(long, default_value_t = !std::io::stdin().is_terminal())]
|
||||
pub non_interactive: bool,
|
||||
|
||||
/* --- Flags --- */
|
||||
/// The path to the directory where the config file for the application will be written to.
|
||||
#[clap(long = "app-dir", conflicts_with = "app")]
|
||||
pub app_dir_path: Option<PathBuf>,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub app_id: AppIdentFlag,
|
||||
|
||||
/// Path to a file with secrets stored in JSON format to update secrets from.
|
||||
#[clap(
|
||||
long,
|
||||
name = "from-file",
|
||||
conflicts_with = "value",
|
||||
conflicts_with = "name"
|
||||
)]
|
||||
pub from_file: Option<PathBuf>,
|
||||
|
||||
/// Whether or not to redeploy the app after creating the secrets.
|
||||
#[clap(long)]
|
||||
pub redeploy: bool,
|
||||
|
||||
/* --- Parameters --- */
|
||||
/// The name of the secret to update.
|
||||
#[clap(name = "name")]
|
||||
pub secret_name: Option<String>,
|
||||
|
||||
/// The value of the secret to update.
|
||||
#[clap(name = "value")]
|
||||
pub secret_value: Option<String>,
|
||||
}
|
||||
|
||||
impl CmdAppSecretsUpdate {
|
||||
fn get_secret_name(&self) -> anyhow::Result<String> {
|
||||
if let Some(name) = &self.secret_name {
|
||||
return Ok(name.clone());
|
||||
}
|
||||
|
||||
if self.non_interactive {
|
||||
anyhow::bail!("No secret name given. Provide one as a positional argument.")
|
||||
} else {
|
||||
let theme = ColorfulTheme::default();
|
||||
Ok(dialoguer::Input::with_theme(&theme)
|
||||
.with_prompt("Enter the name of the secret:")
|
||||
.interact_text()?)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_secret_value(&self) -> anyhow::Result<String> {
|
||||
if let Some(value) = &self.secret_value {
|
||||
return Ok(value.clone());
|
||||
}
|
||||
|
||||
if self.non_interactive {
|
||||
anyhow::bail!("No secret value given. Provide one as a positional argument.")
|
||||
} else {
|
||||
let theme = ColorfulTheme::default();
|
||||
Ok(dialoguer::Input::with_theme(&theme)
|
||||
.with_prompt("Enter the value of the secret:")
|
||||
.interact_text()?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a list of secrets, checks if the given secrets already exist for the given app and
|
||||
/// returns a list of secrets that must be upserted.
|
||||
async fn filter_secrets(
|
||||
&self,
|
||||
client: &WasmerClient,
|
||||
app_id: &str,
|
||||
secrets: Vec<Secret>,
|
||||
) -> anyhow::Result<Vec<Secret>> {
|
||||
let names = secrets.iter().map(|s| &s.name);
|
||||
let app_secrets =
|
||||
wasmer_api::query::get_all_app_secrets_filtered(client, app_id, names).await?;
|
||||
let sset = HashSet::<&str>::from_iter(app_secrets.iter().map(|s| s.name.as_str()));
|
||||
|
||||
let mut ret = vec![];
|
||||
|
||||
for secret in secrets {
|
||||
if !sset.contains(secret.name.as_str()) {
|
||||
if self.non_interactive {
|
||||
anyhow::bail!("Cannot update secret '{}' in app {app_id} as it does not exist yet. Use the `create` command instead.", secret.name.bold());
|
||||
} else {
|
||||
eprintln!(
|
||||
"Secret '{}' does not exist for the selected app.",
|
||||
secret.name.bold()
|
||||
);
|
||||
let theme = ColorfulTheme::default();
|
||||
let res = dialoguer::Confirm::with_theme(&theme)
|
||||
.with_prompt("Do you want to create it?")
|
||||
.interact()?;
|
||||
|
||||
if !res {
|
||||
eprintln!("Cannot update secret '{}' as it does not exist yet. Use the `create` command instead.", secret.name.bold());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ret.push(secret);
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
async fn update(
|
||||
&self,
|
||||
client: &WasmerClient,
|
||||
app_id: &str,
|
||||
secrets: Vec<Secret>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let res = wasmer_api::query::upsert_app_secrets(
|
||||
client,
|
||||
app_id,
|
||||
secrets.iter().map(|s| (s.name.as_str(), s.value.as_str())),
|
||||
)
|
||||
.await?;
|
||||
let res = res.context(
|
||||
"Backend did not return any payload to confirm the successful update of the secret!",
|
||||
)?;
|
||||
|
||||
if !res.success {
|
||||
anyhow::bail!("Secret creation failed!")
|
||||
} else {
|
||||
if !self.quiet {
|
||||
eprintln!("Succesfully updated secret(s):");
|
||||
for secret in &secrets {
|
||||
eprintln!("{}", secret.name.bold());
|
||||
}
|
||||
|
||||
let should_redeploy = self.redeploy || {
|
||||
if !self.non_interactive && self.from_file.is_some() {
|
||||
let theme = ColorfulTheme::default();
|
||||
dialoguer::Confirm::with_theme(&theme)
|
||||
.with_prompt("Do you want to redeploy your app?")
|
||||
.interact()?
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if should_redeploy {
|
||||
wasmer_api::query::redeploy_app_by_id(client, app_id).await?;
|
||||
eprintln!("{} Deployment complete", "𖥔".yellow().bold());
|
||||
} else {
|
||||
eprintln!(
|
||||
"{}: In order for secrets to appear in your app, re-deploy it.",
|
||||
"Info".bold()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_from_file(
|
||||
&self,
|
||||
client: &WasmerClient,
|
||||
path: &Path,
|
||||
app_id: &str,
|
||||
) -> anyhow::Result<(), anyhow::Error> {
|
||||
let secrets = super::utils::read_secrets_from_file(path).await?;
|
||||
|
||||
let secrets = self.filter_secrets(client, app_id, secrets).await?;
|
||||
self.update(client, app_id, secrets).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAppSecretsUpdate {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
let client = self.env.client()?;
|
||||
let app_id = super::utils::get_app_id(
|
||||
&client,
|
||||
self.app_id.app.as_ref(),
|
||||
self.app_dir_path.as_ref(),
|
||||
self.quiet,
|
||||
self.non_interactive,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(file) = &self.from_file {
|
||||
self.update_from_file(&client, file, &app_id).await
|
||||
} else {
|
||||
let name = self.get_secret_name()?;
|
||||
let value = self.get_secret_value()?;
|
||||
let secret = Secret { name, value };
|
||||
self.update(&client, &app_id, vec![secret]).await
|
||||
}
|
||||
}
|
||||
}
|
165
lib/cli/src/commands/app/secrets/utils/mod.rs
Normal file
165
lib/cli/src/commands/app/secrets/utils/mod.rs
Normal file
@ -0,0 +1,165 @@
|
||||
pub(crate) mod render;
|
||||
|
||||
use colored::Colorize;
|
||||
use std::{
|
||||
env::current_dir,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
use wasmer_api::{types::Secret as BackendSecret, WasmerClient};
|
||||
|
||||
use crate::commands::app::util::{get_app_config_from_dir, prompt_app_ident, AppIdent};
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub(super) struct Secret {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
pub(super) async fn read_secrets_from_file(path: &Path) -> anyhow::Result<Vec<Secret>> {
|
||||
let mut ret = vec![];
|
||||
for item in dotenvy::from_path_iter(path)? {
|
||||
let (name, value) = item?;
|
||||
ret.push(Secret { name, value })
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub(super) async fn get_secret_by_name(
|
||||
client: &WasmerClient,
|
||||
app_id: &str,
|
||||
secret_name: &str,
|
||||
) -> anyhow::Result<Option<BackendSecret>> {
|
||||
wasmer_api::query::get_app_secret_by_name(client, app_id, secret_name).await
|
||||
}
|
||||
pub(crate) async fn get_secrets(
|
||||
client: &WasmerClient,
|
||||
app_id: &str,
|
||||
) -> anyhow::Result<Vec<wasmer_api::types::Secret>> {
|
||||
wasmer_api::query::get_all_app_secrets(client, app_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_secret_value(
|
||||
client: &WasmerClient,
|
||||
secret: &wasmer_api::types::Secret,
|
||||
) -> anyhow::Result<String> {
|
||||
wasmer_api::query::get_app_secret_value_by_id(client, secret.id.clone().into_inner())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"No value found for secret with name '{}'",
|
||||
secret.name.bold()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn get_secret_value_by_name(
|
||||
client: &WasmerClient,
|
||||
app_id: &str,
|
||||
secret_name: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
match get_secret_by_name(client, app_id, secret_name).await? {
|
||||
Some(secret) => get_secret_value(client, &secret).await,
|
||||
None => anyhow::bail!("No secret found with name {secret_name} for app {app_id}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn reveal_secrets(
|
||||
client: &WasmerClient,
|
||||
app_id: &str,
|
||||
) -> anyhow::Result<Vec<Secret>> {
|
||||
let secrets = wasmer_api::query::get_all_app_secrets(client, app_id).await?;
|
||||
let mut ret = vec![];
|
||||
for secret in secrets {
|
||||
let name = secret.name.clone();
|
||||
let value = get_secret_value(client, &secret).await?;
|
||||
ret.push(Secret { name, value });
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Utility struct used just to implement [`CliRender`].
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub(super) struct BackendSecretWrapper(pub BackendSecret);
|
||||
|
||||
impl From<BackendSecret> for BackendSecretWrapper {
|
||||
fn from(value: BackendSecret) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A secrets-specific app to retrieve an app identifier.
|
||||
pub(super) async fn get_app_id(
|
||||
client: &WasmerClient,
|
||||
app: Option<&AppIdent>,
|
||||
app_dir_path: Option<&PathBuf>,
|
||||
quiet: bool,
|
||||
non_interactive: bool,
|
||||
) -> anyhow::Result<String> {
|
||||
if let Some(app_id) = app {
|
||||
let app = app_id.resolve(client).await?;
|
||||
return Ok(app.id.into_inner());
|
||||
}
|
||||
|
||||
let path = if let Some(path) = app_dir_path {
|
||||
path.clone()
|
||||
} else {
|
||||
current_dir()?
|
||||
};
|
||||
|
||||
if let Ok(r) = get_app_config_from_dir(&path) {
|
||||
let (app, _) = r;
|
||||
|
||||
let app_name = if let Some(owner) = &app.owner {
|
||||
format!("{owner}/{}", app.name)
|
||||
} else {
|
||||
app.name.to_string()
|
||||
};
|
||||
|
||||
let id = if let Some(id) = &app.app_id {
|
||||
Some(id.clone())
|
||||
} else if let Ok(app_ident) = AppIdent::from_str(&app_name) {
|
||||
if let Ok(app) = app_ident.resolve(client).await {
|
||||
Some(app.id.into_inner())
|
||||
} else {
|
||||
if !quiet {
|
||||
eprintln!("{}: the app found in {} does not exist.\n{}: maybe it was not deployed yet?",
|
||||
"Warning".bold().yellow(),
|
||||
format!("'{}'", path.display()).dimmed(),
|
||||
"Hint".bold());
|
||||
}
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(id) = id {
|
||||
if !quiet {
|
||||
if let Some(owner) = &app.owner {
|
||||
eprintln!(
|
||||
"Managing secrets related to app {} ({owner}).",
|
||||
app.name.bold()
|
||||
);
|
||||
} else {
|
||||
eprintln!("Managing secrets related to app {}.", app.name.bold());
|
||||
}
|
||||
}
|
||||
return Ok(id);
|
||||
}
|
||||
} else if let Some(path) = app_dir_path {
|
||||
anyhow::bail!(
|
||||
"No app configuration file found in path {}.",
|
||||
path.display()
|
||||
)
|
||||
}
|
||||
|
||||
if non_interactive {
|
||||
anyhow::bail!("No app id given. Provide one using the `--app` flag.")
|
||||
} else {
|
||||
let id = prompt_app_ident("Enter the name of the app")?;
|
||||
let app = id.resolve(client).await?;
|
||||
Ok(app.id.into_inner())
|
||||
}
|
||||
}
|
114
lib/cli/src/commands/app/secrets/utils/render.rs
Normal file
114
lib/cli/src/commands/app/secrets/utils/render.rs
Normal file
@ -0,0 +1,114 @@
|
||||
use super::{BackendSecretWrapper, Secret};
|
||||
use crate::utils::render::CliRender;
|
||||
use colored::Colorize;
|
||||
use comfy_table::{Cell, Table};
|
||||
use time::OffsetDateTime;
|
||||
use wasmer_api::types::{DateTime, Secret as BackendSecret};
|
||||
|
||||
impl CliRender for Secret {
|
||||
fn render_item_table(&self) -> String {
|
||||
let mut table = Table::new();
|
||||
let Secret { name, value }: &Secret = self;
|
||||
|
||||
table.load_preset(comfy_table::presets::NOTHING);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
|
||||
let value = sanitize_value(value);
|
||||
table.add_rows([
|
||||
vec![
|
||||
Cell::new("Name".to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
Cell::new("Value".to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
],
|
||||
vec![Cell::new(name.to_string()), Cell::new(format!("'{value}'"))],
|
||||
]);
|
||||
table.to_string()
|
||||
}
|
||||
|
||||
fn render_list_table(items: &[Self]) -> String {
|
||||
if items.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let mut table = Table::new();
|
||||
table.load_preset(comfy_table::presets::NOTHING);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
|
||||
table.set_header(vec![
|
||||
Cell::new("Name".to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
Cell::new("Value".to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
]);
|
||||
table.add_rows(items.iter().map(|s| {
|
||||
vec![
|
||||
Cell::new(s.name.clone()),
|
||||
Cell::new(format!("'{}'", sanitize_value(&s.value))),
|
||||
]
|
||||
}));
|
||||
table.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl CliRender for BackendSecretWrapper {
|
||||
fn render_item_table(&self) -> String {
|
||||
let mut table = Table::new();
|
||||
let BackendSecret {
|
||||
name, updated_at, ..
|
||||
}: &BackendSecret = &self.0;
|
||||
let last_updated = last_updated_to_human(updated_at.clone())
|
||||
.unwrap()
|
||||
.to_string();
|
||||
table.add_rows([
|
||||
vec!["Name".to_string(), name.to_string()],
|
||||
vec![
|
||||
"Last updated".to_string(),
|
||||
format!("{last_updated} ago").dimmed().to_string(),
|
||||
],
|
||||
]);
|
||||
table.to_string()
|
||||
}
|
||||
|
||||
fn render_list_table(items: &[Self]) -> String {
|
||||
if items.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let mut table = Table::new();
|
||||
table.load_preset(comfy_table::presets::NOTHING);
|
||||
table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
|
||||
|
||||
table.set_header(vec![
|
||||
Cell::new("Name".to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
Cell::new("Last updated".to_string()).add_attribute(comfy_table::Attribute::Bold),
|
||||
]);
|
||||
table.add_rows(items.iter().map(|s| {
|
||||
let last_updated = last_updated_to_human(s.0.updated_at.clone())
|
||||
.unwrap()
|
||||
.to_string();
|
||||
vec![
|
||||
Cell::new(s.0.name.clone()),
|
||||
Cell::new(format!("{last_updated} ago").dimmed().to_string()),
|
||||
]
|
||||
}));
|
||||
table.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn last_updated_to_human(last_update: DateTime) -> anyhow::Result<humantime::Duration> {
|
||||
let last_update: OffsetDateTime = last_update.try_into()?;
|
||||
let elapsed: std::time::Duration = (OffsetDateTime::now_utc() - last_update).try_into()?;
|
||||
Ok(humantime::Duration::from(std::time::Duration::from_secs(
|
||||
elapsed.as_secs(),
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn sanitize_value(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_ascii() {
|
||||
let c = c as u8;
|
||||
std::ascii::escape_default(c).to_string()
|
||||
} else {
|
||||
c.to_string()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("")
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
use std::{path::Path, str::FromStr};
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use colored::Colorize;
|
||||
use dialoguer::Confirm;
|
||||
use dialoguer::{theme::ColorfulTheme, Confirm};
|
||||
use wasmer_api::{
|
||||
global_id::{GlobalId, NodeKind},
|
||||
types::DeployApp,
|
||||
@ -9,8 +11,8 @@ use wasmer_api::{
|
||||
use wasmer_config::app::AppConfigV1;
|
||||
|
||||
use crate::{
|
||||
commands::Login,
|
||||
opts::{ApiOpts, WasmerEnv},
|
||||
commands::{AsyncCliCommand, Login},
|
||||
config::WasmerEnv,
|
||||
};
|
||||
|
||||
/// App identifier.
|
||||
@ -94,29 +96,6 @@ impl std::str::FromStr for AppIdent {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_app_config_from_current_dir() -> Result<(AppConfigV1, std::path::PathBuf), anyhow::Error>
|
||||
{
|
||||
// read the information from local `app.yaml
|
||||
let current_dir = std::env::current_dir()?;
|
||||
let app_config_path = current_dir.join(AppConfigV1::CANONICAL_FILE_NAME);
|
||||
|
||||
if !app_config_path.exists() || !app_config_path.is_file() {
|
||||
bail!(
|
||||
"Could not find app.yaml at path: '{}'.\nPlease specify an app like 'wasmer app get <namespace>/<name>' or 'wasmer app get <name>`'",
|
||||
app_config_path.display()
|
||||
);
|
||||
}
|
||||
// read the app.yaml
|
||||
let raw_app_config = std::fs::read_to_string(&app_config_path)
|
||||
.with_context(|| format!("Could not read file '{}'", app_config_path.display()))?;
|
||||
|
||||
// parse the app.yaml
|
||||
let config = AppConfigV1::parse_yaml(&raw_app_config)
|
||||
.map_err(|err| anyhow::anyhow!("Could not parse app.yaml: {err:?}"))?;
|
||||
|
||||
Ok((config, app_config_path))
|
||||
}
|
||||
|
||||
/// Options for identifying an app.
|
||||
///
|
||||
/// Provides convenience methods for resolving an app identifier or loading it
|
||||
@ -218,19 +197,34 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A utility struct used by commands that need the [`AppIdent`] as a flag.
|
||||
///
|
||||
/// NOTE: Differently from [`AppIdentOpts`], the use of this struct does not entail searching the
|
||||
/// current directory for an `app.yaml` if not specified.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct AppIdentFlag {
|
||||
/// Identifier of the application.
|
||||
///
|
||||
/// Valid input:
|
||||
/// - namespace/app-name
|
||||
/// - app-alias
|
||||
/// - App ID
|
||||
#[clap(long)]
|
||||
pub app: Option<AppIdent>,
|
||||
}
|
||||
|
||||
pub(super) async fn login_user(
|
||||
api: &ApiOpts,
|
||||
env: &WasmerEnv,
|
||||
interactive: bool,
|
||||
msg: &str,
|
||||
) -> anyhow::Result<WasmerClient> {
|
||||
if let Ok(client) = api.client() {
|
||||
if let Ok(client) = env.client() {
|
||||
return Ok(client);
|
||||
}
|
||||
|
||||
let theme = dialoguer::theme::ColorfulTheme::default();
|
||||
|
||||
if api.token.is_none() {
|
||||
if env.token().is_none() {
|
||||
if interactive {
|
||||
eprintln!(
|
||||
"{}: You need to be logged in to {msg}.",
|
||||
@ -243,13 +237,10 @@ pub(super) async fn login_user(
|
||||
{
|
||||
Login {
|
||||
no_browser: false,
|
||||
wasmer_dir: env.wasmer_dir.clone(),
|
||||
registry: api
|
||||
.registry
|
||||
.clone()
|
||||
.map(|l| wasmer_registry::wasmer_env::Registry::from(l.to_string())),
|
||||
token: api.token.clone(),
|
||||
cache_dir: Some(env.cache_dir.clone()),
|
||||
wasmer_dir: env.dir().to_path_buf(),
|
||||
cache_dir: env.cache_dir().to_path_buf(),
|
||||
token: None,
|
||||
registry: env.registry.clone(),
|
||||
}
|
||||
.run_async()
|
||||
.await?;
|
||||
@ -268,5 +259,48 @@ pub(super) async fn login_user(
|
||||
}
|
||||
}
|
||||
|
||||
api.client()
|
||||
env.client()
|
||||
}
|
||||
|
||||
pub fn get_app_config_from_dir(
|
||||
path: &Path,
|
||||
) -> Result<(AppConfigV1, std::path::PathBuf), anyhow::Error> {
|
||||
let app_config_path = path.join(AppConfigV1::CANONICAL_FILE_NAME);
|
||||
|
||||
if !app_config_path.exists() || !app_config_path.is_file() {
|
||||
bail!(
|
||||
"Could not find app.yaml at path: '{}'.\nPlease specify an app like 'wasmer app get <namespace>/<name>' or 'wasmer app get <name>`'",
|
||||
app_config_path.display()
|
||||
);
|
||||
}
|
||||
// read the app.yaml
|
||||
let raw_app_config = std::fs::read_to_string(&app_config_path)
|
||||
.with_context(|| format!("Could not read file '{}'", app_config_path.display()))?;
|
||||
|
||||
// parse the app.yaml
|
||||
let config = AppConfigV1::parse_yaml(&raw_app_config)
|
||||
.map_err(|err| anyhow::anyhow!("Could not parse app.yaml: {err:?}"))?;
|
||||
|
||||
Ok((config, app_config_path))
|
||||
}
|
||||
|
||||
pub fn get_app_config_from_current_dir() -> Result<(AppConfigV1, std::path::PathBuf), anyhow::Error>
|
||||
{
|
||||
let current_dir = std::env::current_dir()?;
|
||||
get_app_config_from_dir(¤t_dir)
|
||||
}
|
||||
|
||||
/// Prompt for an app ident.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn prompt_app_ident(message: &str) -> Result<AppIdent, anyhow::Error> {
|
||||
let theme = ColorfulTheme::default();
|
||||
loop {
|
||||
let ident: String = dialoguer::Input::with_theme(&theme)
|
||||
.with_prompt(message)
|
||||
.interact_text()?;
|
||||
match AppIdent::from_str(&ident) {
|
||||
Ok(id) => break Ok(id),
|
||||
Err(e) => eprintln!("{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,12 @@
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
opts::{ApiOpts, ItemFormatOpts},
|
||||
};
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ItemFormatOpts};
|
||||
|
||||
/// Switch the active version of an app. (rollback / rollforward)
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppVersionActivate {
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub api: ApiOpts,
|
||||
pub env: WasmerEnv,
|
||||
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub fmt: ItemFormatOpts,
|
||||
|
||||
/// App version ID to activate.
|
||||
@ -25,7 +21,7 @@ impl AsyncCliCommand for CmdAppVersionActivate {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
|
||||
let app = wasmer_api::query::app_version_activate(&client, self.version).await?;
|
||||
|
||||
|
@ -2,17 +2,17 @@ use anyhow::Context;
|
||||
|
||||
use crate::{
|
||||
commands::{app::util::AppIdentOpts, AsyncCliCommand},
|
||||
opts::{ApiOpts, ItemFormatOpts},
|
||||
config::WasmerEnv,
|
||||
opts::ItemFormatOpts,
|
||||
};
|
||||
|
||||
/// Show information for a specific app version.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppVersionGet {
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub api: ApiOpts,
|
||||
pub env: WasmerEnv,
|
||||
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub fmt: ItemFormatOpts,
|
||||
|
||||
/// *Name* of the version - NOT the unique version id!
|
||||
@ -29,7 +29,7 @@ impl AsyncCliCommand for CmdAppVersionGet {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
let (_ident, app) = self.ident.load_app(&client).await?;
|
||||
|
||||
let version = wasmer_api::query::get_app_version(
|
||||
|
@ -2,15 +2,16 @@ use wasmer_api::types::{DeployAppVersionsSortBy, GetDeployAppVersionsVars};
|
||||
|
||||
use crate::{
|
||||
commands::{app::util::AppIdentOpts, AsyncCliCommand},
|
||||
opts::{ApiOpts, ListFormatOpts},
|
||||
config::WasmerEnv,
|
||||
opts::ListFormatOpts,
|
||||
};
|
||||
|
||||
/// List versions of an app.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppVersionList {
|
||||
#[clap(flatten)]
|
||||
#[allow(missing_docs)]
|
||||
pub api: ApiOpts,
|
||||
pub env: WasmerEnv,
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[clap(flatten)]
|
||||
pub fmt: ListFormatOpts,
|
||||
@ -72,7 +73,7 @@ impl AsyncCliCommand for CmdAppVersionList {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
let (_ident, app) = self.ident.load_app(&client).await?;
|
||||
|
||||
let versions = if self.all {
|
||||
|
120
lib/cli/src/commands/app/volumes/credentials/mod.rs
Normal file
120
lib/cli/src/commands/app/volumes/credentials/mod.rs
Normal file
@ -0,0 +1,120 @@
|
||||
pub(super) mod rotate_secrets;
|
||||
|
||||
use crate::{
|
||||
commands::{app::util::AppIdentOpts, AsyncCliCommand},
|
||||
config::WasmerEnv,
|
||||
};
|
||||
|
||||
/// Retrieve access credentials for the volumes of an app.
|
||||
///
|
||||
/// The credentials can be used to access volumes with any S3 client, for example rclone. Note:
|
||||
/// using --format=rclone - which is the default - will output an rclone configuration snippet.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppVolumesCredentials {
|
||||
#[clap(flatten)]
|
||||
pub env: WasmerEnv,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub fmt: ItemFormatOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub ident: AppIdentOpts,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAppVolumesCredentials {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
let client = self.env.client()?;
|
||||
let (_ident, app) = self.ident.load_app(&client).await?;
|
||||
|
||||
let creds =
|
||||
wasmer_api::query::get_app_s3_credentials(&client, app.id.clone().into_inner()).await?;
|
||||
|
||||
match self.fmt.format {
|
||||
CredsItemFormat::Rclone => {
|
||||
let rclone_config = format!(
|
||||
r#"
|
||||
[edge-{app_name}]
|
||||
# rclone configuration for volumes of {app_name}
|
||||
type = s3
|
||||
provider = Other
|
||||
acl = private
|
||||
access_key_id = {access_key}
|
||||
secret_access_key = {secret_key}
|
||||
endpoint = {endpoint}
|
||||
|
||||
"#,
|
||||
app_name = app.name,
|
||||
access_key = creds.access_key,
|
||||
secret_key = creds.secret_key,
|
||||
endpoint = creds.endpoint,
|
||||
);
|
||||
println!("{rclone_config}");
|
||||
}
|
||||
CredsItemFormat::Json => {
|
||||
let value = serde_json::json!({
|
||||
"app_name": app.name,
|
||||
"access_key": creds.access_key,
|
||||
"secret_key": creds.secret_key,
|
||||
"endpoint": creds.endpoint,
|
||||
});
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&value).unwrap());
|
||||
}
|
||||
CredsItemFormat::Yaml => {
|
||||
let config = format!(
|
||||
r#"
|
||||
app_name: {app_name}
|
||||
access_key: {access_key}
|
||||
secret_key: {secret_key}
|
||||
endpoint: {endpoint}
|
||||
"#,
|
||||
app_name = app.name,
|
||||
access_key = creds.access_key,
|
||||
secret_key = creds.secret_key,
|
||||
endpoint = creds.endpoint,
|
||||
);
|
||||
println!("{config}");
|
||||
}
|
||||
|
||||
CredsItemFormat::Table => {
|
||||
let mut table = comfy_table::Table::new();
|
||||
table.add_row(vec!["App name", "Access key", "Secret key", "Endpoint"]);
|
||||
table.add_row(vec![
|
||||
app.name,
|
||||
creds.access_key,
|
||||
creds.secret_key,
|
||||
creds.endpoint,
|
||||
]);
|
||||
|
||||
println!("{table}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The following is a copy of [`crate::opts::ItemFormatOpts`] with an
|
||||
* additional formatting - rclone - that only makes sense in this context.
|
||||
*/
|
||||
|
||||
/// The possibile formats to output the credentials in.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
|
||||
pub enum CredsItemFormat {
|
||||
Json,
|
||||
Yaml,
|
||||
Table,
|
||||
Rclone,
|
||||
}
|
||||
|
||||
/// Formatting options for credentials.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct ItemFormatOpts {
|
||||
/// Output format
|
||||
#[clap(short = 'f', long, default_value = "rclone")]
|
||||
pub format: CredsItemFormat,
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
use super::ItemFormatOpts;
|
||||
use crate::{
|
||||
commands::{
|
||||
app::util::{AppIdent, AppIdentOpts},
|
||||
AsyncCliCommand,
|
||||
},
|
||||
config::WasmerEnv,
|
||||
};
|
||||
|
||||
/// Rotate the secrets linked to volumes of an app.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppVolumesRotateSecrets {
|
||||
#[clap(flatten)]
|
||||
pub env: WasmerEnv,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub ident: AppIdentOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub fmt: ItemFormatOpts,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAppVolumesRotateSecrets {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
let client = self.env.client()?;
|
||||
let (_ident, app) = self.ident.load_app(&client).await?;
|
||||
|
||||
wasmer_api::query::rotate_s3_secrets(&client, app.id).await?;
|
||||
|
||||
// Don't print it here and leave it implied, so users can append the output
|
||||
// of `wasmer app volumes credentials` without worrying about this message.
|
||||
//
|
||||
//println!(
|
||||
// "Correctly rotated s3 secrets for app {} ({})",
|
||||
// app.name,
|
||||
// app.owner.global_name.bold()
|
||||
//);
|
||||
|
||||
super::CmdAppVolumesCredentials {
|
||||
env: self.env,
|
||||
fmt: self.fmt,
|
||||
ident: AppIdentOpts {
|
||||
app: Some(AppIdent::NamespacedName(app.owner.global_name, app.name)),
|
||||
},
|
||||
}
|
||||
.run_async()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
38
lib/cli/src/commands/app/volumes/list.rs
Normal file
38
lib/cli/src/commands/app/volumes/list.rs
Normal file
@ -0,0 +1,38 @@
|
||||
//! List volumes tied to an edge app.
|
||||
|
||||
use super::super::util::AppIdentOpts;
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ListFormatOpts};
|
||||
|
||||
/// List the volumes of an app.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdAppVolumesList {
|
||||
#[clap(flatten)]
|
||||
fmt: ListFormatOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
env: WasmerEnv,
|
||||
|
||||
#[clap(flatten)]
|
||||
ident: AppIdentOpts,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAppVolumesList {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.env.client()?;
|
||||
|
||||
let (_ident, app) = self.ident.load_app(&client).await?;
|
||||
let volumes =
|
||||
wasmer_api::query::get_app_volumes(&client, &app.owner.global_name, &app.name).await?;
|
||||
|
||||
if volumes.is_empty() {
|
||||
eprintln!("App {} has no volumes!", app.name);
|
||||
} else {
|
||||
println!("{}", self.fmt.format.render(volumes.as_slice()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
34
lib/cli/src/commands/app/volumes/mod.rs
Normal file
34
lib/cli/src/commands/app/volumes/mod.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use crate::commands::AsyncCliCommand;
|
||||
|
||||
pub mod credentials;
|
||||
pub mod list;
|
||||
|
||||
/// App volume management.
|
||||
#[derive(Debug, clap::Parser)]
|
||||
pub enum CmdAppVolumes {
|
||||
Credentials(credentials::CmdAppVolumesCredentials),
|
||||
List(list::CmdAppVolumesList),
|
||||
RotateSecrets(credentials::rotate_secrets::CmdAppVolumesRotateSecrets),
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAppVolumes {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
match self {
|
||||
Self::Credentials(c) => {
|
||||
c.run_async().await?;
|
||||
Ok(())
|
||||
}
|
||||
Self::RotateSecrets(c) => {
|
||||
c.run_async().await?;
|
||||
Ok(())
|
||||
}
|
||||
Self::List(c) => {
|
||||
c.run_async().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
146
lib/cli/src/commands/auth/login/auth_server.rs
Normal file
146
lib/cli/src/commands/auth/login/auth_server.rs
Normal file
@ -0,0 +1,146 @@
|
||||
use super::AuthorizationState;
|
||||
use http_body_util::BodyExt;
|
||||
use hyper::{body::Incoming, Request, Response, StatusCode};
|
||||
use reqwest::{Body, Method};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
/// A utility struct used to manage the local server for browser-based authorization.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct BrowserAuthContext {
|
||||
pub server_shutdown_tx: tokio::sync::mpsc::Sender<bool>,
|
||||
pub token_tx: tokio::sync::mpsc::Sender<AuthorizationState>,
|
||||
}
|
||||
|
||||
/// Payload from the frontend after the user has authenticated.
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(super) enum TokenStatus {
|
||||
/// Signifying that the token is cancelled
|
||||
Cancelled,
|
||||
/// Signifying that the token is authorized
|
||||
Authorized,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) async fn setup_listener() -> Result<(TcpListener, String), anyhow::Error> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let addr = listener.local_addr()?;
|
||||
let port = addr.port();
|
||||
|
||||
let server_url = format!("http://localhost:{}", port);
|
||||
|
||||
Ok((listener, server_url))
|
||||
}
|
||||
|
||||
/// Payload from the frontend after the user has authenticated.
|
||||
///
|
||||
/// This has the token that we need to set in the WASMER_TOML file.
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
pub(super) struct ValidatedNonceOutput {
|
||||
/// Token Received from the frontend
|
||||
pub token: Option<String>,
|
||||
/// Status of the token , whether it is authorized or cancelled
|
||||
pub status: TokenStatus,
|
||||
}
|
||||
|
||||
pub(super) async fn service_router(
|
||||
context: BrowserAuthContext,
|
||||
req: Request<Incoming>,
|
||||
) -> Result<Response<Body>, anyhow::Error> {
|
||||
match *req.method() {
|
||||
Method::OPTIONS => preflight(req).await,
|
||||
Method::POST => handle_post_save_token(context, req).await,
|
||||
_ => handle_unknown_method(context).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn preflight(_: Request<Incoming>) -> Result<Response<Body>, anyhow::Error> {
|
||||
let response = Response::builder()
|
||||
.status(http::StatusCode::OK)
|
||||
.header("Access-Control-Allow-Origin", "*") // FIXME: this is not secure, Don't allow all origins. @syrusakbary
|
||||
.header("Access-Control-Allow-Headers", "Content-Type")
|
||||
.header("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
.body(Body::default())?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_post_save_token(
|
||||
context: BrowserAuthContext,
|
||||
req: Request<Incoming>,
|
||||
) -> Result<Response<Body>, anyhow::Error> {
|
||||
let BrowserAuthContext {
|
||||
server_shutdown_tx,
|
||||
token_tx,
|
||||
} = context;
|
||||
let (.., body) = req.into_parts();
|
||||
let body = body.collect().await?.to_bytes();
|
||||
|
||||
let ValidatedNonceOutput {
|
||||
token,
|
||||
status: token_status,
|
||||
} = serde_json::from_slice::<ValidatedNonceOutput>(&body)?;
|
||||
|
||||
// send the AuthorizationState based on token_status to the main thread and get the response message
|
||||
let (response_message, parse_failure) = match token_status {
|
||||
TokenStatus::Cancelled => {
|
||||
token_tx
|
||||
.send(AuthorizationState::Cancelled)
|
||||
.await
|
||||
.expect("Failed to send token");
|
||||
|
||||
("Token Cancelled by the user", false)
|
||||
}
|
||||
TokenStatus::Authorized => {
|
||||
if let Some(token) = token {
|
||||
token_tx
|
||||
.send(AuthorizationState::TokenSuccess(token.clone()))
|
||||
.await
|
||||
.expect("Failed to send token");
|
||||
("Token Authorized", false)
|
||||
} else {
|
||||
("Token not found", true)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
server_shutdown_tx
|
||||
.send(true)
|
||||
.await
|
||||
.expect("Failed to send shutdown signal");
|
||||
|
||||
let status = if parse_failure {
|
||||
StatusCode::BAD_REQUEST
|
||||
} else {
|
||||
StatusCode::OK
|
||||
};
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(status)
|
||||
.header("Access-Control-Allow-Origin", "*") // FIXME: this is not secure, Don't allow all origins. @syrusakbary
|
||||
.header("Access-Control-Allow-Headers", "Content-Type")
|
||||
.header("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
.body(Body::from(response_message))?)
|
||||
}
|
||||
|
||||
async fn handle_unknown_method(
|
||||
context: BrowserAuthContext,
|
||||
) -> Result<Response<Body>, anyhow::Error> {
|
||||
let BrowserAuthContext {
|
||||
server_shutdown_tx,
|
||||
token_tx,
|
||||
} = context;
|
||||
|
||||
token_tx
|
||||
.send(AuthorizationState::UnknownMethod)
|
||||
.await
|
||||
.expect("Failed to send token");
|
||||
|
||||
server_shutdown_tx
|
||||
.send(true)
|
||||
.await
|
||||
.expect("Failed to send shutdown signal");
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::METHOD_NOT_ALLOWED)
|
||||
.body(Body::from("Method not allowed"))?)
|
||||
}
|
405
lib/cli/src/commands/auth/login/mod.rs
Normal file
405
lib/cli/src/commands/auth/login/mod.rs
Normal file
@ -0,0 +1,405 @@
|
||||
mod auth_server;
|
||||
use auth_server::*;
|
||||
use colored::Colorize;
|
||||
use hyper::{server::conn::http1::Builder, service::service_fn};
|
||||
use hyper_util::server::graceful::GracefulShutdown;
|
||||
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
config::{UpdateRegistry, UserRegistry, WasmerConfig, WasmerEnv},
|
||||
};
|
||||
use futures_util::{stream::FuturesUnordered, StreamExt};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
use wasmer_api::{types::Nonce, WasmerClient};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum AuthorizationState {
|
||||
TokenSuccess(String),
|
||||
Cancelled,
|
||||
TimedOut,
|
||||
UnknownMethod,
|
||||
}
|
||||
|
||||
/// Subcommand for log in a user into Wasmer (using a browser or provided a token)
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct Login {
|
||||
/// Variable to login without opening a browser
|
||||
#[clap(long, name = "no-browser", default_value = "false")]
|
||||
pub no_browser: bool,
|
||||
|
||||
// This is a copy of [`WasmerEnv`] to allow users to specify
|
||||
// the token as a parameter rather than as a flag.
|
||||
/// Set Wasmer's home directory
|
||||
#[clap(long, env = "WASMER_DIR", default_value = crate::config::DEFAULT_WASMER_DIR.as_os_str())]
|
||||
pub wasmer_dir: PathBuf,
|
||||
|
||||
/// The directory cached artefacts are saved to.
|
||||
#[clap(long, env = "WASMER_CACHE_DIR", default_value = crate::config::DEFAULT_WASMER_CACHE_DIR.as_os_str())]
|
||||
pub cache_dir: PathBuf,
|
||||
|
||||
/// The API token to use when communicating with the registry (inferred from the environment by default)
|
||||
#[clap(env = "WASMER_TOKEN")]
|
||||
pub token: Option<String>,
|
||||
|
||||
/// Change the current registry
|
||||
#[clap(long, env = "WASMER_REGISTRY")]
|
||||
pub registry: Option<UserRegistry>,
|
||||
}
|
||||
|
||||
impl Login {
|
||||
fn get_token_from_env_or_user(
|
||||
&self,
|
||||
env: &WasmerEnv,
|
||||
) -> Result<AuthorizationState, anyhow::Error> {
|
||||
if let Some(token) = &self.token {
|
||||
return Ok(AuthorizationState::TokenSuccess(token.clone()));
|
||||
}
|
||||
|
||||
let registry_host = env.registry_endpoint()?;
|
||||
let registry_tld = tldextract::TldExtractor::new(tldextract::TldOption::default())
|
||||
.extract(registry_host.as_str())
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Invalid registry for login {}: {e}", registry_host),
|
||||
)
|
||||
})?;
|
||||
|
||||
let login_prompt = match (
|
||||
registry_tld.domain.as_deref(),
|
||||
registry_tld.suffix.as_deref(),
|
||||
) {
|
||||
(Some(d), Some(s)) => {
|
||||
format!("Please paste the login token from https://{d}.{s}/settings/access-tokens")
|
||||
}
|
||||
_ => "Please paste the login token".to_string(),
|
||||
};
|
||||
#[cfg(test)]
|
||||
{
|
||||
Ok(AuthorizationState::TokenSuccess(login_prompt))
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
let token = dialoguer::Input::new()
|
||||
.with_prompt(&login_prompt)
|
||||
.interact_text()?;
|
||||
Ok(AuthorizationState::TokenSuccess(token))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_token_from_browser(
|
||||
&self,
|
||||
client: &WasmerClient,
|
||||
) -> anyhow::Result<AuthorizationState> {
|
||||
let (listener, server_url) = setup_listener().await?;
|
||||
|
||||
let (server_shutdown_tx, mut server_shutdown_rx) = tokio::sync::mpsc::channel::<bool>(1);
|
||||
let (token_tx, mut token_rx) = tokio::sync::mpsc::channel::<AuthorizationState>(1);
|
||||
|
||||
// Create a new AppContext
|
||||
let app_context = BrowserAuthContext {
|
||||
server_shutdown_tx,
|
||||
token_tx,
|
||||
};
|
||||
|
||||
let Nonce { auth_url, .. } =
|
||||
wasmer_api::query::create_nonce(client, "wasmer-cli".to_string(), server_url)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("The backend did not return any nonce to auth the login!")
|
||||
})?;
|
||||
|
||||
// if failed to open the browser, then don't error out just print the auth_url with a message
|
||||
println!("Opening auth link in your default browser: {}", &auth_url);
|
||||
opener::open_browser(&auth_url).unwrap_or_else(|_| {
|
||||
println!(
|
||||
"⚠️ Failed to open the browser.\n
|
||||
Please open the url: {}",
|
||||
&auth_url
|
||||
);
|
||||
});
|
||||
|
||||
// Jump through hyper 1.0's hoops...
|
||||
let graceful = GracefulShutdown::new();
|
||||
|
||||
let http = Builder::new();
|
||||
|
||||
let mut futs = FuturesUnordered::new();
|
||||
|
||||
let service = service_fn(move |req| service_router(app_context.clone(), req));
|
||||
|
||||
print!("Waiting for session... ");
|
||||
|
||||
// start the server
|
||||
loop {
|
||||
tokio::select! {
|
||||
Result::Ok((stream, _addr)) = listener.accept() => {
|
||||
let io = hyper_util::rt::tokio::TokioIo::new(stream);
|
||||
let conn = http.serve_connection(io, service.clone());
|
||||
// watch this connection
|
||||
let fut = graceful.watch(conn);
|
||||
futs.push(async move {
|
||||
if let Err(e) = fut.await {
|
||||
eprintln!("Error serving connection: {:?}", e);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_ = futs.next() => {}
|
||||
|
||||
_ = server_shutdown_rx.recv() => {
|
||||
// stop the accept loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// receive the token from the server
|
||||
let token = token_rx
|
||||
.recv()
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("❌ Failed to receive token from localhost"))?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
async fn do_login(&self, env: &WasmerEnv) -> anyhow::Result<AuthorizationState> {
|
||||
let client = env.client_unauthennticated()?;
|
||||
|
||||
let should_login = if let Some(user) = wasmer_api::query::current_user(&client).await? {
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
println!(
|
||||
"You are already logged in as {} in registry {}.",
|
||||
user.username.bold(),
|
||||
env.registry_public_url()?.host_str().unwrap().bold()
|
||||
);
|
||||
let theme = dialoguer::theme::ColorfulTheme::default();
|
||||
let dialog = dialoguer::Confirm::with_theme(&theme).with_prompt("Login again?");
|
||||
|
||||
dialog.interact()?
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
// prevent unused binding warning
|
||||
_ = user;
|
||||
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if !should_login {
|
||||
Ok(AuthorizationState::Cancelled)
|
||||
} else if self.no_browser {
|
||||
self.get_token_from_env_or_user(env)
|
||||
} else {
|
||||
// switch between two methods of getting the token.
|
||||
// start two async processes, 10 minute timeout and get token from browser. Whichever finishes first, use that.
|
||||
let timeout_future = tokio::time::sleep(Duration::from_secs(60 * 10));
|
||||
tokio::select! {
|
||||
_ = timeout_future => {
|
||||
Ok(AuthorizationState::TimedOut)
|
||||
},
|
||||
token = self.get_token_from_browser(&client) => {
|
||||
token
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_and_save(&self, env: &WasmerEnv, token: String) -> anyhow::Result<String> {
|
||||
let registry = env.registry_endpoint()?;
|
||||
let mut config = WasmerConfig::from_file(env.dir())
|
||||
.map_err(|e| anyhow::anyhow!("config from file: {e}"))?;
|
||||
config
|
||||
.registry
|
||||
.set_current_registry(registry.as_ref())
|
||||
.await;
|
||||
config.registry.set_login_token_for_registry(
|
||||
&config.registry.get_current_registry(),
|
||||
&token,
|
||||
UpdateRegistry::Update,
|
||||
);
|
||||
let path = WasmerConfig::get_file_location(env.dir());
|
||||
config.save(path)?;
|
||||
|
||||
// This will automatically read the config again, picking up the new edits.
|
||||
let client = env.client()?;
|
||||
|
||||
wasmer_api::query::current_user(&client)
|
||||
.await?
|
||||
.map(|v| v.username)
|
||||
.ok_or_else(|| anyhow::anyhow!("Not logged in!"))
|
||||
}
|
||||
|
||||
pub(crate) fn get_wasmer_env(&self) -> WasmerEnv {
|
||||
WasmerEnv::new(
|
||||
self.wasmer_dir.clone(),
|
||||
self.cache_dir.clone(),
|
||||
self.token.clone(),
|
||||
self.registry.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for Login {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
let env = self.get_wasmer_env();
|
||||
|
||||
let auth_state = match &self.token {
|
||||
Some(token) => AuthorizationState::TokenSuccess(token.clone()),
|
||||
None => self.do_login(&env).await?,
|
||||
};
|
||||
|
||||
match auth_state {
|
||||
AuthorizationState::TokenSuccess(token) => {
|
||||
match self.login_and_save(&env, token).await {
|
||||
Ok(s) => {
|
||||
print!("Done!");
|
||||
println!("\n{} Login for Wasmer user {:?} saved","✔".green().bold(), s)
|
||||
}
|
||||
Err(_) => print!(
|
||||
"Warning: no user found on {:?} with the provided token.\nToken saved regardless.",
|
||||
env.registry_public_url()
|
||||
),
|
||||
}
|
||||
}
|
||||
AuthorizationState::TimedOut => {
|
||||
print!("Timed out (10 mins exceeded)");
|
||||
}
|
||||
AuthorizationState::Cancelled => {
|
||||
println!("Cancelled by the user");
|
||||
}
|
||||
AuthorizationState::UnknownMethod => {
|
||||
println!("Error: unknown method\n");
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::CommandFactory;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::commands::CliCommand;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn interactive_login() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let login = Login {
|
||||
no_browser: true,
|
||||
registry: Some("wasmer.wtf".into()),
|
||||
wasmer_dir: temp.path().to_path_buf(),
|
||||
token: None,
|
||||
cache_dir: temp.path().join("cache").to_path_buf(),
|
||||
};
|
||||
let env = login.get_wasmer_env();
|
||||
|
||||
let token = login.get_token_from_env_or_user(&env).unwrap();
|
||||
match token {
|
||||
AuthorizationState::TokenSuccess(token) => {
|
||||
assert_eq!(
|
||||
token,
|
||||
"Please paste the login token from https://wasmer.wtf/settings/access-tokens"
|
||||
);
|
||||
}
|
||||
AuthorizationState::Cancelled
|
||||
| AuthorizationState::TimedOut
|
||||
| AuthorizationState::UnknownMethod => {
|
||||
panic!("Should not reach here")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_with_token() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let login = Login {
|
||||
no_browser: true,
|
||||
registry: Some("wasmer.wtf".into()),
|
||||
wasmer_dir: temp.path().to_path_buf(),
|
||||
token: Some("abc".to_string()),
|
||||
cache_dir: temp.path().join("cache").to_path_buf(),
|
||||
};
|
||||
let env = login.get_wasmer_env();
|
||||
|
||||
let token = login.get_token_from_env_or_user(&env).unwrap();
|
||||
|
||||
match token {
|
||||
AuthorizationState::TokenSuccess(token) => {
|
||||
assert_eq!(token, "abc");
|
||||
}
|
||||
AuthorizationState::Cancelled
|
||||
| AuthorizationState::TimedOut
|
||||
| AuthorizationState::UnknownMethod => {
|
||||
panic!("Should not reach here")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_sync_with_wasmer_env() {
|
||||
let wasmer_env = WasmerEnv::command();
|
||||
let login = Login::command();
|
||||
|
||||
// All options except --token should be the same
|
||||
let wasmer_env_opts: Vec<_> = wasmer_env
|
||||
.get_opts()
|
||||
.filter(|arg| arg.get_id() != "token")
|
||||
.collect();
|
||||
let login_opts: Vec<_> = login.get_opts().collect();
|
||||
|
||||
assert_eq!(wasmer_env_opts, login_opts);
|
||||
|
||||
// The token argument should have the same message, even if it is an
|
||||
// argument rather than a --flag.
|
||||
let wasmer_env_token_help = wasmer_env
|
||||
.get_opts()
|
||||
.find(|arg| arg.get_id() == "token")
|
||||
.unwrap()
|
||||
.get_help()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let login_token_help = login
|
||||
.get_positionals()
|
||||
.find(|arg| arg.get_id() == "token")
|
||||
.unwrap()
|
||||
.get_help()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert_eq!(wasmer_env_token_help, login_token_help);
|
||||
}
|
||||
|
||||
/// Regression test for panics on API errors.
|
||||
/// See https://github.com/wasmerio/wasmer/issues/4147.
|
||||
#[test]
|
||||
fn login_with_invalid_token_does_not_panic() {
|
||||
let cmd = Login {
|
||||
no_browser: true,
|
||||
wasmer_dir: crate::config::DEFAULT_WASMER_DIR.clone(),
|
||||
registry: Some("http://localhost:11".to_string().into()),
|
||||
token: Some("invalid".to_string()),
|
||||
cache_dir: crate::config::DEFAULT_WASMER_CACHE_DIR.clone(),
|
||||
};
|
||||
|
||||
let res = cmd.run();
|
||||
// The CLI notices that either the registry is unreachable or the token is not tied to any
|
||||
// user. It shows a warning to the user, but does not return with an error code.
|
||||
//
|
||||
// ------ i.e. this will fail
|
||||
// |
|
||||
// v
|
||||
// assert!(res.is_err());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
}
|
110
lib/cli/src/commands/auth/logout.rs
Normal file
110
lib/cli/src/commands/auth/logout.rs
Normal file
@ -0,0 +1,110 @@
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
config::{WasmerConfig, WasmerEnv, DEFAULT_PROD_REGISTRY},
|
||||
};
|
||||
use colored::Colorize;
|
||||
use is_terminal::IsTerminal;
|
||||
|
||||
/// Subcommand for log in a user into Wasmer (using a browser or provided a token)
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct Logout {
|
||||
#[clap(flatten)]
|
||||
env: WasmerEnv,
|
||||
|
||||
/// Do not prompt for user input.
|
||||
#[clap(long, default_value_t = !std::io::stdin().is_terminal())]
|
||||
pub non_interactive: bool,
|
||||
|
||||
/// Whether or not to revoke the associated token
|
||||
#[clap(long)]
|
||||
pub revoke_token: bool,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for Logout {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
let registry = self.env.registry_endpoint()?.to_string();
|
||||
let host_str = self
|
||||
.env
|
||||
.registry_public_url()
|
||||
.map_err(|_| anyhow::anyhow!("No registry not specified!"))?
|
||||
.host_str()
|
||||
.unwrap()
|
||||
.bold();
|
||||
|
||||
let client = self
|
||||
.env
|
||||
.client()
|
||||
.map_err(|_| anyhow::anyhow!("Not logged into registry {host_str}"))?;
|
||||
|
||||
let user = wasmer_api::query::current_user(&client)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Not logged into registry {host_str}: {e}"))?
|
||||
.ok_or_else(|| anyhow::anyhow!("Not logged into registry {host_str}"))?;
|
||||
|
||||
let theme = dialoguer::theme::ColorfulTheme::default();
|
||||
let prompt = dialoguer::Confirm::with_theme(&theme).with_prompt(format!(
|
||||
"Log user {} out of registry {host_str}?",
|
||||
user.username
|
||||
));
|
||||
|
||||
if prompt.interact()? || self.non_interactive {
|
||||
let mut config = self.env.config()?;
|
||||
let token = config
|
||||
.registry
|
||||
.get_login_token_for_registry(®istry)
|
||||
.unwrap();
|
||||
config.registry.remove_registry(®istry);
|
||||
if config.registry.is_current_registry(®istry) {
|
||||
if config.registry.tokens.is_empty() {
|
||||
_ = config
|
||||
.registry
|
||||
.set_current_registry(DEFAULT_PROD_REGISTRY)
|
||||
.await;
|
||||
} else {
|
||||
let new_reg = config.registry.tokens[0].registry.clone();
|
||||
_ = config.registry.set_current_registry(&new_reg).await;
|
||||
}
|
||||
}
|
||||
let path = WasmerConfig::get_file_location(self.env.dir());
|
||||
config.save(path)?;
|
||||
|
||||
// Read it again..
|
||||
//
|
||||
let config = self.env.config()?;
|
||||
if config
|
||||
.registry
|
||||
.get_login_token_for_registry(®istry)
|
||||
.is_none()
|
||||
{
|
||||
println!(
|
||||
"User {} correctly logged out of registry {host_str}",
|
||||
user.username.bold()
|
||||
);
|
||||
|
||||
let should_revoke = self.revoke_token || {
|
||||
let theme = dialoguer::theme::ColorfulTheme::default();
|
||||
dialoguer::Confirm::with_theme(&theme)
|
||||
.with_prompt("Revoke token?")
|
||||
.interact()?
|
||||
};
|
||||
|
||||
if should_revoke {
|
||||
wasmer_api::query::revoke_token(&client, token).await?;
|
||||
println!(
|
||||
"Token for user {} in registry {host_str} correctly revoked",
|
||||
user.username.bold()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!("Something went wrong! User is not logged out.")
|
||||
}
|
||||
} else {
|
||||
println!("No action taken.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
29
lib/cli/src/commands/auth/mod.rs
Normal file
29
lib/cli/src/commands/auth/mod.rs
Normal file
@ -0,0 +1,29 @@
|
||||
mod login;
|
||||
mod logout;
|
||||
mod whoami;
|
||||
|
||||
pub use login::*;
|
||||
pub use whoami::*;
|
||||
|
||||
use super::AsyncCliCommand;
|
||||
|
||||
/// Manage your .
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
pub enum CmdAuth {
|
||||
Login(login::Login),
|
||||
Logout(logout::Logout),
|
||||
Whoami(whoami::Whoami),
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for CmdAuth {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
match self {
|
||||
CmdAuth::Login(l) => l.run_async().await,
|
||||
CmdAuth::Logout(l) => l.run_async().await,
|
||||
CmdAuth::Whoami(w) => w.run_async().await,
|
||||
}
|
||||
}
|
||||
}
|
32
lib/cli/src/commands/auth/whoami.rs
Normal file
32
lib/cli/src/commands/auth/whoami.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
|
||||
use crate::config::WasmerEnv;
|
||||
|
||||
use super::AsyncCliCommand;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
/// The options for the `wasmer whoami` subcommand
|
||||
pub struct Whoami {
|
||||
#[clap(flatten)]
|
||||
env: WasmerEnv,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCliCommand for Whoami {
|
||||
type Output = ();
|
||||
|
||||
/// Execute `wasmer whoami`
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
|
||||
let client = self.env.client_unauthennticated()?;
|
||||
let host_str = self.env.registry_public_url()?.host_str().unwrap().bold();
|
||||
let user = wasmer_api::query::current_user(&client)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("Not logged in registry {host_str}"))?;
|
||||
println!(
|
||||
"Logged into registry {host_str} as user {}",
|
||||
user.username.bold()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -57,9 +57,9 @@ impl Compile {
|
||||
Target::new(target_triple.clone(), features)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let (store, compiler_type) = self.store.get_store_for_target(target.clone())?;
|
||||
let (mut store, compiler_type) = self.store.get_store_for_target(target.clone())?;
|
||||
|
||||
let mut engine = store.engine().clone();
|
||||
let engine = store.engine_mut();
|
||||
let hash_algorithm = self.hash_algorithm.unwrap_or_default().into();
|
||||
engine.set_hash_algorithm(Some(hash_algorithm));
|
||||
|
||||
@ -80,7 +80,7 @@ impl Compile {
|
||||
warning!("the output file has no extension. We recommend using `{}.{}` for the chosen target", &output_filename, &recommended_extension)
|
||||
}
|
||||
}
|
||||
println!("Compiler: {}", compiler_type.to_string());
|
||||
println!("Compiler: {}", compiler_type);
|
||||
println!("Target: {}", target.triple());
|
||||
|
||||
let module = Module::from_file(&store, &self.path)?;
|
||||
|
@ -1,13 +1,14 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::config::WasmerEnv;
|
||||
|
||||
use super::AsyncCliCommand;
|
||||
use crate::opts::ApiOpts;
|
||||
|
||||
/// Connects to the Wasmer Edge distributed network.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdConnect {
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
|
||||
/// Runs in promiscuous mode
|
||||
#[clap(long)]
|
||||
|
@ -1,118 +1,9 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
use dialoguer::console::{style, Emoji};
|
||||
use indicatif::ProgressBar;
|
||||
|
||||
/// Extract contents of a container to a directory.
|
||||
/// RENAMED: the 'container unpack' command has been renamed to 'package unpack'!
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct PackageUnpack {
|
||||
/// The output directory.
|
||||
#[clap(short = 'o', long)]
|
||||
out_dir: PathBuf,
|
||||
|
||||
/// Overwrite existing directories/files.
|
||||
#[clap(long)]
|
||||
overwrite: bool,
|
||||
|
||||
/// Run the unpack command without any output
|
||||
#[clap(long)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Path to the package.
|
||||
package_path: PathBuf,
|
||||
}
|
||||
|
||||
static PACKAGE_EMOJI: Emoji<'_, '_> = Emoji("📦 ", "");
|
||||
static EXTRACTED_TO_EMOJI: Emoji<'_, '_> = Emoji("📂 ", "");
|
||||
pub struct PackageUnpack {}
|
||||
|
||||
impl PackageUnpack {
|
||||
pub(crate) fn execute(&self) -> Result<(), anyhow::Error> {
|
||||
// Setup the progress bar
|
||||
let pb = if self.quiet {
|
||||
ProgressBar::hidden()
|
||||
} else {
|
||||
ProgressBar::new_spinner()
|
||||
};
|
||||
|
||||
pb.println(format!(
|
||||
"{} {}Unpacking...",
|
||||
style("[1/2]").bold().dim(),
|
||||
PACKAGE_EMOJI
|
||||
));
|
||||
|
||||
let pkg = webc::compat::Container::from_disk(&self.package_path).with_context(|| {
|
||||
format!(
|
||||
"could not open package at '{}'",
|
||||
self.package_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
let outdir = &self.out_dir;
|
||||
std::fs::create_dir_all(outdir)
|
||||
.with_context(|| format!("could not create output directory '{}'", outdir.display()))?;
|
||||
|
||||
pkg.unpack(outdir, self.overwrite)
|
||||
.with_context(|| "could not extract package".to_string())?;
|
||||
|
||||
pb.println(format!(
|
||||
"{} {}Extracted package contents to '{}'",
|
||||
style("[2/2]").bold().dim(),
|
||||
EXTRACTED_TO_EMOJI,
|
||||
self.out_dir.display()
|
||||
));
|
||||
|
||||
pb.finish();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Download a package from the dev registry.
|
||||
#[test]
|
||||
fn test_cmd_package_extract() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let package_path = std::env::var("CARGO_MANIFEST_DIR").map(PathBuf::from).unwrap()
|
||||
.parent().unwrap()
|
||||
.parent().unwrap()
|
||||
.join("tests/integration/cli/tests/webc/hello-0.1.0-665d2ddc-80e6-4845-85d3-4587b1693bb7.webc");
|
||||
|
||||
assert!(package_path.is_file());
|
||||
|
||||
let cmd = PackageUnpack {
|
||||
out_dir: dir.path().to_owned(),
|
||||
overwrite: false,
|
||||
package_path,
|
||||
quiet: true,
|
||||
};
|
||||
|
||||
cmd.execute().unwrap();
|
||||
|
||||
let mut items = std::fs::read_dir(dir.path())
|
||||
.unwrap()
|
||||
.map(|x| {
|
||||
x.unwrap()
|
||||
.path()
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.sort();
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![
|
||||
"atom".to_string(),
|
||||
"manifest.json".to_string(),
|
||||
"metadata".to_string(),
|
||||
]
|
||||
);
|
||||
anyhow::bail!("This command was renamed: use 'wasmer package unpack instead'");
|
||||
}
|
||||
}
|
||||
|
@ -227,7 +227,7 @@ impl CreateExe {
|
||||
let hash_algorithm = self.hash_algorithm.unwrap_or_default().into();
|
||||
engine.set_hash_algorithm(Some(hash_algorithm));
|
||||
|
||||
println!("Compiler: {}", compiler_type.to_string());
|
||||
println!("Compiler: {}", compiler_type);
|
||||
println!("Target: {}", target.triple());
|
||||
println!(
|
||||
"Using path `{}` as libwasmer path.",
|
||||
@ -548,7 +548,7 @@ fn serialize_volume_to_webc_v1(volume: &WebcVolume) -> Vec<u8> {
|
||||
impl AllowMultiWasm {
|
||||
fn validate(
|
||||
&self,
|
||||
all_atoms: &Vec<(String, webc::compat::SharedBytes)>,
|
||||
all_atoms: &[(String, webc::compat::SharedBytes)],
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if matches!(self, AllowMultiWasm::Reject(None)) && all_atoms.len() > 1 {
|
||||
let keys = all_atoms
|
||||
|
@ -80,7 +80,7 @@ impl CreateObj {
|
||||
&self.cpu_features,
|
||||
);
|
||||
let (_, compiler_type) = self.compiler.get_store_for_target(target.clone())?;
|
||||
println!("Compiler: {}", compiler_type.to_string());
|
||||
println!("Compiler: {}", compiler_type);
|
||||
println!("Target: {}", target.triple());
|
||||
|
||||
let atoms = if let Ok(webc) = webc::compat::Container::from_disk(&input_path) {
|
||||
|
@ -1,15 +1,13 @@
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
opts::{ApiOpts, ItemTableFormatOpts},
|
||||
};
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ItemTableFormatOpts};
|
||||
|
||||
/// Show a domain
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdDomainGet {
|
||||
#[clap(flatten)]
|
||||
fmt: ItemTableFormatOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
|
||||
/// Name of the domain.
|
||||
name: String,
|
||||
@ -20,7 +18,7 @@ impl AsyncCliCommand for CmdDomainGet {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
if let Some(domain) = wasmer_api::query::get_domain_with_records(&client, self.name).await?
|
||||
{
|
||||
println!("{}", self.fmt.format.render(&domain));
|
||||
|
@ -1,17 +1,15 @@
|
||||
use wasmer_api::types::GetAllDomainsVariables;
|
||||
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
opts::{ApiOpts, ListFormatOpts},
|
||||
};
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ListFormatOpts};
|
||||
|
||||
/// List domains.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdDomainList {
|
||||
#[clap(flatten)]
|
||||
fmt: ListFormatOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
|
||||
/// Name of the namespace.
|
||||
namespace: Option<String>,
|
||||
@ -22,7 +20,7 @@ impl AsyncCliCommand for CmdDomainList {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
let domains = wasmer_api::query::get_all_domains(
|
||||
&client,
|
||||
GetAllDomainsVariables {
|
||||
|
@ -1,15 +1,13 @@
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
opts::{ApiOpts, ItemTableFormatOpts},
|
||||
};
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ItemTableFormatOpts};
|
||||
|
||||
/// Show a domain
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdDomainRegister {
|
||||
#[clap(flatten)]
|
||||
fmt: ItemTableFormatOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
|
||||
/// Name of the domain.
|
||||
name: String,
|
||||
@ -28,7 +26,7 @@ impl AsyncCliCommand for CmdDomainRegister {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
let domain = wasmer_api::query::register_domain(
|
||||
&client,
|
||||
self.name,
|
||||
|
@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
opts::{ApiOpts, ItemFormatOpts},
|
||||
};
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ItemFormatOpts};
|
||||
use anyhow::Context;
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
@ -11,7 +8,7 @@ pub struct CmdZoneFileGet {
|
||||
fmt: ItemFormatOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
|
||||
/// Name of the domain.
|
||||
domain_name: String,
|
||||
@ -25,7 +22,7 @@ pub struct CmdZoneFileGet {
|
||||
/// Show a zone file
|
||||
pub struct CmdZoneFileSync {
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
|
||||
/// filename of zone-file to sync
|
||||
zone_file_path: String,
|
||||
@ -40,7 +37,7 @@ impl AsyncCliCommand for CmdZoneFileGet {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
if let Some(domain) =
|
||||
wasmer_api::query::get_domain_zone_file(&client, self.domain_name).await?
|
||||
{
|
||||
@ -66,7 +63,7 @@ impl AsyncCliCommand for CmdZoneFileSync {
|
||||
let data = std::fs::read(&self.zone_file_path).context("Unable to read file")?;
|
||||
let zone_file_contents = String::from_utf8(data).context("Not a valid UTF-8 sequence")?;
|
||||
let domain = wasmer_api::query::upsert_domain_from_zone_file(
|
||||
&self.api.client()?,
|
||||
&self.env.client()?,
|
||||
zone_file_contents,
|
||||
!self.no_delete_missing_records,
|
||||
)
|
||||
|
@ -349,7 +349,7 @@ impl GetBindingsResult {
|
||||
fn first_binding(&self) -> Option<wasmer_config::package::Bindings> {
|
||||
match self {
|
||||
Self::OneBinding(s) => Some(s.clone()),
|
||||
Self::MultiBindings(s) => s.get(0).cloned(),
|
||||
Self::MultiBindings(s) => s.first().cloned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -424,7 +424,7 @@ fn construct_manifest(
|
||||
.collect::<Vec<_>>()
|
||||
.join("\r\n");
|
||||
|
||||
let msg = vec![
|
||||
let msg = [
|
||||
String::new(),
|
||||
" It looks like your project contains multiple *.wai files.".to_string(),
|
||||
" Make sure you update the [[module.bindings]] appropriately".to_string(),
|
||||
|
@ -1,546 +0,0 @@
|
||||
use std::{net::TcpListener, path::PathBuf, str::FromStr, time::Duration};
|
||||
|
||||
use anyhow::Ok;
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
#[cfg(not(test))]
|
||||
use dialoguer::{console::style, Input};
|
||||
use hyper::{
|
||||
service::{make_service_fn, service_fn},
|
||||
Body, Request, Response, Server, StatusCode,
|
||||
};
|
||||
use reqwest::Method;
|
||||
use serde::Deserialize;
|
||||
use wasmer_registry::{
|
||||
types::NewNonceOutput,
|
||||
wasmer_env::{Registry, WasmerEnv, WASMER_DIR},
|
||||
RegistryClient,
|
||||
};
|
||||
|
||||
const WASMER_CLI: &str = "wasmer-cli";
|
||||
|
||||
/// Payload from the frontend after the user has authenticated.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TokenStatus {
|
||||
/// Signifying that the token is cancelled
|
||||
Cancelled,
|
||||
/// Signifying that the token is authorized
|
||||
Authorized,
|
||||
}
|
||||
|
||||
/// Payload from the frontend after the user has authenticated.
|
||||
///
|
||||
/// This has the token that we need to set in the WASMER_TOML file.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct ValidatedNonceOutput {
|
||||
/// Token Received from the frontend
|
||||
pub token: Option<String>,
|
||||
/// Status of the token , whether it is authorized or cancelled
|
||||
pub status: TokenStatus,
|
||||
}
|
||||
|
||||
/// Enum for the boolean like prompt options
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum BoolPromptOptions {
|
||||
/// Signifying a yes/true - using `y/Y`
|
||||
Yes,
|
||||
/// Signifying a No/false - using `n/N`
|
||||
No,
|
||||
}
|
||||
|
||||
impl FromStr for BoolPromptOptions {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"y" | "Y" => Ok(BoolPromptOptions::Yes),
|
||||
"n" | "N" => Ok(BoolPromptOptions::No),
|
||||
_ => Err(anyhow::anyhow!("Invalid option")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for BoolPromptOptions {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
BoolPromptOptions::Yes => "y".to_string(),
|
||||
BoolPromptOptions::No => "n".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Token = String;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum AuthorizationState {
|
||||
TokenSuccess(Token),
|
||||
Cancelled,
|
||||
TimedOut,
|
||||
UnknownMethod,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppContext {
|
||||
server_shutdown_tx: tokio::sync::mpsc::Sender<bool>,
|
||||
token_tx: tokio::sync::mpsc::Sender<AuthorizationState>,
|
||||
}
|
||||
|
||||
/// Subcommand for log in a user into Wasmer (using a browser or provided a token)
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct Login {
|
||||
/// Variable to login without opening a browser
|
||||
#[clap(long, name = "no-browser", default_value = "false")]
|
||||
pub no_browser: bool,
|
||||
// Note: This is essentially a copy of WasmerEnv except the token is
|
||||
// accepted as a main argument instead of via --token.
|
||||
/// Set Wasmer's home directory
|
||||
#[clap(long, env = "WASMER_DIR", default_value = WASMER_DIR.as_os_str())]
|
||||
pub wasmer_dir: PathBuf,
|
||||
/// The registry to fetch packages from (inferred from the environment by
|
||||
/// default)
|
||||
#[clap(long, env = "WASMER_REGISTRY")]
|
||||
pub registry: Option<Registry>,
|
||||
/// The API token to use when communicating with the registry (inferred from
|
||||
/// the environment by default)
|
||||
pub token: Option<String>,
|
||||
/// The directory cached artefacts are saved to.
|
||||
#[clap(long, env = "WASMER_CACHE_DIR")]
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Login {
|
||||
fn get_token_or_ask_user(&self, env: &WasmerEnv) -> Result<AuthorizationState, anyhow::Error> {
|
||||
if let Some(token) = &self.token {
|
||||
return Ok(AuthorizationState::TokenSuccess(token.clone()));
|
||||
}
|
||||
|
||||
let registry_host = env.registry_endpoint()?;
|
||||
let registry_tld = tldextract::TldExtractor::new(tldextract::TldOption::default())
|
||||
.extract(registry_host.as_str())
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Invalid registry for login {}: {e}", registry_host),
|
||||
)
|
||||
})?;
|
||||
|
||||
let login_prompt = match (
|
||||
registry_tld.domain.as_deref(),
|
||||
registry_tld.suffix.as_deref(),
|
||||
) {
|
||||
(Some(d), Some(s)) => {
|
||||
format!("Please paste the login token from https://{d}.{s}/settings/access-tokens")
|
||||
}
|
||||
_ => "Please paste the login token".to_string(),
|
||||
};
|
||||
#[cfg(test)]
|
||||
{
|
||||
Ok(AuthorizationState::TokenSuccess(login_prompt))
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
let token = Input::new().with_prompt(&login_prompt).interact_text()?;
|
||||
Ok(AuthorizationState::TokenSuccess(token))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_token_from_browser(
|
||||
&self,
|
||||
env: &WasmerEnv,
|
||||
) -> Result<AuthorizationState, anyhow::Error> {
|
||||
let registry = env.registry_endpoint()?;
|
||||
|
||||
let client = RegistryClient::new(registry.clone(), None, None);
|
||||
|
||||
let (listener, server_url) = Self::setup_listener().await?;
|
||||
|
||||
let (server_shutdown_tx, mut server_shutdown_rx) = tokio::sync::mpsc::channel::<bool>(1);
|
||||
let (token_tx, mut token_rx) = tokio::sync::mpsc::channel::<AuthorizationState>(1);
|
||||
|
||||
// Create a new AppContext
|
||||
let app_context = AppContext {
|
||||
server_shutdown_tx,
|
||||
token_tx,
|
||||
};
|
||||
|
||||
let NewNonceOutput { auth_url } =
|
||||
wasmer_registry::api::create_nonce(&client, WASMER_CLI.to_string(), server_url).await?;
|
||||
|
||||
// if failed to open the browser, then don't error out just print the auth_url with a message
|
||||
println!("Opening auth link in your default browser: {}", &auth_url);
|
||||
opener::open_browser(&auth_url).unwrap_or_else(|_| {
|
||||
println!(
|
||||
"⚠️ Failed to open the browser.\n
|
||||
Please open the url: {}",
|
||||
&auth_url
|
||||
);
|
||||
});
|
||||
|
||||
// Create a new server
|
||||
let make_svc = make_service_fn(move |_| {
|
||||
let context = app_context.clone();
|
||||
|
||||
// Create a `Service` for responding to the request.
|
||||
let service = service_fn(move |req| service_router(context.clone(), req));
|
||||
|
||||
// Return the service to hyper.
|
||||
async move { Ok(service) }
|
||||
});
|
||||
|
||||
print!("Waiting for session... ");
|
||||
|
||||
// start the server
|
||||
Server::from_tcp(listener)?
|
||||
.serve(make_svc)
|
||||
.with_graceful_shutdown(async {
|
||||
server_shutdown_rx.recv().await;
|
||||
})
|
||||
.await?;
|
||||
|
||||
// receive the token from the server
|
||||
let token = token_rx
|
||||
.recv()
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("❌ Failed to receive token from localhost"))?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
fn wasmer_env(&self) -> WasmerEnv {
|
||||
WasmerEnv::new(
|
||||
self.wasmer_dir.clone(),
|
||||
self.registry.clone(),
|
||||
self.token.clone(),
|
||||
self.cache_dir.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn setup_listener() -> Result<(TcpListener, String), anyhow::Error> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")?;
|
||||
let addr = listener.local_addr()?;
|
||||
let port = addr.port();
|
||||
|
||||
let server_url = format!("http://localhost:{}", port);
|
||||
|
||||
Ok((listener, server_url))
|
||||
}
|
||||
|
||||
pub async fn run_async(&self) -> Result<(), anyhow::Error> {
|
||||
let env = self.wasmer_env();
|
||||
let registry = env.registry_endpoint()?;
|
||||
|
||||
let auth_state = match self.token.clone() {
|
||||
Some(token) => Ok(AuthorizationState::TokenSuccess(token)),
|
||||
None => {
|
||||
let person_wants_to_login =
|
||||
match wasmer_registry::whoami(env.dir(), Some(registry.as_str()), None) {
|
||||
std::result::Result::Ok((registry, user)) => {
|
||||
println!(
|
||||
"You are already logged in as {:?} on registry {:?}",
|
||||
user, registry
|
||||
);
|
||||
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
let login_again = Input::new()
|
||||
.with_prompt(format!(
|
||||
"{} {} - [y/{}]",
|
||||
style("?").yellow().bold(),
|
||||
style("Do you want to login again?").bright().bold(),
|
||||
style("N").green().bold()
|
||||
))
|
||||
.show_default(false)
|
||||
.default(BoolPromptOptions::No)
|
||||
.interact_text()?;
|
||||
|
||||
login_again == BoolPromptOptions::Yes
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if !person_wants_to_login {
|
||||
Ok(AuthorizationState::Cancelled)
|
||||
} else if self.no_browser {
|
||||
self.get_token_or_ask_user(&env)
|
||||
} else {
|
||||
// switch between two methods of getting the token.
|
||||
// start two async processes, 10 minute timeout and get token from browser. Whichever finishes first, use that.
|
||||
let timeout_future = tokio::time::sleep(Duration::from_secs(60 * 10));
|
||||
tokio::select! {
|
||||
_ = timeout_future => {
|
||||
Ok(AuthorizationState::TimedOut)
|
||||
},
|
||||
token = self.get_token_from_browser(&env) => {
|
||||
token
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}?;
|
||||
|
||||
match auth_state {
|
||||
AuthorizationState::TokenSuccess(token) => {
|
||||
let res = std::thread::spawn({
|
||||
let dir = env.dir().to_owned();
|
||||
let registry = registry.clone();
|
||||
move || {
|
||||
wasmer_registry::login::login_and_save_token(
|
||||
&dir,
|
||||
registry.as_str(),
|
||||
&token,
|
||||
)
|
||||
}
|
||||
})
|
||||
.join()
|
||||
.map_err(|err| anyhow::format_err!("handler thread died: {err:?}"))??;
|
||||
|
||||
match res {
|
||||
Some(s) => {
|
||||
print!("Done!");
|
||||
println!("\n{} Login for Wasmer user {:?} saved","✔".green().bold(), s)
|
||||
}
|
||||
None => print!(
|
||||
"Warning: no user found on {:?} with the provided token.\nToken saved regardless.",
|
||||
registry.domain().unwrap_or("registry.wasmer.io")
|
||||
),
|
||||
};
|
||||
}
|
||||
AuthorizationState::TimedOut => {
|
||||
print!("Timed out (10 mins exceeded)");
|
||||
}
|
||||
AuthorizationState::Cancelled => {
|
||||
println!("Cancelled by the user");
|
||||
}
|
||||
AuthorizationState::UnknownMethod => {
|
||||
println!("Error: unknown method\n");
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// execute [List]
|
||||
#[tokio::main]
|
||||
pub async fn execute(&self) -> Result<(), anyhow::Error> {
|
||||
self.run_async().await
|
||||
}
|
||||
}
|
||||
|
||||
async fn preflight(req: Request<Body>) -> Result<Response<Body>, anyhow::Error> {
|
||||
let _whole_body = hyper::body::aggregate(req).await?;
|
||||
let response = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("Access-Control-Allow-Origin", "*") // FIXME: this is not secure, Don't allow all origins. @syrusakbary
|
||||
.header("Access-Control-Allow-Headers", "Content-Type")
|
||||
.header("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
.body(Body::default())?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_post_save_token(
|
||||
context: AppContext,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response<Body>, anyhow::Error> {
|
||||
let AppContext {
|
||||
server_shutdown_tx,
|
||||
token_tx,
|
||||
} = context;
|
||||
let (.., body) = req.into_parts();
|
||||
let body = hyper::body::to_bytes(body).await?;
|
||||
|
||||
let ValidatedNonceOutput {
|
||||
token,
|
||||
status: token_status,
|
||||
} = serde_json::from_slice::<ValidatedNonceOutput>(&body)?;
|
||||
|
||||
// send the AuthorizationState based on token_status to the main thread and get the response message
|
||||
let (response_message, parse_failure) = match token_status {
|
||||
TokenStatus::Cancelled => {
|
||||
token_tx
|
||||
.send(AuthorizationState::Cancelled)
|
||||
.await
|
||||
.expect("Failed to send token");
|
||||
|
||||
("Token Cancelled by the user", false)
|
||||
}
|
||||
TokenStatus::Authorized => {
|
||||
if let Some(token) = token {
|
||||
token_tx
|
||||
.send(AuthorizationState::TokenSuccess(token.clone()))
|
||||
.await
|
||||
.expect("Failed to send token");
|
||||
("Token Authorized", false)
|
||||
} else {
|
||||
("Token not found", true)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
server_shutdown_tx
|
||||
.send(true)
|
||||
.await
|
||||
.expect("Failed to send shutdown signal");
|
||||
|
||||
let status = if parse_failure {
|
||||
StatusCode::BAD_REQUEST
|
||||
} else {
|
||||
StatusCode::OK
|
||||
};
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(status)
|
||||
.header("Access-Control-Allow-Origin", "*") // FIXME: this is not secure, Don't allow all origins. @syrusakbary
|
||||
.header("Access-Control-Allow-Headers", "Content-Type")
|
||||
.header("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
.body(Body::from(response_message))?)
|
||||
}
|
||||
|
||||
async fn handle_unknown_method(context: AppContext) -> Result<Response<Body>, anyhow::Error> {
|
||||
let AppContext {
|
||||
server_shutdown_tx,
|
||||
token_tx,
|
||||
} = context;
|
||||
|
||||
token_tx
|
||||
.send(AuthorizationState::UnknownMethod)
|
||||
.await
|
||||
.expect("Failed to send token");
|
||||
|
||||
server_shutdown_tx
|
||||
.send(true)
|
||||
.await
|
||||
.expect("Failed to send shutdown signal");
|
||||
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::METHOD_NOT_ALLOWED)
|
||||
.body(Body::from("Method not allowed"))?)
|
||||
}
|
||||
|
||||
/// Handle the preflight headers first - OPTIONS request
|
||||
/// Then proceed to handle the actual request - POST request
|
||||
async fn service_router(
|
||||
context: AppContext,
|
||||
req: Request<Body>,
|
||||
) -> Result<Response<Body>, anyhow::Error> {
|
||||
match *req.method() {
|
||||
Method::OPTIONS => preflight(req).await,
|
||||
Method::POST => handle_post_save_token(context, req).await,
|
||||
_ => handle_unknown_method(context).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::CommandFactory;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn interactive_login() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let login = Login {
|
||||
no_browser: true,
|
||||
registry: Some("wasmer.wtf".into()),
|
||||
wasmer_dir: temp.path().to_path_buf(),
|
||||
token: None,
|
||||
cache_dir: None,
|
||||
};
|
||||
let env = login.wasmer_env();
|
||||
|
||||
let token = login.get_token_or_ask_user(&env).unwrap();
|
||||
match token {
|
||||
AuthorizationState::TokenSuccess(token) => {
|
||||
assert_eq!(
|
||||
token,
|
||||
"Please paste the login token from https://wasmer.wtf/settings/access-tokens"
|
||||
);
|
||||
}
|
||||
AuthorizationState::Cancelled
|
||||
| AuthorizationState::TimedOut
|
||||
| AuthorizationState::UnknownMethod => {
|
||||
panic!("Should not reach here")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_with_token() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let login = Login {
|
||||
no_browser: true,
|
||||
registry: Some("wasmer.wtf".into()),
|
||||
wasmer_dir: temp.path().to_path_buf(),
|
||||
token: Some("abc".to_string()),
|
||||
cache_dir: None,
|
||||
};
|
||||
let env = login.wasmer_env();
|
||||
|
||||
let token = login.get_token_or_ask_user(&env).unwrap();
|
||||
|
||||
match token {
|
||||
AuthorizationState::TokenSuccess(token) => {
|
||||
assert_eq!(token, "abc");
|
||||
}
|
||||
AuthorizationState::Cancelled
|
||||
| AuthorizationState::TimedOut
|
||||
| AuthorizationState::UnknownMethod => {
|
||||
panic!("Should not reach here")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_sync_with_wasmer_env() {
|
||||
let wasmer_env = WasmerEnv::command();
|
||||
let login = Login::command();
|
||||
|
||||
// All options except --token should be the same
|
||||
let wasmer_env_opts: Vec<_> = wasmer_env
|
||||
.get_opts()
|
||||
.filter(|arg| arg.get_id() != "token")
|
||||
.collect();
|
||||
let login_opts: Vec<_> = login.get_opts().collect();
|
||||
|
||||
assert_eq!(wasmer_env_opts, login_opts);
|
||||
|
||||
// The token argument should have the same message, even if it is an
|
||||
// argument rather than a --flag.
|
||||
let wasmer_env_token_help = wasmer_env
|
||||
.get_opts()
|
||||
.find(|arg| arg.get_id() == "token")
|
||||
.unwrap()
|
||||
.get_help()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let login_token_help = login
|
||||
.get_positionals()
|
||||
.find(|arg| arg.get_id() == "token")
|
||||
.unwrap()
|
||||
.get_help()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert_eq!(wasmer_env_token_help, login_token_help);
|
||||
}
|
||||
|
||||
/// Regression test for panics on API errors.
|
||||
/// See https://github.com/wasmerio/wasmer/issues/4147.
|
||||
#[test]
|
||||
fn login_with_invalid_token_does_not_panic() {
|
||||
let cmd = Login {
|
||||
no_browser: true,
|
||||
wasmer_dir: WASMER_DIR.clone(),
|
||||
registry: Some("http://localhost:11".to_string().into()),
|
||||
token: Some("invalid".to_string()),
|
||||
cache_dir: None,
|
||||
};
|
||||
|
||||
let res = cmd.execute();
|
||||
assert!(res.is_err());
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
//! The commands available in the Wasmer binary.
|
||||
mod add;
|
||||
mod app;
|
||||
mod auth;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod binfmt;
|
||||
mod cache;
|
||||
@ -22,7 +23,6 @@ mod init;
|
||||
mod inspect;
|
||||
#[cfg(feature = "journal")]
|
||||
mod journal;
|
||||
mod login;
|
||||
pub(crate) mod namespace;
|
||||
mod package;
|
||||
mod run;
|
||||
@ -31,8 +31,8 @@ pub mod ssh;
|
||||
mod validate;
|
||||
#[cfg(feature = "wast")]
|
||||
mod wast;
|
||||
mod whoami;
|
||||
use std::env::args;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use binfmt::*;
|
||||
@ -49,8 +49,8 @@ pub use {create_obj::*, gen_c_header::*};
|
||||
#[cfg(feature = "journal")]
|
||||
pub use self::journal::*;
|
||||
pub use self::{
|
||||
add::*, cache::*, config::*, container::*, init::*, inspect::*, login::*, package::*,
|
||||
publish::*, run::Run, self_update::*, validate::*, whoami::*,
|
||||
add::*, auth::*, cache::*, config::*, container::*, init::*, inspect::*, package::*,
|
||||
publish::*, run::Run, self_update::*, validate::*,
|
||||
};
|
||||
use crate::error::PrettyError;
|
||||
|
||||
@ -70,13 +70,64 @@ pub(crate) trait AsyncCliCommand: Send + Sync {
|
||||
type Output: Send + Sync;
|
||||
|
||||
async fn run_async(self) -> Result<Self::Output, anyhow::Error>;
|
||||
|
||||
fn setup(
|
||||
&self,
|
||||
done: tokio::sync::oneshot::Receiver<()>,
|
||||
) -> Option<JoinHandle<anyhow::Result<()>>> {
|
||||
if is_terminal::IsTerminal::is_terminal(&std::io::stdin()) {
|
||||
return Some(tokio::task::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = done => {}
|
||||
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
let term = console::Term::stdout();
|
||||
let _ = term.show_cursor();
|
||||
// https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-constants
|
||||
#[cfg(target_os = "windows")]
|
||||
std::process::exit(3);
|
||||
|
||||
// POSIX compliant OSs: 128 + SIGINT (2)
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
std::process::exit(130);
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: Send + Sync, C: AsyncCliCommand<Output = O>> CliCommand for C {
|
||||
type Output = O;
|
||||
|
||||
fn run(self) -> Result<(), anyhow::Error> {
|
||||
tokio::runtime::Runtime::new()?.block_on(AsyncCliCommand::run_async(self))?;
|
||||
tokio::runtime::Runtime::new()?.block_on(async {
|
||||
let (snd, rcv) = tokio::sync::oneshot::channel();
|
||||
let handle = self.setup(rcv);
|
||||
|
||||
if let Err(e) = AsyncCliCommand::run_async(self).await {
|
||||
if let Some(handle) = handle {
|
||||
handle.abort();
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
if let Some(handle) = handle {
|
||||
if snd.send(()).is_err() {
|
||||
tracing::warn!("Failed to send 'done' signal to setup thread!");
|
||||
handle.abort();
|
||||
} else {
|
||||
handle.await??;
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -133,7 +184,8 @@ impl WasmerCmd {
|
||||
Some(Cmd::Config(config)) => config.execute(),
|
||||
Some(Cmd::Inspect(inspect)) => inspect.execute(),
|
||||
Some(Cmd::Init(init)) => init.execute(),
|
||||
Some(Cmd::Login(login)) => login.execute(),
|
||||
Some(Cmd::Login(login)) => login.run(),
|
||||
Some(Cmd::Auth(auth)) => auth.run(),
|
||||
Some(Cmd::Publish(publish)) => publish.run().map(|_| ()),
|
||||
Some(Cmd::Package(cmd)) => match cmd {
|
||||
Package::Download(cmd) => cmd.execute(),
|
||||
@ -141,6 +193,7 @@ impl WasmerCmd {
|
||||
Package::Tag(cmd) => cmd.run(),
|
||||
Package::Push(cmd) => cmd.run(),
|
||||
Package::Publish(cmd) => cmd.run().map(|_| ()),
|
||||
Package::Unpack(cmd) => cmd.execute(),
|
||||
},
|
||||
Some(Cmd::Container(cmd)) => match cmd {
|
||||
crate::commands::Container::Unpack(cmd) => cmd.execute(),
|
||||
@ -154,7 +207,7 @@ impl WasmerCmd {
|
||||
Some(Cmd::Wast(wast)) => wast.execute(),
|
||||
#[cfg(target_os = "linux")]
|
||||
Some(Cmd::Binfmt(binfmt)) => binfmt.execute(),
|
||||
Some(Cmd::Whoami(whoami)) => whoami.execute(),
|
||||
Some(Cmd::Whoami(whoami)) => whoami.run(),
|
||||
Some(Cmd::Add(install)) => install.execute(),
|
||||
|
||||
// Deploy commands.
|
||||
@ -237,9 +290,12 @@ enum Cmd {
|
||||
/// Login into a wasmer.io-like registry
|
||||
Login(Login),
|
||||
|
||||
#[clap(subcommand)]
|
||||
Auth(CmdAuth),
|
||||
|
||||
/// Publish a package to a registry [alias: package publish]
|
||||
#[clap(name = "publish")]
|
||||
Publish(crate::commands::package::publish::PackagePublish),
|
||||
Publish(PackagePublish),
|
||||
|
||||
/// Manage the local Wasmer cache
|
||||
Cache(Cache),
|
||||
@ -370,7 +426,7 @@ enum Cmd {
|
||||
/// Deploy apps to Wasmer Edge [alias: app deploy]
|
||||
Deploy(crate::commands::app::deploy::CmdAppDeploy),
|
||||
|
||||
/// Manage deployed Edge apps
|
||||
/// Create and manage Wasmer Edge apps
|
||||
#[clap(subcommand, alias = "apps")]
|
||||
App(crate::commands::app::CmdApp),
|
||||
|
||||
|
@ -1,15 +1,13 @@
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
opts::{ApiOpts, ItemFormatOpts},
|
||||
};
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ItemFormatOpts};
|
||||
|
||||
/// Create a new namespace.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdNamespaceCreate {
|
||||
#[clap(flatten)]
|
||||
fmt: ItemFormatOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
|
||||
/// Description of the namespace.
|
||||
#[clap(long)]
|
||||
@ -24,7 +22,7 @@ impl AsyncCliCommand for CmdNamespaceCreate {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
|
||||
let vars = wasmer_api::types::CreateNamespaceVars {
|
||||
name: self.name.clone(),
|
||||
|
@ -1,17 +1,15 @@
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
opts::{ApiOpts, ItemFormatOpts},
|
||||
};
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ItemFormatOpts};
|
||||
|
||||
/// Show a namespace.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct CmdNamespaceGet {
|
||||
#[clap(flatten)]
|
||||
fmt: ItemFormatOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
|
||||
/// Name of the namespace.
|
||||
name: String,
|
||||
@ -22,7 +20,7 @@ impl AsyncCliCommand for CmdNamespaceGet {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
|
||||
let namespace = wasmer_api::query::get_namespace(&client, self.name)
|
||||
.await?
|
||||
|
@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
commands::AsyncCliCommand,
|
||||
opts::{ApiOpts, ListFormatOpts},
|
||||
};
|
||||
use crate::{commands::AsyncCliCommand, config::WasmerEnv, opts::ListFormatOpts};
|
||||
|
||||
/// List namespaces.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
@ -9,7 +6,7 @@ pub struct CmdNamespaceList {
|
||||
#[clap(flatten)]
|
||||
fmt: ListFormatOpts,
|
||||
#[clap(flatten)]
|
||||
api: ApiOpts,
|
||||
env: WasmerEnv,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@ -17,7 +14,7 @@ impl AsyncCliCommand for CmdNamespaceList {
|
||||
type Output = ();
|
||||
|
||||
async fn run_async(self) -> Result<(), anyhow::Error> {
|
||||
let client = self.api.client()?;
|
||||
let client = self.env.client()?;
|
||||
|
||||
let namespaces = wasmer_api::query::user_namespaces(&client).await?;
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
use crate::{
|
||||
commands::Login,
|
||||
opts::{ApiOpts, WasmerEnv},
|
||||
commands::{AsyncCliCommand, Login},
|
||||
config::WasmerEnv,
|
||||
utils::load_package_manifest,
|
||||
};
|
||||
use colored::Colorize;
|
||||
use dialoguer::Confirm;
|
||||
use hyper::Body;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use reqwest::Body;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::{Path, PathBuf},
|
||||
@ -175,18 +175,17 @@ pub(super) fn get_manifest(path: &Path) -> anyhow::Result<(PathBuf, Manifest)> {
|
||||
}
|
||||
|
||||
pub(super) async fn login_user(
|
||||
api: &ApiOpts,
|
||||
env: &WasmerEnv,
|
||||
interactive: bool,
|
||||
msg: &str,
|
||||
) -> anyhow::Result<WasmerClient> {
|
||||
if let Ok(client) = api.client() {
|
||||
if let Ok(client) = env.client() {
|
||||
return Ok(client);
|
||||
}
|
||||
|
||||
let theme = dialoguer::theme::ColorfulTheme::default();
|
||||
|
||||
if api.token.is_none() {
|
||||
if env.token().is_none() {
|
||||
if interactive {
|
||||
eprintln!(
|
||||
"{}: You need to be logged in to {msg}.",
|
||||
@ -199,17 +198,13 @@ pub(super) async fn login_user(
|
||||
{
|
||||
Login {
|
||||
no_browser: false,
|
||||
wasmer_dir: env.wasmer_dir.clone(),
|
||||
registry: api
|
||||
.registry
|
||||
.clone()
|
||||
.map(|l| wasmer_registry::wasmer_env::Registry::from(l.to_string())),
|
||||
token: api.token.clone(),
|
||||
cache_dir: Some(env.cache_dir.clone()),
|
||||
wasmer_dir: env.dir().to_path_buf(),
|
||||
cache_dir: env.cache_dir().to_path_buf(),
|
||||
token: None,
|
||||
registry: env.registry.clone(),
|
||||
}
|
||||
.run_async()
|
||||
.await?;
|
||||
// self.api = ApiOpts::default();
|
||||
} else {
|
||||
anyhow::bail!("Stopping the flow as the user is not logged in.")
|
||||
}
|
||||
@ -220,7 +215,7 @@ pub(super) async fn login_user(
|
||||
}
|
||||
}
|
||||
|
||||
api.client()
|
||||
env.client()
|
||||
}
|
||||
|
||||
pub(super) fn make_package_url(client: &WasmerClient, pkg: &NamedPackageIdent) -> String {
|
||||
|
@ -7,14 +7,11 @@ use tempfile::NamedTempFile;
|
||||
use wasmer_config::package::{PackageIdent, PackageSource};
|
||||
use wasmer_wasix::http::reqwest::get_proxy;
|
||||
|
||||
use crate::opts::{ApiOpts, WasmerEnv};
|
||||
use crate::config::WasmerEnv;
|
||||
|
||||
/// Download a package from the registry.
|
||||
#[derive(clap::Parser, Debug)]
|
||||
pub struct PackageDownload {
|
||||
#[clap(flatten)]
|
||||
pub api: ApiOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub env: WasmerEnv,
|
||||
|
||||
@ -31,6 +28,10 @@ pub struct PackageDownload {
|
||||
#[clap(long)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// proxy to use for downloading
|
||||
#[clap(long)]
|
||||
pub proxy: Option<String>,
|
||||
|
||||
/// The package to download.
|
||||
package: PackageSource,
|
||||
}
|
||||
@ -97,11 +98,16 @@ impl PackageDownload {
|
||||
|
||||
let (download_url, ident, filename) = match &self.package {
|
||||
PackageSource::Ident(PackageIdent::Named(id)) => {
|
||||
let client = if self.api.token.is_some() {
|
||||
self.api.client()
|
||||
// caveat: client_unauthennticated will use a token if provided, it
|
||||
// just won't fail if none is present. So, _unauthenticated() can actually
|
||||
// produce an authenticated client.
|
||||
let client = if let Some(proxy) = &self.proxy {
|
||||
let proxy = reqwest::Proxy::all(proxy)?;
|
||||
|
||||
self.env.client_unauthennticated_with_proxy(proxy)?
|
||||
} else {
|
||||
self.api.client_unauthennticated()
|
||||
}?;
|
||||
self.env.client_unauthennticated()?
|
||||
};
|
||||
|
||||
let version = id.version_or_default().to_string();
|
||||
let version = if version == "*" {
|
||||
@ -145,11 +151,10 @@ impl PackageDownload {
|
||||
(download_url, ident, filename)
|
||||
}
|
||||
PackageSource::Ident(PackageIdent::Hash(hash)) => {
|
||||
let client = if self.api.token.is_some() {
|
||||
self.api.client()
|
||||
} else {
|
||||
self.api.client_unauthennticated()
|
||||
}?;
|
||||
// caveat: client_unauthennticated will use a token if provided, it
|
||||
// just won't fail if none is present. So, _unauthenticated() can actually
|
||||
// produce an authenticated client.
|
||||
let client = self.env.client_unauthennticated()?;
|
||||
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
let pkg = rt.block_on(wasmer_api::query::get_package_release(&client, &hash.to_string()))?
|
||||
@ -285,7 +290,6 @@ impl PackageDownload {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Download a package from the dev registry.
|
||||
#[test]
|
||||
@ -295,15 +299,17 @@ mod tests {
|
||||
let out_path = dir.path().join("hello.webc");
|
||||
|
||||
let cmd = PackageDownload {
|
||||
env: WasmerEnv::default(),
|
||||
api: ApiOpts {
|
||||
token: None,
|
||||
registry: Some(url::Url::from_str("https://registry.wasmer.io/graphql").unwrap()),
|
||||
},
|
||||
env: WasmerEnv::new(
|
||||
crate::config::DEFAULT_WASMER_CACHE_DIR.clone(),
|
||||
crate::config::DEFAULT_WASMER_CACHE_DIR.clone(),
|
||||
None,
|
||||
Some("https://registry.wasmer.io/graphql".to_owned().into()),
|
||||
),
|
||||
validate: true,
|
||||
out_path: Some(out_path.clone()),
|
||||
package: "wasmer/hello@0.1.0".parse().unwrap(),
|
||||
quiet: true,
|
||||
proxy: None,
|
||||
};
|
||||
|
||||
cmd.execute().unwrap();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user