Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
name: Setup spiceio
description: Install a released version of spiceio and start the S3-to-SMB proxy

inputs:
version:
description: Release tag to install (e.g. "v0.1.0"). Use "latest" for the most recent release.
required: false
default: latest
smb-url:
description: >
SMB connection URL: smb://user:pass@server/share or smb://user:pass@server:port/share.
Password can also be provided separately via smb-pass.
required: true
smb-pass:
description: SMB password (overrides password in smb-url if both provided)
required: false
default: ""
bucket:
description: Virtual S3 bucket name
required: false
default: spiceio
region:
description: AWS region to advertise
required: false
default: us-east-1
bind:
description: Listen address for the S3 endpoint
required: false
default: "127.0.0.1:8333"
token:
description: GitHub token for downloading release assets from private repos
required: true

outputs:
endpoint:
description: The S3-compatible endpoint URL
value: ${{ steps.start.outputs.endpoint }}
pid:
description: PID of the spiceio background process (empty if skipped)
value: ${{ steps.start.outputs.pid }}

runs:
using: composite
steps:
- name: Download and install spiceio
id: install
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
run: |
set -euo pipefail

REPO="spiceai/spiceio"
INSTALL_DIR="${RUNNER_TEMP}/spiceio-bin"
ASSET="spiceio-${RUNNER_OS}-${RUNNER_ARCH}.tar.gz"
VERSION="${{ inputs.version }}"

mkdir -p "$INSTALL_DIR"

if [[ "$VERSION" == "latest" ]]; then
gh release download --repo "$REPO" --pattern "$ASSET" --pattern "${ASSET}.sha256" --dir "$INSTALL_DIR"
else
gh release download "$VERSION" --repo "$REPO" --pattern "$ASSET" --pattern "${ASSET}.sha256" --dir "$INSTALL_DIR"
fi

# Verify integrity
cd "$INSTALL_DIR"
shasum -a 256 -c "${ASSET}.sha256"

tar xzf "$ASSET" -C "$INSTALL_DIR"
chmod +x "$INSTALL_DIR/spiceio"
echo "$INSTALL_DIR" >> "$GITHUB_PATH"
echo "pid_file=${RUNNER_TEMP}/spiceio.pid" >> "$GITHUB_OUTPUT"

- name: Start spiceio
id: start
shell: bash
env:
SMB_URL: ${{ inputs.smb-url }}
SMB_PASS_OVERRIDE: ${{ inputs.smb-pass }}
SPICEIO_BUCKET: ${{ inputs.bucket }}
SPICEIO_REGION: ${{ inputs.region }}
SPICEIO_BIND: ${{ inputs.bind }}
run: |
set -euo pipefail

ENDPOINT="http://${SPICEIO_BIND}"
PID_FILE="${{ steps.install.outputs.pid_file }}"

# Skip if spiceio is already listening on the requested address
# Verify it's spiceio by checking for the "spiceio" server header
if curl -sf -I "$ENDPOINT/" 2>/dev/null | grep -qi "server: spiceio"; then
echo "spiceio already running at $ENDPOINT — skipping start"
echo "endpoint=$ENDPOINT" >> "$GITHUB_OUTPUT"
echo "pid=" >> "$GITHUB_OUTPUT"
echo "skipped=true" >> "$GITHUB_OUTPUT"
exit 0
Comment thread
lukekim marked this conversation as resolved.
fi

# Parse smb://user:pass@server:port/share
# Strips the smb:// prefix, then splits on :, @, /
URL="${SMB_URL#smb://}"

USERINFO="${URL%%@*}"
HOSTPATH="${URL#*@}"

SPICEIO_SMB_USER="${USERINFO%%:*}"
# Extract password from URL only if user:pass@ format is used
if [[ "$USERINFO" == *:* ]]; then
URL_PASS="${USERINFO#*:}"
else
URL_PASS=""
fi
# smb-pass input takes priority, then URL password
if [[ -n "$SMB_PASS_OVERRIDE" ]]; then
SPICEIO_SMB_PASS="$SMB_PASS_OVERRIDE"
elif [[ -n "$URL_PASS" ]]; then
SPICEIO_SMB_PASS="$URL_PASS"
Comment thread
lukekim marked this conversation as resolved.
else
echo "::error::No SMB password provided (use smb-pass input or include in smb-url)"
exit 1
fi

HOSTPORT="${HOSTPATH%%/*}"
SPICEIO_SMB_SHARE="${HOSTPATH#*/}"

if [[ "$HOSTPORT" == *:* ]]; then
SPICEIO_SMB_SERVER="${HOSTPORT%%:*}"
SPICEIO_SMB_PORT="${HOSTPORT#*:}"
else
SPICEIO_SMB_SERVER="$HOSTPORT"
SPICEIO_SMB_PORT="445"
fi

export SPICEIO_SMB_SERVER SPICEIO_SMB_PORT SPICEIO_SMB_USER SPICEIO_SMB_PASS SPICEIO_SMB_SHARE
export SPICEIO_BUCKET SPICEIO_REGION SPICEIO_BIND

spiceio &
PID=$!
echo "$PID" > "$PID_FILE"
echo "pid=$PID" >> "$GITHUB_OUTPUT"
echo "endpoint=$ENDPOINT" >> "$GITHUB_OUTPUT"
echo "skipped=false" >> "$GITHUB_OUTPUT"

Comment thread
lukekim marked this conversation as resolved.
# Wait for readiness
echo "Waiting for spiceio on ${SPICEIO_BIND}..."
for i in $(seq 1 30); do
if curl -sf -o /dev/null "$ENDPOINT/" 2>/dev/null; then
echo "spiceio ready at $ENDPOINT (PID $PID)"
exit 0
fi
if ! kill -0 "$PID" 2>/dev/null; then
echo "::error::spiceio exited unexpectedly"
exit 1
fi
sleep 1
done
echo "::error::spiceio failed to start within 30s"
exit 1

- name: Register cleanup
if: always() && steps.start.outputs.skipped != 'true'
shell: bash
run: |
PID_FILE="${{ steps.install.outputs.pid_file }}"
if [[ -f "$PID_FILE" ]]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo "Stopping spiceio (PID $PID)"
kill "$PID" 2>/dev/null || true
wait "$PID" 2>/dev/null || true
fi
rm -f "$PID_FILE"
fi
91 changes: 91 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: CI

on:
pull_request:
types: [opened, synchronize, reopened]

Comment thread
lukekim marked this conversation as resolved.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
CARGO_NET_GIT_FETCH_WITH_CLI: true

jobs:
ci:
name: Format, lint, build, and test
# Self-hosted macOS runner required for CommonCrypto FFI and NAS access.
# Skip fork PRs to prevent untrusted code execution on self-hosted runners.
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: spiceai-macos
permissions:
contents: read

Comment thread
lukekim marked this conversation as resolved.
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt,clippy

- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2

- name: Check formatting
run: cargo fmt --all --check

- name: Run cargo check
run: cargo check --locked --all-targets --all-features

- name: Run clippy
run: cargo clippy --locked --all-targets --all-features -- -D warnings -D clippy::all -D clippy::cargo -A clippy::cargo-common-metadata

- name: Check rustdoc
run: RUSTDOCFLAGS="-D warnings" cargo doc --locked --workspace --no-deps --document-private-items

- name: Build debug binary
run: cargo build --locked

- name: Run unit tests
run: cargo test --locked

- name: Check SMB credentials
run: |
if [[ -n "$SPICEIO_SMB_PASS" ]]; then
echo "HAS_SMB_PASS=true" >> "$GITHUB_ENV"
fi
env:
SPICEIO_SMB_PASS: ${{ secrets.UNAS_SMB_PASS }}

- name: Install AWS CLI
if: ${{ env.HAS_SMB_PASS == 'true' }}
run: |
if ! command -v aws &>/dev/null; then
brew install awscli
fi

- name: Run sccache integration test
# Skipped when UNAS_SMB_PASS secret is not configured (e.g. fork PRs)
if: ${{ env.HAS_SMB_PASS == 'true' }}
env:
SPICEIO_SMB_SERVER: ${{ vars.SPICEIO_SMB_SERVER || '192.168.3.148' }}
SPICEIO_SMB_USER: ${{ vars.SPICEIO_SMB_USER || 'runner' }}
SPICEIO_SMB_PASS: ${{ secrets.UNAS_SMB_PASS }}
SPICEIO_SMB_SHARE: ${{ vars.SPICEIO_SMB_SHARE || 'ai_platform_dev' }}
SPICEIO_BUCKET: ${{ vars.SPICEIO_BUCKET || 'spiceio' }}
SPICEIO_REGION: ${{ vars.SPICEIO_REGION || 'us-west-1' }}
run: ./scripts/test-sccache.sh
Comment thread
lukekim marked this conversation as resolved.
Comment thread
lukekim marked this conversation as resolved.
Comment thread
lukekim marked this conversation as resolved.

- name: Build release artifact
run: cargo build --release --locked --bin spiceio

- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: spiceio-${{ runner.os }}-${{ runner.arch }}
path: target/release/spiceio
if-no-files-found: error
49 changes: 49 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Release

on:
release:
types: [created]
Comment thread
lukekim marked this conversation as resolved.

Comment thread
lukekim marked this conversation as resolved.
Comment thread
lukekim marked this conversation as resolved.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
CARGO_NET_GIT_FETCH_WITH_CLI: true

jobs:
build:
name: Build release binary
# Self-hosted macOS runner required for CommonCrypto FFI
runs-on: spiceai-macos
permissions:
contents: write

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2

- name: Build release binary
run: cargo build --release --locked --bin spiceio

- name: Package binary
run: |
cd target/release
tar czf spiceio-${{ runner.os }}-${{ runner.arch }}.tar.gz spiceio
shasum -a 256 spiceio-${{ runner.os }}-${{ runner.arch }}.tar.gz > spiceio-${{ runner.os }}-${{ runner.arch }}.tar.gz.sha256

- name: Upload release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload "${{ github.event.release.tag_name }}" --clobber \
target/release/spiceio-${{ runner.os }}-${{ runner.arch }}.tar.gz \
target/release/spiceio-${{ runner.os }}-${{ runner.arch }}.tar.gz.sha256
15 changes: 15 additions & 0 deletions scripts/test-sccache.sh
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ CARGO_TARGET_DIR="$TEST_TARGET_DIR" cargo build 2>&1
echo ""
echo "[test] === warm build (should hit cache) ==="
rm -rf "$TEST_TARGET_DIR"
sccache --zero-stats 2>/dev/null || true
CARGO_TARGET_DIR="$TEST_TARGET_DIR" cargo build 2>&1

echo ""
Expand All @@ -267,3 +268,17 @@ echo "[test] sccache stats:"
echo "======================================="
sccache --show-stats
echo "======================================="

# ── Verify cache hits ───────────────────────────────────────────────────────

STATS=$(sccache --show-stats 2>&1)
CACHE_HITS=$(echo "$STATS" | grep -m1 "^Cache hits" | awk '{print $NF}' || echo "0")
WRITE_ERRORS=$(echo "$STATS" | grep -m1 "Cache write errors" | awk '{print $NF}' || echo "0")

echo ""
if [[ "${CACHE_HITS:-0}" -gt 0 && "${WRITE_ERRORS:-0}" -eq 0 ]]; then
echo "[test] PASS: warm build got $CACHE_HITS cache hits, 0 write errors"
else
echo "[test] FAIL: expected cache hits > 0 (got ${CACHE_HITS:-0}) and write errors == 0 (got ${WRITE_ERRORS:-0})"
Comment thread
lukekim marked this conversation as resolved.
exit 1
fi
4 changes: 3 additions & 1 deletion src/s3/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,9 @@ async fn handle_complete_multipart_upload(
return error_response(
StatusCode::BAD_REQUEST,
"InvalidPart",
&format!("One or more of the specified parts could not be found. Part number: {pn}"),
&format!(
"One or more of the specified parts could not be found. Part number: {pn}"
),
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/smb/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ pub fn wrap_spnego_auth(ntlmssp: &[u8]) -> Vec<u8> {
der_wrap(0xa1, &seq)
}

/// Wrap data in a DER TLV: [tag][length][data].
/// Wrap data in a DER TLV: `[tag][length][data]`.
fn der_wrap(tag: u8, data: &[u8]) -> Vec<u8> {
let mut buf = Vec::with_capacity(1 + 4 + data.len());
buf.push(tag);
Expand Down
1 change: 0 additions & 1 deletion src/smb/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,4 +558,3 @@ fn guess_content_type(key: &str) -> String {
}
.into()
}

Loading