Repository: fermyon/spin
Pull request #1079 (2023-01-27 21:20:55)

Support SSL/TLS in `outbound-mysql`
This is a hack workaround to support SSL/TLS in the `outbound-mysql` host component. Our dependency `mysql_async` wasn't properly parsing the ssl parameters so we do it ourselves here and then hide that parameter from the `mysql_async`.

I've tested without SSL/TLS against a local database and against Planetscale with SSL/TLS.

itowlson: @etehtsea I think you looked at SSL for Postgres, and also did the original MySQL support - would you be able to take a look at this please?  Thanks!
calebschoepp: > LGTM. Like we discussed, it would be great to move this responsibility into the `mysql_async` crate itself if you haven't already opened an issue there about it.

I'm in the process of opening an issue with `mysql_async` to get that process started.

Patch:
diff --git a/.github/workflows/audits.yml b/.github/workflows/audits.yml
index 5ee77055..c33204ea 100644
--- a/.github/workflows/audits.yml
+++ b/.github/workflows/audits.yml
@@ -10,10 +10,10 @@ jobs:
     env:
       CARGO_VET_VERSION: 0.3.0
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@master
       - name: Install Rust
         run: rustup update stable && rustup default stable
-      - uses: actions/cache@v3
+      - uses: actions/cache@v2
         with:
           path: ${{ runner.tool_cache }}/cargo-vet
           key: cargo-vet-bin-${{ env.CARGO_VET_VERSION }}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5579d464..d7464397 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,11 +18,11 @@ jobs:
     name: Lint Rust
     runs-on: "ubuntu-latest"
     steps:
-      - uses: actions/checkout@v3
-      - name: Install Rust toolchain
+      - uses: actions/checkout@v2
+      - name: Install latest Rust stable toolchain
         uses: actions-rs/toolchain@v1
         with:
-          toolchain: 1.66
+          toolchain: stable
           default: true
           components: clippy, rustfmt
 
@@ -61,12 +61,12 @@ jobs:
               extraArgs: "",
             }
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
 
-      - name: Install Rust toolchain
+      - name: Install latest Rust stable toolchain
         uses: actions-rs/toolchain@v1
         with:
-          toolchain: 1.66
+          toolchain: stable
           default: true
 
       - name: "Install Wasm Rust target"
@@ -135,26 +135,26 @@ jobs:
               hippoUrl: "https://github.com/deislabs/hippo/releases/download/v0.19.0/hippo-server-win-x64.zip",
             }
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
 
-      - name: Install Rust toolchain
+      - name: Install latest Rust stable toolchain
         uses: actions-rs/toolchain@v1
         with:
-          toolchain: 1.66
+          toolchain: stable
           default: true
 
       - name: "Install Wasm Rust target"
         run: rustup target add wasm32-wasi
 
       - name: Install bindle
-        uses: engineerd/configurator@v0.0.9
+        uses: engineerd/configurator@v0.0.8
         with:
           name: ${{ matrix.config.bindleBinary }}
           url: ${{ matrix.config.bindleUrl }}
           pathInArchive: ${{ matrix.config.pathInBindleArchive }}
 
       - name: Install nomad
-        uses: engineerd/configurator@v0.0.9
+        uses: engineerd/configurator@v0.0.8
         with:
           name: ${{ matrix.config.nomadBinary }}
           url: ${{ matrix.config.nomadUrl }}
@@ -209,7 +209,7 @@ jobs:
               pathInWasmtimeArchive: "wasmtime-v0.36.0-x86_64-linux/wasmtime",
             }
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
 
       - name: Retrieve saved Spin Binary
         uses: actions/download-artifact@v3
@@ -224,7 +224,7 @@ jobs:
           chmod +x target/debug/spin
 
       - name: Install bindle
-        uses: engineerd/configurator@v0.0.9
+        uses: engineerd/configurator@v0.0.8
         with:
           name: ${{ matrix.config.bindleBinary }}
           url: ${{ matrix.config.bindleUrl }}
@@ -242,7 +242,7 @@ jobs:
           tinygo env
 
       - name: "Install Wasmtime"
-        uses: engineerd/configurator@v0.0.9
+        uses: engineerd/configurator@v0.0.8
         with:
           name: ${{ matrix.config.wasmtimeBinary }}
           url: ${{ matrix.config.wasmtimeUrl }}
@@ -250,3 +250,86 @@ jobs:
 
       - name: "Test Go SDK"
         run: make test-sdk-go
+
+  code-coverage:
+    name: Run code coverage
+    runs-on: ${{ matrix.config.os }}
+    needs: build-rust
+    strategy:
+      matrix:
+        config:
+          - {
+              os: "ubuntu-latest",
+              extension: "",
+              # We have this enabled for releases, so we should test it.
+              extraArgs: "--features openssl/vendored",
+              bindleUrl: "https://bindle.blob.core.windows.net/releases/bindle-v0.8.0-linux-amd64.tar.gz",
+              bindleBinary: "bindle-server",
+              pathInBindleArchive: "bindle-server",
+              nomadUrl: "https://releases.hashicorp.com/nomad/1.3.1/nomad_1.3.1_linux_amd64.zip",
+              nomadBinary: "nomad",
+              pathInNomadArchive: "nomad",
+              hippoUrl: "https://github.com/deislabs/hippo/releases/download/v0.19.0/hippo-server-linux-x64.tar.gz",
+            }
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Install latest Rust stable toolchain
+        uses: actions-rs/toolchain@v1
+        with:
+          toolchain: stable
+          default: true
+
+      - name: "Install Wasm Rust target"
+        run: rustup target add wasm32-wasi
+
+      - name: Install bindle
+        uses: engineerd/configurator@v0.0.8
+        with:
+          name: ${{ matrix.config.bindleBinary }}
+          url: ${{ matrix.config.bindleUrl }}
+          pathInArchive: ${{ matrix.config.pathInBindleArchive }}
+
+      - name: Install nomad
+        uses: engineerd/configurator@v0.0.8
+        with:
+          name: ${{ matrix.config.nomadBinary }}
+          url: ${{ matrix.config.nomadUrl }}
+          pathInArchive: ${{ matrix.config.pathInNomadArchive }}
+
+      - name: Install hippo
+        run: |
+          curl -L ${{ matrix.config.hippoUrl }} -o hippo-server.tar.gz
+          mkdir hippo-server-output
+          tar xz -C hippo-server-output -f hippo-server.tar.gz
+          cp -r hippo-server-output/**/* ~/configurator/bin
+          chmod +x ~/configurator/bin/Hippo.Web
+
+      - name: Install cargo-tarpaulin binary crate
+        uses: actions-rs/install@v0.1
+        with:
+          crate: cargo-tarpaulin
+          version: 0.19.1
+          use-tool-cache: true
+
+      - name: Cargo Tarpaulin
+        run: cargo tarpaulin --follow-exec --skip-clean -t 6000 --out xml --features openssl/vendored,default,e2e-tests
+        env:
+          RUST_LOG: spin=trace
+
+      # Only attempt to upload to codecov.io if the repo owner is fermyon.
+      # This allows forks to run CI on their own main branches as usual,
+      # without needing to have a codecov token for uploading.
+      - name: Upload to codecov.io
+        uses: codecov/codecov-action@v3
+        if: ${{ github.ref == 'refs/heads/main' && github.repository_owner == 'fermyon' }}
+        with:
+          fail_ci_if_error: true
+          verbose: true
+
+      - name: Archive code coverage results
+        uses: actions/upload-artifact@v3
+        if: ${{ github.ref == 'refs/heads/main' }}
+        with:
+          name: code-coverage-report
+          path: cobertura.xml
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
deleted file mode 100644
index e25fbefc..00000000
--- a/.github/workflows/code-coverage.yml
+++ /dev/null
@@ -1,127 +0,0 @@
-name: Code coverage
-on:
-  schedule:
-    - cron: '0 0 * * *'
-  workflow_dispatch:
-jobs:
-  build-rust:
-    name: Build Spin
-    runs-on: ${{ matrix.config.os }}
-    if: ${{ github.repository_owner == 'fermyon' }} || ${{ github.event_name == 'workflow_dispatch' }}
-    strategy:
-      matrix:
-        config:
-          - {
-              os: "ubuntu-latest",
-              extension: "",
-              # We have this enabled for releases, so we should test it.
-              extraArgs: "--features openssl/vendored",
-            }
-    steps:
-      - uses: actions/checkout@v3
-
-      - name: Install Rust toolchain
-        uses: actions-rs/toolchain@v1
-        with:
-          toolchain: 1.66
-          default: true
-
-      - name: "Install Wasm Rust target"
-        run: rustup target add wasm32-wasi
-
-      - uses: Swatinem/rust-cache@v2
-        with:
-          shared-key: "${{ runner.os }}-full-${{ hashFiles('./Cargo.lock') }}"
-
-      - name: Cargo Build
-        run: cargo build --workspace --all-targets --all-features ${{ matrix.config.extraArgs }}
-
-      - name: "Archive executable artifact"
-        uses: actions/upload-artifact@v3
-        with:
-          name: spin-${{ matrix.config.os }}
-          path: target/debug/spin${{ matrix.config.extension }}
-
-  code-coverage:
-    name: Run code coverage
-    runs-on: ${{ matrix.config.os }}
-    if: ${{ github.repository_owner == 'fermyon' }} || ${{ github.event_name == 'workflow_dispatch' }}
-    needs: build-rust
-    strategy:
-      matrix:
-        config:
-          - {
-              os: "ubuntu-latest",
-              extension: "",
-              # We have this enabled for releases, so we should test it.
-              extraArgs: "--features openssl/vendored",
-              bindleUrl: "https://bindle.blob.core.windows.net/releases/bindle-v0.8.0-linux-amd64.tar.gz",
-              bindleBinary: "bindle-server",
-              pathInBindleArchive: "bindle-server",
-              nomadUrl: "https://releases.hashicorp.com/nomad/1.3.1/nomad_1.3.1_linux_amd64.zip",
-              nomadBinary: "nomad",
-              pathInNomadArchive: "nomad",
-              hippoUrl: "https://github.com/deislabs/hippo/releases/download/v0.19.0/hippo-server-linux-x64.tar.gz",
-            }
-    steps:
-      - uses: actions/checkout@v3
-
-      - name: Install Rust toolchain
-        uses: actions-rs/toolchain@v1
-        with:
-          toolchain: 1.66
-          default: true
-
-      - name: "Install Wasm Rust target"
-        run: rustup target add wasm32-wasi
-
-      - name: Install bindle
-        uses: engineerd/configurator@v0.0.9
-        with:
-          name: ${{ matrix.config.bindleBinary }}
-          url: ${{ matrix.config.bindleUrl }}
-          pathInArchive: ${{ matrix.config.pathInBindleArchive }}
-
-      - name: Install nomad
-        uses: engineerd/configurator@v0.0.9
-        with:
-          name: ${{ matrix.config.nomadBinary }}
-          url: ${{ matrix.config.nomadUrl }}
-          pathInArchive: ${{ matrix.config.pathInNomadArchive }}
-
-      - name: Install hippo
-        run: |
-          curl -L ${{ matrix.config.hippoUrl }} -o hippo-server.tar.gz
-          mkdir hippo-server-output
-          tar xz -C hippo-server-output -f hippo-server.tar.gz
-          cp -r hippo-server-output/**/* ~/configurator/bin
-          chmod +x ~/configurator/bin/Hippo.Web
-
-      - name: Install cargo-tarpaulin binary crate
-        uses: actions-rs/install@v0.1
-        with:
-          crate: cargo-tarpaulin
-          version: 0.19.1
-          use-tool-cache: true
-
-      - name: Cargo Tarpaulin
-        run: cargo tarpaulin --follow-exec --skip-clean -t 6000 --out xml --features openssl/vendored,default,e2e-tests
-        env:
-          RUST_LOG: spin=trace
-
-      # Only attempt to upload to codecov.io if the repo owner is fermyon.
-      # This allows forks to run code coverage manually, without needing
-      # to have a codecov token for uploading.
-      - name: Upload to codecov.io
-        uses: codecov/codecov-action@v3
-        if: ${{ github.ref == 'refs/heads/main' && github.repository_owner == 'fermyon' }}
-        with:
-          fail_ci_if_error: true
-          verbose: true
-
-      - name: Archive code coverage results
-        uses: actions/upload-artifact@v3
-        if: ${{ github.ref == 'refs/heads/main' }}
-        with:
-          name: code-coverage-report
-          path: cobertura.xml
diff --git a/.github/workflows/dispatch.yml b/.github/workflows/dispatch.yml
index 0004e19c..43f9f892 100644
--- a/.github/workflows/dispatch.yml
+++ b/.github/workflows/dispatch.yml
@@ -13,7 +13,7 @@ jobs:
     outputs:
       docs: ${{ steps.filter.outputs.docs }}
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
       - uses: dorny/paths-filter@v2
         id: filter
         with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 75e5f9a1..85fd9429 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -56,7 +56,7 @@ jobs:
               targetDir: "target/release",
             }
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
 
       - name: set the release version (tag)
         if: startsWith(github.ref, 'refs/tags/v')
@@ -74,16 +74,16 @@ jobs:
           OS=$(echo "${{ runner.os }}" | tr '[:upper:]' '[:lower:]')
           echo "RUNNER_OS=$OS" >> $GITHUB_ENV
 
-      - name: Install Rust toolchain
+      - name: "Install Wasm Rust target"
+        run: rustup target add wasm32-wasi
+
+      - name: Install latest Rust stable toolchain
         uses: actions-rs/toolchain@v1
         with:
-          toolchain: 1.66
+          toolchain: stable
           default: true
           target: ${{ matrix.config.target }}
 
-      - name: "Install Wasm Rust target"
-        run: rustup target add wasm32-wasi --toolchain 1.66
-
       - name: setup for cross-compiled linux aarch64 build
         if: matrix.config.target == 'aarch64-unknown-linux-gnu'
         run: |
@@ -217,7 +217,7 @@ jobs:
     needs: build
     if: startsWith(github.ref, 'refs/tags/v')
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
 
       - name: Set the tag to sdk/go/v*
         shell: bash
@@ -237,7 +237,7 @@ jobs:
     needs: create-go-sdk-tag
     if: startsWith(github.ref, 'refs/tags/v')
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
 
       - name: Set the spin tag
         shell: bash
@@ -276,7 +276,7 @@ jobs:
     needs: build
     if: github.event.commits[0].author.name == 'fermybot' && contains(github.event.commits[0].message, 'update sdk')
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
 
       - name: Set the tag to spin/templates/v*
         shell: bash
diff --git a/Cargo.lock b/Cargo.lock
index dc0bb385..152d579e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -215,7 +215,7 @@ dependencies = [
  "tokio-stream",
  "tokio-tar",
  "tokio-util 0.6.10",
- "toml 0.5.9",
+ "toml",
  "tracing",
  "tracing-futures",
  "url",
@@ -1767,7 +1767,7 @@ dependencies = [
  "serde_json",
  "sha2 0.9.9",
  "tokio",
- "toml 0.5.9",
+ "toml",
  "uuid",
 ]
 
@@ -2551,15 +2551,6 @@ dependencies = [
  "minimal-lexical",
 ]
 
-[[package]]
-name = "nom8"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
-dependencies = [
- "memchr",
-]
-
 [[package]]
 name = "nu-ansi-term"
 version = "0.46.0"
@@ -2767,6 +2758,7 @@ dependencies = [
  "spin-core",
  "tokio",
  "tracing",
+ "url",
  "wit-bindgen-wasmtime",
 ]
 
@@ -3083,7 +3075,7 @@ version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
 dependencies = [
- "toml 0.5.9",
+ "toml",
 ]
 
 [[package]]
@@ -3791,15 +3783,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "serde_spanned"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c68e921cef53841b8925c2abadd27c9b891d9613bdc43d6b823062866df38e8"
-dependencies = [
- "serde",
-]
-
 [[package]]
 name = "serde_urlencoded"
 version = "0.7.1"
@@ -4017,7 +4000,7 @@ dependencies = [
  "serde",
  "serde_json",
  "tempfile",
- "toml 0.5.9",
+ "toml",
  "wasi-common",
  "wasmtime",
  "wasmtime-wasi",
@@ -4047,7 +4030,7 @@ dependencies = [
  "spin-loader",
  "subprocess",
  "tokio",
- "toml 0.5.9",
+ "toml",
  "tracing",
 ]
 
@@ -4100,7 +4083,7 @@ dependencies = [
  "spin-trigger",
  "tempfile",
  "tokio",
- "toml 0.6.0",
+ "toml",
  "tracing",
  "tracing-subscriber",
  "url",
@@ -4123,7 +4106,7 @@ dependencies = [
  "spin-core",
  "thiserror",
  "tokio",
- "toml 0.5.9",
+ "toml",
  "vaultrs",
  "wit-bindgen-wasmtime",
 ]
@@ -4195,7 +4178,7 @@ dependencies = [
  "tempfile",
  "tokio",
  "tokio-util 0.6.10",
- "toml 0.5.9",
+ "toml",
  "tracing",
  "walkdir",
 ]
@@ -4222,7 +4205,7 @@ dependencies = [
  "indexmap",
  "serde",
  "thiserror",
- "toml 0.5.9",
+ "toml",
 ]
 
 [[package]]
@@ -4265,7 +4248,7 @@ dependencies = [
  "spin-manifest",
  "thiserror",
  "tokio",
- "toml 0.5.9",
+ "toml",
 ]
 
 [[package]]
@@ -4325,7 +4308,7 @@ dependencies = [
  "sha2 0.10.6",
  "tempfile",
  "tokio",
- "toml 0.5.9",
+ "toml",
  "url",
  "walkdir",
  "wasmtime",
@@ -4374,7 +4357,7 @@ dependencies = [
  "spin-manifest",
  "tempfile",
  "tokio",
- "toml 0.5.9",
+ "toml",
  "tracing",
  "url",
  "wasmtime",
@@ -4790,40 +4773,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "toml"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb9d890e4dc9298b70f740f615f2e05b9db37dce531f6b24fb77ac993f9f217"
-dependencies = [
- "serde",
- "serde_spanned",
- "toml_datetime",
- "toml_edit",
-]
-
-[[package]]
-name = "toml_datetime"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "toml_edit"
-version = "0.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "729bfd096e40da9c001f778f5cdecbd2957929a24e10e5883d9392220a751581"
-dependencies = [
- "indexmap",
- "nom8",
- "serde",
- "serde_spanned",
- "toml_datetime",
-]
-
 [[package]]
 name = "tower-service"
 version = "0.3.2"
@@ -5299,7 +5248,7 @@ dependencies = [
  "rustix 0.35.11",
  "serde",
  "sha2 0.10.6",
- "toml 0.5.9",
+ "toml",
  "windows-sys 0.36.1",
  "zstd",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 9a6a3967..83c40483 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -54,7 +54,7 @@ spin-templates = { path = "crates/templates" }
 spin-trigger = { path = "crates/trigger" }
 tempfile = "3.3.0"
 tokio = { version = "1.23", features = [ "full" ] }
-toml = "0.6"
+toml = "0.5"
 tracing = { workspace = true }
 tracing-subscriber = { version = "0.3.7", features = [ "env-filter" ] }
 url = "2.2.2"
diff --git a/crates/outbound-mysql/Cargo.toml b/crates/outbound-mysql/Cargo.toml
index 4ea40cfe..b9b3d423 100644
--- a/crates/outbound-mysql/Cargo.toml
+++ b/crates/outbound-mysql/Cargo.toml
@@ -14,4 +14,5 @@ mysql_common = "0.29.1"
 spin-core = { path = "../core" }
 tokio = { version = "1", features = [ "rt-multi-thread" ] }
 tracing = { version = "0.1", features = [ "log" ] }
+url = "2.3.1"
 wit-bindgen-wasmtime = { workspace = true }
diff --git a/crates/outbound-mysql/src/lib.rs b/crates/outbound-mysql/src/lib.rs
index a139e9e5..bc11e08f 100644
--- a/crates/outbound-mysql/src/lib.rs
+++ b/crates/outbound-mysql/src/lib.rs
@@ -1,9 +1,10 @@
 use mysql_async::consts::ColumnType;
-use mysql_async::{from_value_opt, prelude::*};
+use mysql_async::{from_value_opt, prelude::*, Opts, OptsBuilder, SslOpts};
 pub use outbound_mysql::add_to_linker;
 use spin_core::HostComponent;
 use std::collections::HashMap;
 use std::sync::Arc;
+use url::Url;
 use wit_bindgen_wasmtime::async_trait;
 
 wit_bindgen_wasmtime::export!({paths: ["../../wit/ephemeral/outbound-mysql.wit"], async: *});
@@ -244,11 +245,93 @@ impl OutboundMysql {
 async fn build_conn(address: &str) -> Result<mysql_async::Conn, mysql_async::Error> {
     tracing::log::debug!("Build new connection: {}", address);
 
-    let connection_pool = mysql_async::Pool::new(address);
+    let opts = build_opts(address)?;
+
+    let connection_pool = mysql_async::Pool::new(opts);
 
     connection_pool.get_conn().await
 }
 
+fn is_ssl_param(s: &str) -> bool {
+    ["ssl-mode", "sslmode"].contains(&s.to_lowercase().as_str())
+}
+
+/// The mysql_async crate blows up if you pass it an SSL parameter and doesn't support SSL opts properly. This function
+/// is a workaround to manually set SSL opts if the user requests them.
+///
+/// We only support ssl-mode in the query as per
+/// https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-security.html#cj-conn-prop_sslMode.
+///
+/// An issue has been filed in the upstream repository https://github.com/blackbeam/mysql_async/issues/225.
+fn build_opts(address: &str) -> Result<Opts, mysql_async::Error> {
+    let url = Url::parse(address)?;
+
+    let use_ssl = url
+        .query_pairs()
+        .any(|(k, v)| is_ssl_param(&k) && v.to_lowercase() != "disabled");
+
+    let query_without_ssl: Vec<(_, _)> = url
+        .query_pairs()
+        .filter(|(k, _v)| !is_ssl_param(k))
+        .collect();
+    let mut cleaned_url = url.clone();
+    cleaned_url.set_query(None);
+    cleaned_url
+        .query_pairs_mut()
+        .extend_pairs(query_without_ssl);
+
+    Ok(OptsBuilder::from_opts(cleaned_url.as_str())
+        .ssl_opts(if use_ssl {
+            Some(SslOpts::default())
+        } else {
+            None
+        })
+        .into())
+}
+
 fn convert_value_to<T: FromValue>(value: mysql_async::Value) -> Result<T, MysqlError> {
     from_value_opt::<T>(value).map_err(|e| MysqlError::ValueConversionFailed(format!("{}", e)))
 }
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_mysql_address_without_ssl_mode() {
+        assert!(build_opts("mysql://myuser:password@127.0.0.1/db")
+            .unwrap()
+            .ssl_opts()
+            .is_none())
+    }
+
+    #[test]
+    fn test_mysql_address_with_ssl_mode_disabled() {
+        assert!(
+            build_opts("mysql://myuser:password@127.0.0.1/db?ssl-mode=DISABLED")
+                .unwrap()
+                .ssl_opts()
+                .is_none()
+        )
+    }
+
+    #[test]
+    fn test_mysql_address_with_ssl_mode_verify_ca() {
+        assert!(
+            build_opts("mysql://myuser:password@127.0.0.1/db?sslMode=VERIFY_CA")
+                .unwrap()
+                .ssl_opts()
+                .is_some()
+        )
+    }
+
+    #[test]
+    fn test_mysql_address_with_more_to_query() {
+        let address = "mysql://myuser:password@127.0.0.1/db?SsLmOdE=VERIFY_CA&pool_max=10";
+        assert!(build_opts(address).unwrap().ssl_opts().is_some());
+        assert_eq!(
+            build_opts(address).unwrap().pool_opts().constraints().max(),
+            10
+        )
+    }
+}
diff --git a/crates/plugins/src/store.rs b/crates/plugins/src/store.rs
index ad052b92..83910b90 100644
--- a/crates/plugins/src/store.rs
+++ b/crates/plugins/src/store.rs
@@ -1,4 +1,4 @@
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Result};
 use flate2::read::GzDecoder;
 use std::{
     ffi::OsStr,
@@ -85,15 +85,8 @@ impl PluginStore {
         //    |- bar.json
         let catalogue_dir =
             crate::lookup::spin_plugins_repo_manifest_dir(self.get_plugins_directory());
-
-        // Catalogue directory doesn't exist so likely nothing has been installed.
-        if !catalogue_dir.exists() {
-            return Ok(Vec::new());
-        }
-
         let plugin_dirs = catalogue_dir
-            .read_dir()
-            .context("reading manifest catalogue at {catalogue_dir:?}")?
+            .read_dir()?
             .filter_map(|d| d.ok())
             .map(|d| d.path())
             .filter(|p| p.is_dir());
diff --git a/crates/trigger/src/cli.rs b/crates/trigger/src/cli.rs
index a462fd87..5f73e3f5 100644
--- a/crates/trigger/src/cli.rs
+++ b/crates/trigger/src/cli.rs
@@ -205,25 +205,3 @@ async fn warn_slow_wasm_build() {
     println!("Preparing Wasm modules is taking a few seconds...");
     println!();
 }
-
-pub mod help {
-    use super::*;
-
-    /// Null object to support --help-args-only in the absence of
-    /// a `spin.toml` file.
-    pub struct HelpArgsOnlyTrigger;
-
-    #[async_trait::async_trait]
-    impl TriggerExecutor for HelpArgsOnlyTrigger {
-        const TRIGGER_TYPE: &'static str = "help-args-only";
-        type RuntimeData = ();
-        type TriggerConfig = ();
-        type RunConfig = NoArgs;
-        fn new(_: crate::TriggerAppEngine<Self>) -> Result<Self> {
-            Ok(Self)
-        }
-        async fn run(self, _: Self::RunConfig) -> Result<()> {
-            Ok(())
-        }
-    }
-}
diff --git a/sdk/go/http/internals.go b/sdk/go/http/internals.go
index a3e45c4a..39bb8401 100644
--- a/sdk/go/http/internals.go
+++ b/sdk/go/http/internals.go
@@ -63,15 +63,17 @@ func toSpinHeaders(hm http.Header) C.spin_http_headers_t {
 
 	if headersLen > 0 {
 		reqHeaders.len = C.ulong(headersLen)
-		var x C.spin_http_tuple2_string_string_t
-		reqHeaders.ptr = (*C.spin_http_tuple2_string_string_t)(C.malloc(C.size_t(headersLen) * C.size_t(unsafe.Sizeof(x))))
-		headers := unsafe.Slice(reqHeaders.ptr, headersLen)
+		var x C.spin_http_string_t
+		headersPtr := C.malloc(C.size_t(headersLen) * C.size_t(unsafe.Sizeof(x)))
+		ptr := (*[1 << 30]C.spin_http_tuple2_string_string_t)(unsafe.Pointer(&headersPtr))[:headersLen:headersLen]
 
 		idx := 0
 		for k, v := range hm {
-			headers[idx] = newSpinHeader(k, v[0])
+			ptr[idx] = newSpinHeader(k, v[0])
 			idx++
 		}
+
+		reqHeaders.ptr = &ptr[0]
 	}
 	return reqHeaders
 }
@@ -91,12 +93,9 @@ func toSpinBody(body io.Reader) (C.spin_http_option_body_t, error) {
 	}
 
 	if len > 0 {
-		bodyPtr := (*C.uint8_t)(C.malloc(C.size_t(len)))
-		copy(unsafe.Slice(bodyPtr, len), buf.Bytes())
-
 		spinBody.is_some = true
 		spinBody.val = C.spin_http_body_t{
-			ptr: bodyPtr,
+			ptr: &buf.Bytes()[0],
 			len: C.size_t(len),
 		}
 	}
diff --git a/sdk/rust/Cargo.toml b/sdk/rust/Cargo.toml
index 4df70c7e..d5f60f0f 100644
--- a/sdk/rust/Cargo.toml
+++ b/sdk/rust/Cargo.toml
@@ -3,7 +3,6 @@ name = "spin-sdk"
 version = { workspace = true }
 authors = { workspace = true }
 edition = { workspace = true }
-rust-version = "1.64"
 
 [lib]
 name = "spin_sdk"
diff --git a/src/bin/spin.rs b/src/bin/spin.rs
index 6fd686f7..4149fc1e 100644
--- a/src/bin/spin.rs
+++ b/src/bin/spin.rs
@@ -15,7 +15,6 @@ use spin_cli::commands::{
 };
 use spin_http::HttpTrigger;
 use spin_redis_engine::RedisTrigger;
-use spin_trigger::cli::help::HelpArgsOnlyTrigger;
 use spin_trigger::cli::TriggerExecutorCommand;
 
 #[tokio::main]
@@ -66,8 +65,6 @@ enum SpinApp {
 enum TriggerCommands {
     Http(TriggerExecutorCommand<HttpTrigger>),
     Redis(TriggerExecutorCommand<RedisTrigger>),
-    #[clap(name = spin_cli::HELP_ARGS_ONLY_TRIGGER_TYPE, hide = true)]
-    HelpArgsOnly(TriggerExecutorCommand<HelpArgsOnlyTrigger>),
 }
 
 impl SpinApp {
@@ -83,7 +80,6 @@ impl SpinApp {
             Self::Build(cmd) => cmd.run().await,
             Self::Trigger(TriggerCommands::Http(cmd)) => cmd.run().await,
             Self::Trigger(TriggerCommands::Redis(cmd)) => cmd.run().await,
-            Self::Trigger(TriggerCommands::HelpArgsOnly(cmd)) => cmd.run().await,
             Self::Login(cmd) => cmd.run().await,
             Self::Plugins(cmd) => cmd.run().await,
             Self::External(cmd) => execute_external_subcommand(cmd, SpinApp::command()).await,
diff --git a/src/commands/new.rs b/src/commands/new.rs
index 5b581a47..c1ba3af9 100644
--- a/src/commands/new.rs
+++ b/src/commands/new.rs
@@ -7,7 +7,7 @@ use std::{
 use anyhow::{anyhow, Context, Result};
 use clap::Parser;
 use path_absolutize::Absolutize;
-use tokio;
+use tokio::{fs::File, io::AsyncReadExt};
 
 use spin_loader::local::absolutize;
 use spin_templates::{RunOptions, Template, TemplateManager, TemplateVariantInfo};
@@ -178,19 +178,17 @@ impl FromStr for ParameterValue {
     }
 }
 
-/// This function reads a file and parses it as TOML, then
-/// returns the resulting hashmap of key-value pairs.
-async fn values_from_file(path: impl AsRef<Path>) -> Result<HashMap<String, String>> {
-    // Get the absolute path of the file we're reading.
-    let path = absolutize(path)?;
+async fn values_from_file(file: impl AsRef<Path>) -> Result<HashMap<String, String>> {
+    let file = absolutize(file)?;
 
-    // Open the file.
-    let text = tokio::fs::read_to_string(&path)
+    let mut buf = vec![];
+    File::open(&file)
+        .await?
+        .read_to_end(&mut buf)
         .await
-        .with_context(|| format!("Failed to read text from values file {}", path.display()))?;
+        .with_context(|| anyhow!("Cannot read values file from {:?}", file))?;
 
-    // Parse the TOML file into a hashmap of values.
-    toml::from_str(&text).context("Failed to deserialize values file")
+    toml::from_slice(&buf).context("Failed to deserialize values file")
 }
 
 /// Merges values from file and values passed as command line options. CLI
@@ -202,7 +200,7 @@ fn merge_values(from_file: &mut HashMap<String, String>, from_cli: &[ParameterVa
 }
 
 async fn prompt_template(template_manager: &TemplateManager) -> anyhow::Result<Option<Template>> {
-    let mut templates = match list_or_install_templates(template_manager).await? {
+    let mut templates = match get_or_install_templates(template_manager).await? {
         Some(t) => t,
         None => return Ok(None),
     };
@@ -223,17 +221,48 @@ async fn prompt_template(template_manager: &TemplateManager) -> anyhow::Result<O
     Ok(Some(choice))
 }
 
-async fn list_or_install_templates(
+const DEFAULT_TEMPLATES_INSTALL_PROMPT: &str =
+    "You don't have any templates yet. Would you like to install the default set?";
+const DEFAULT_TEMPLATE_REPO: &str = "https://github.com/fermyon/spin";
+
+async fn get_or_install_templates(
     template_manager: &TemplateManager,
 ) -> anyhow::Result<Option<Vec<Template>>> {
     let templates = template_manager.list().await?.templates;
     if templates.is_empty() {
-        super::templates::prompt_install_default_templates(template_manager).await
+        let should_install = dialoguer::Confirm::new()
+            .with_prompt(DEFAULT_TEMPLATES_INSTALL_PROMPT)
+            .default(true)
+            .interact_opt()?;
+        if should_install == Some(true) {
+            install_default_templates().await?;
+            Ok(Some(template_manager.list().await?.templates))
+        } else {
+            println!(
+                "You can install the default templates later with 'spin install --git {}'",
+                DEFAULT_TEMPLATE_REPO
+            );
+            Ok(None)
+        }
     } else {
         Ok(Some(templates))
     }
 }
 
+async fn install_default_templates() -> Result<(), anyhow::Error> {
+    let install_cmd = super::templates::Install {
+        git: Some(DEFAULT_TEMPLATE_REPO.to_owned()),
+        branch: None,
+        dir: None,
+        update: false,
+    };
+    install_cmd
+        .run()
+        .await
+        .context("Failed to install the default templates")?;
+    Ok(())
+}
+
 async fn prompt_name() -> anyhow::Result<String> {
     let mut prompt = "Enter a name for your new project";
     loop {
diff --git a/src/commands/templates.rs b/src/commands/templates.rs
index 821a27a7..daeafd1c 100644
--- a/src/commands/templates.rs
+++ b/src/commands/templates.rs
@@ -13,10 +13,6 @@ use spin_templates::{
 const INSTALL_FROM_DIR_OPT: &str = "FROM_DIR";
 const INSTALL_FROM_GIT_OPT: &str = "FROM_GIT";
 
-const DEFAULT_TEMPLATES_INSTALL_PROMPT: &str =
-    "You don't have any templates yet. Would you like to install the default set?";
-const DEFAULT_TEMPLATE_REPO: &str = "https://github.com/fermyon/spin";
-
 /// Commands for working with WebAssembly component templates.
 #[derive(Subcommand, Debug)]
 pub enum TemplateCommands {
@@ -180,12 +176,9 @@ impl List {
             .await
             .context("Failed to list templates")?;
 
-        match (&self.format, list_results.templates.is_empty()) {
-            (ListFormat::Table, false) => self.print_templates_table(&list_results),
-            (ListFormat::Table, true) => {
-                prompt_install_default_templates(&template_manager).await?;
-            }
-            (ListFormat::Json, _) => self.print_templates_json(&list_results)?,
+        match self.format {
+            ListFormat::Table => self.print_templates_table(&list_results),
+            ListFormat::Json => self.print_templates_json(&list_results)?,
         };
 
         Ok(())
@@ -195,6 +188,9 @@ impl List {
         let templates = &list_results.templates;
         let warnings = &list_results.warnings;
         if templates.is_empty() {
+            println!("You have no templates installed. Run");
+            println!("spin templates install --git https://github.com/fermyon/spin");
+            println!("to install a starter set.");
             println!();
         } else {
             let mut table = Table::new();
@@ -269,36 +265,3 @@ fn list_warn_reason_text(reason: &InstalledTemplateWarning) -> String {
         InstalledTemplateWarning::InvalidManifest(msg) => format!("Template load error: {}", msg),
     }
 }
-
-pub(crate) async fn prompt_install_default_templates(
-    template_manager: &TemplateManager,
-) -> anyhow::Result<Option<Vec<Template>>> {
-    let should_install = dialoguer::Confirm::new()
-        .with_prompt(DEFAULT_TEMPLATES_INSTALL_PROMPT)
-        .default(true)
-        .interact_opt()?;
-    if should_install == Some(true) {
-        install_default_templates().await?;
-        Ok(Some(template_manager.list().await?.templates))
-    } else {
-        println!(
-            "You can install the default templates later with 'spin templates install --git {}'",
-            DEFAULT_TEMPLATE_REPO
-        );
-        Ok(None)
-    }
-}
-
-async fn install_default_templates() -> anyhow::Result<()> {
-    let install_cmd = Install {
-        git: Some(DEFAULT_TEMPLATE_REPO.to_owned()),
-        branch: None,
-        dir: None,
-        update: false,
-    };
-    install_cmd
-        .run()
-        .await
-        .context("Failed to install the default templates")?;
-    Ok(())
-}
diff --git a/src/commands/up.rs b/src/commands/up.rs
index 3098800a..7664e434 100644
--- a/src/commands/up.rs
+++ b/src/commands/up.rs
@@ -123,17 +123,6 @@ impl UpCommand {
     }
 
     async fn run_inner(self) -> Result<()> {
-        if self.help
-            && self.app.is_none()
-            && self.bindle.is_none()
-            && !PathBuf::from(DEFAULT_MANIFEST_FILE).exists()
-        {
-            return self.run_trigger(
-                trigger_command(HELP_ARGS_ONLY_TRIGGER_TYPE),
-                TriggerExecOpts::NoApp,
-            );
-        }
-
         let working_dir_holder = match &self.tmp {
             None => WorkingDirectory::Temporary(tempfile::tempdir()?),
             Some(d) => WorkingDirectory::Given(d.to_owned()),
@@ -172,41 +161,23 @@ impl UpCommand {
         }
 
         let trigger_type = match &app.info.trigger {
-            ApplicationTrigger::Http(_) => trigger_command("http"),
-            ApplicationTrigger::Redis(_) => trigger_command("redis"),
+            ApplicationTrigger::Http(_) => vec!["trigger".to_owned(), "http".to_owned()],
+            ApplicationTrigger::Redis(_) => vec!["trigger".to_owned(), "redis".to_owned()],
             ApplicationTrigger::External(cfg) => vec![resolve_trigger_plugin(cfg.trigger_type())?],
         };
 
-        let exec_opts = if self.help {
-            TriggerExecOpts::NoApp
-        } else {
-            TriggerExecOpts::App { app, working_dir }
-        };
-
-        self.run_trigger(trigger_type, exec_opts)
-    }
-
-    fn run_trigger(
-        self,
-        trigger_type: Vec<String>,
-        exec_opts: TriggerExecOpts,
-    ) -> Result<(), anyhow::Error> {
         // The docs for `current_exe` warn that this may be insecure because it could be executed
         // via hard-link. I think it should be fine as long as we aren't `setuid`ing this binary.
         let mut cmd = std::process::Command::new(std::env::current_exe().unwrap());
-        cmd.args(&trigger_type);
+        cmd.args(&trigger_type).env(SPIN_WORKING_DIR, &working_dir);
 
-        match exec_opts {
-            TriggerExecOpts::NoApp => {
-                cmd.arg("--help-args-only");
-            }
-            TriggerExecOpts::App { app, working_dir } => {
-                let locked_url = self.write_locked_app(app, &working_dir)?;
-                cmd.env(SPIN_LOCKED_URL, locked_url)
-                    .env(SPIN_WORKING_DIR, &working_dir)
-                    .args(&self.trigger_args);
-            }
-        }
+        if self.help {
+            cmd.arg("--help-args-only");
+        } else {
+            let locked_url = self.write_locked_app(app, &working_dir)?;
+            cmd.env(SPIN_LOCKED_URL, locked_url)
+                .args(&self.trigger_args);
+        };
 
         tracing::trace!("Running trigger executor: {:?}", cmd);
 
@@ -318,16 +289,3 @@ fn resolve_trigger_plugin(trigger_type: &str) -> Result<String> {
         Err(anyhow!("No built-in trigger named '{trigger_type}', and no plugin named '{subcommand}' was found"))
     }
 }
-
-#[allow(clippy::large_enum_variant)] // The large variant is the common case and really this is equivalent to an Option
-enum TriggerExecOpts {
-    NoApp,
-    App {
-        app: spin_manifest::Application,
-        working_dir: PathBuf,
-    },
-}
-
-fn trigger_command(trigger_type: &str) -> Vec<String> {
-    vec!["trigger".to_owned(), trigger_type.to_owned()]
-}
diff --git a/src/lib.rs b/src/lib.rs
index 1ad51a8c..54e45aa4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,8 +7,6 @@ use semver::BuildMetadata;
 use spin_publish::PublishError;
 use std::path::Path;
 
-pub use crate::opts::HELP_ARGS_ONLY_TRIGGER_TYPE;
-
 pub(crate) fn push_all_failed_msg(path: &Path, server_url: &str) -> String {
     format!(
         "Failed to push bindle from '{}' to the server at '{}'",
diff --git a/src/opts.rs b/src/opts.rs
index b42a82e3..cea8a46b 100644
--- a/src/opts.rs
+++ b/src/opts.rs
@@ -19,4 +19,3 @@ pub const PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT: &str = "REMOTE_PLUGIN_MANIFEST";
 pub const PLUGIN_LOCAL_PLUGIN_MANIFEST_OPT: &str = "LOCAL_PLUGIN_MANIFEST";
 pub const PLUGIN_ALL_OPT: &str = "ALL";
 pub const PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG: &str = "override-compatibility-check";
-pub const HELP_ARGS_ONLY_TRIGGER_TYPE: &str = "provide-help-args-no-app";
diff --git a/templates/http-rust/content/src/lib.rs b/templates/http-rust/content/src/lib.rs
index 6e1c9b8b..5234cf98 100644
--- a/templates/http-rust/content/src/lib.rs
+++ b/templates/http-rust/content/src/lib.rs
@@ -6,7 +6,7 @@ use spin_sdk::{
 
 /// A simple Spin HTTP component.
 #[http_component]
-fn handle_{{project-name | snake_case}}(req: Request) -> Result<Response> {
+fn {{project-name | snake_case}}(req: Request) -> Result<Response> {
     println!("{:?}", req.headers());
     Ok(http::Response::builder()
         .status(200)


Test patch:
diff --git a/tests/integration.rs b/tests/integration.rs
index 5257e9c8..944bebe3 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -146,7 +146,6 @@ mod integration_tests {
         }
 
         #[tokio::test]
-        #[cfg(not(tarpaulin))] // Flaky under `cargo tarpaulin`, was blocking CI
         async fn test_bindle_static_assets() -> Result<()> {
             // start the Bindle registry.
             let config = BindleTestControllerConfig {