diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 16a8447c9..69a8d1d17 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: component: - description: "Component to build (gateway, cluster)" + description: "Component to build (gateway, supervisor, cluster)" required: true type: string timeout-minutes: @@ -93,4 +93,4 @@ jobs: # Enable dev-settings feature for test settings (dummy_bool, dummy_int) # used by e2e tests. EXTRA_CARGO_FEATURES: openshell-core/dev-settings - run: mise run --no-prepare docker:build:${{ inputs.component }} + run: mise run --no-prepare build:docker:${{ inputs.component }} diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index bdc8b1a71..fc42794cc 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -54,6 +54,13 @@ jobs: component: gateway cargo-version: ${{ needs.compute-versions.outputs.cargo_version }} + build-supervisor: + needs: [compute-versions] + uses: ./.github/workflows/docker-build.yml + with: + component: supervisor + cargo-version: ${{ needs.compute-versions.outputs.cargo_version }} + build-cluster: needs: [compute-versions] uses: ./.github/workflows/docker-build.yml @@ -70,7 +77,7 @@ jobs: tag-ghcr-dev: name: Tag GHCR Images as Dev - needs: [build-gateway, build-cluster] + needs: [build-gateway, build-supervisor, build-cluster] runs-on: build-amd64 timeout-minutes: 10 steps: @@ -81,7 +88,7 @@ jobs: run: | set -euo pipefail REGISTRY="ghcr.io/nvidia/openshell" - for component in gateway cluster; do + for component in gateway supervisor cluster; do echo "Tagging ${REGISTRY}/${component}:${{ github.sha }} as dev..." docker buildx imagetools create \ --prefer-index=false \ @@ -282,11 +289,6 @@ jobs: # Override z3-sys default (stdc++) so Rust links the matching runtime. echo "CXXSTDLIB=c++" >> "$GITHUB_ENV" - - name: Scope workspace to CLI crates - run: | - set -euo pipefail - sed -i 's|members = \["crates/\*"\]|members = ["crates/openshell-cli", "crates/openshell-core", "crates/openshell-bootstrap", "crates/openshell-policy", "crates/openshell-prover", "crates/openshell-providers", "crates/openshell-tui"]|' Cargo.toml - - name: Patch workspace version if: needs.compute-versions.outputs.cargo_version != '' run: | @@ -378,12 +380,247 @@ jobs: path: artifacts/*.tar.gz retention-days: 5 + # --------------------------------------------------------------------------- + # Build standalone gateway binaries (Linux GNU — native on each arch) + # --------------------------------------------------------------------------- + build-gateway-binary-linux: + name: Build Gateway Binary (Linux ${{ matrix.arch }}) + needs: [compute-versions] + strategy: + matrix: + include: + - arch: amd64 + runner: build-amd64 + target: x86_64-unknown-linux-gnu + - arch: arm64 + runner: build-arm64 + target: aarch64-unknown-linux-gnu + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + container: + image: ghcr.io/nvidia/openshell/ci:latest + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --privileged + env: + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SCCACHE_MEMCACHED_ENDPOINT: ${{ vars.SCCACHE_MEMCACHED_ENDPOINT }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Mark workspace safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Fetch tags + run: git fetch --tags --force + + - name: Install tools + run: mise install + + - name: Cache Rust target and registry + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 + with: + shared-key: gateway-binary-gnu-${{ matrix.arch }} + cache-directories: .cache/sccache + cache-targets: "true" + + - name: Patch workspace version + if: needs.compute-versions.outputs.cargo_version != '' + run: | + set -euo pipefail + sed -i -E '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*".*"/version = "'"${{ needs.compute-versions.outputs.cargo_version }}"'"/}' Cargo.toml + + - name: Build ${{ matrix.target }} + run: | + set -euo pipefail + mise x -- cargo build --release --target ${{ matrix.target }} -p openshell-server + + - name: Verify packaged binary + run: | + set -euo pipefail + OUTPUT="$(target/${{ matrix.target }}/release/openshell-gateway --version)" + echo "$OUTPUT" + grep -q '^openshell-gateway ' <<<"$OUTPUT" + + - name: sccache stats + if: always() + run: mise x -- sccache --show-stats + + - name: Package binary + run: | + set -euo pipefail + mkdir -p artifacts + tar -czf artifacts/openshell-gateway-${{ matrix.target }}.tar.gz \ + -C target/${{ matrix.target }}/release openshell-gateway + ls -lh artifacts/ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: gateway-binary-linux-${{ matrix.arch }} + path: artifacts/*.tar.gz + retention-days: 5 + + # --------------------------------------------------------------------------- + # Build standalone gateway binary (macOS aarch64 via osxcross) + # --------------------------------------------------------------------------- + build-gateway-binary-macos: + name: Build Gateway Binary (macOS) + needs: [compute-versions] + runs-on: build-amd64 + timeout-minutes: 60 + container: + image: ghcr.io/nvidia/openshell/ci:latest + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --privileged + volumes: + - /var/run/docker.sock:/var/run/docker.sock + env: + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SCCACHE_MEMCACHED_ENDPOINT: ${{ vars.SCCACHE_MEMCACHED_ENDPOINT }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Mark workspace safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Fetch tags + run: git fetch --tags --force + + - name: Log in to GHCR + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + + - name: Set up Docker Buildx + uses: ./.github/actions/setup-buildx + + - name: Build macOS binary via Docker + run: | + set -euo pipefail + docker buildx build \ + --file deploy/docker/Dockerfile.gateway-macos \ + --build-arg OPENSHELL_CARGO_VERSION="${{ needs.compute-versions.outputs.cargo_version }}" \ + --build-arg CARGO_TARGET_CACHE_SCOPE="${{ github.sha }}" \ + --target binary \ + --output type=local,dest=out/ \ + . + + - name: Verify packaged binary shape + run: | + set -euo pipefail + test -x out/openshell-gateway + + - name: Package binary + run: | + set -euo pipefail + mkdir -p artifacts + tar -czf artifacts/openshell-gateway-aarch64-apple-darwin.tar.gz \ + -C out openshell-gateway + ls -lh artifacts/ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: gateway-binary-macos + path: artifacts/*.tar.gz + retention-days: 5 + + # --------------------------------------------------------------------------- + # Build standalone supervisor binaries (Linux GNU — native on each arch) + # --------------------------------------------------------------------------- + build-supervisor-binary-linux: + name: Build Supervisor Binary (Linux ${{ matrix.arch }}) + needs: [compute-versions] + strategy: + matrix: + include: + - arch: amd64 + runner: build-amd64 + target: x86_64-unknown-linux-gnu + - arch: arm64 + runner: build-arm64 + target: aarch64-unknown-linux-gnu + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + container: + image: ghcr.io/nvidia/openshell/ci:latest + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --privileged + env: + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SCCACHE_MEMCACHED_ENDPOINT: ${{ vars.SCCACHE_MEMCACHED_ENDPOINT }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Mark workspace safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Fetch tags + run: git fetch --tags --force + + - name: Install tools + run: mise install + + - name: Cache Rust target and registry + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 + with: + shared-key: supervisor-binary-gnu-${{ matrix.arch }} + cache-directories: .cache/sccache + cache-targets: "true" + + - name: Patch workspace version + if: needs.compute-versions.outputs.cargo_version != '' + run: | + set -euo pipefail + sed -i -E '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*".*"/version = "'"${{ needs.compute-versions.outputs.cargo_version }}"'"/}' Cargo.toml + + - name: Build ${{ matrix.target }} + run: | + set -euo pipefail + mise x -- cargo build --release --target ${{ matrix.target }} -p openshell-sandbox --bin openshell-sandbox + + - name: Verify packaged binary + run: | + set -euo pipefail + OUTPUT="$(target/${{ matrix.target }}/release/openshell-sandbox --version)" + echo "$OUTPUT" + grep -q '^openshell-sandbox ' <<<"$OUTPUT" + + - name: sccache stats + if: always() + run: mise x -- sccache --show-stats + + - name: Package binary + run: | + set -euo pipefail + mkdir -p artifacts + tar -czf artifacts/openshell-sandbox-${{ matrix.target }}.tar.gz \ + -C target/${{ matrix.target }}/release openshell-sandbox + ls -lh artifacts/ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: supervisor-binary-linux-${{ matrix.arch }} + path: artifacts/*.tar.gz + retention-days: 5 + # --------------------------------------------------------------------------- # Create / update the dev GitHub Release with CLI binaries and wheels # --------------------------------------------------------------------------- release-dev: name: Release Dev - needs: [compute-versions, build-cli-linux, build-cli-macos, build-python-wheels-linux, build-python-wheel-macos] + needs: [compute-versions, build-cli-linux, build-cli-macos, build-gateway-binary-linux, build-gateway-binary-macos, build-supervisor-binary-linux, build-python-wheels-linux, build-python-wheel-macos] runs-on: build-amd64 timeout-minutes: 10 outputs: @@ -398,6 +635,20 @@ jobs: path: release/ merge-multiple: true + - name: Download gateway binary artifacts + uses: actions/download-artifact@v4 + with: + pattern: gateway-binary-* + path: release/ + merge-multiple: true + + - name: Download supervisor binary artifacts + uses: actions/download-artifact@v4 + with: + pattern: supervisor-binary-* + path: release/ + merge-multiple: true + - name: Download wheel artifacts uses: actions/download-artifact@v4 with: @@ -417,8 +668,21 @@ jobs: run: | set -euo pipefail cd release - sha256sum *.tar.gz *.whl > openshell-checksums-sha256.txt + sha256sum \ + openshell-x86_64-unknown-linux-musl.tar.gz \ + openshell-aarch64-unknown-linux-musl.tar.gz \ + openshell-aarch64-apple-darwin.tar.gz \ + *.whl > openshell-checksums-sha256.txt cat openshell-checksums-sha256.txt + sha256sum \ + openshell-gateway-x86_64-unknown-linux-gnu.tar.gz \ + openshell-gateway-aarch64-unknown-linux-gnu.tar.gz \ + openshell-gateway-aarch64-apple-darwin.tar.gz > openshell-gateway-checksums-sha256.txt + cat openshell-gateway-checksums-sha256.txt + sha256sum \ + openshell-sandbox-x86_64-unknown-linux-gnu.tar.gz \ + openshell-sandbox-aarch64-unknown-linux-gnu.tar.gz > openshell-sandbox-checksums-sha256.txt + cat openshell-sandbox-checksums-sha256.txt - name: Prune stale wheel assets from dev release uses: actions/github-script@v7 @@ -496,8 +760,15 @@ jobs: release/openshell-x86_64-unknown-linux-musl.tar.gz release/openshell-aarch64-unknown-linux-musl.tar.gz release/openshell-aarch64-apple-darwin.tar.gz + release/openshell-gateway-x86_64-unknown-linux-gnu.tar.gz + release/openshell-gateway-aarch64-unknown-linux-gnu.tar.gz + release/openshell-gateway-aarch64-apple-darwin.tar.gz + release/openshell-sandbox-x86_64-unknown-linux-gnu.tar.gz + release/openshell-sandbox-aarch64-unknown-linux-gnu.tar.gz release/*.whl release/openshell-checksums-sha256.txt + release/openshell-gateway-checksums-sha256.txt + release/openshell-sandbox-checksums-sha256.txt trigger-wheel-publish: name: Trigger Wheel Publish diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index d89d67032..9b6a94065 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -69,6 +69,13 @@ jobs: component: gateway cargo-version: ${{ needs.compute-versions.outputs.cargo_version }} + build-supervisor: + needs: [compute-versions] + uses: ./.github/workflows/docker-build.yml + with: + component: supervisor + cargo-version: ${{ needs.compute-versions.outputs.cargo_version }} + build-cluster: needs: [compute-versions] uses: ./.github/workflows/docker-build.yml @@ -85,7 +92,7 @@ jobs: tag-ghcr-release: name: Tag GHCR Images for Release - needs: [compute-versions, build-gateway, build-cluster, e2e] + needs: [compute-versions, build-gateway, build-supervisor, build-cluster, e2e] runs-on: build-amd64 timeout-minutes: 10 steps: @@ -97,7 +104,7 @@ jobs: set -euo pipefail REGISTRY="ghcr.io/nvidia/openshell" VERSION="${{ needs.compute-versions.outputs.semver }}" - for component in gateway cluster; do + for component in gateway supervisor cluster; do echo "Tagging ${REGISTRY}/${component}:${{ github.sha }} as ${VERSION} and latest..." docker buildx imagetools create \ --prefer-index=false \ @@ -305,11 +312,6 @@ jobs: # Override z3-sys default (stdc++) so Rust links the matching runtime. echo "CXXSTDLIB=c++" >> "$GITHUB_ENV" - - name: Scope workspace to CLI crates - run: | - set -euo pipefail - sed -i 's|members = \["crates/\*"\]|members = ["crates/openshell-cli", "crates/openshell-core", "crates/openshell-bootstrap", "crates/openshell-policy", "crates/openshell-prover", "crates/openshell-providers", "crates/openshell-tui"]|' Cargo.toml - - name: Patch workspace version if: needs.compute-versions.outputs.cargo_version != '' run: | @@ -402,12 +404,250 @@ jobs: path: artifacts/*.tar.gz retention-days: 5 + # --------------------------------------------------------------------------- + # Build standalone gateway binaries (Linux GNU — native on each arch) + # --------------------------------------------------------------------------- + build-gateway-binary-linux: + name: Build Gateway Binary (Linux ${{ matrix.arch }}) + needs: [compute-versions] + strategy: + matrix: + include: + - arch: amd64 + runner: build-amd64 + target: x86_64-unknown-linux-gnu + - arch: arm64 + runner: build-arm64 + target: aarch64-unknown-linux-gnu + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + container: + image: ghcr.io/nvidia/openshell/ci:latest + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --privileged + env: + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SCCACHE_MEMCACHED_ENDPOINT: ${{ vars.SCCACHE_MEMCACHED_ENDPOINT }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag || github.ref }} + fetch-depth: 0 + + - name: Mark workspace safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Fetch tags + run: git fetch --tags --force + + - name: Install tools + run: mise install + + - name: Cache Rust target and registry + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 + with: + shared-key: gateway-binary-gnu-${{ matrix.arch }} + cache-directories: .cache/sccache + cache-targets: "true" + + - name: Patch workspace version + if: needs.compute-versions.outputs.cargo_version != '' + run: | + set -euo pipefail + sed -i -E '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*".*"/version = "'"${{ needs.compute-versions.outputs.cargo_version }}"'"/}' Cargo.toml + + - name: Build ${{ matrix.target }} + run: | + set -euo pipefail + mise x -- cargo build --release --target ${{ matrix.target }} -p openshell-server + + - name: Verify packaged binary + run: | + set -euo pipefail + OUTPUT="$(target/${{ matrix.target }}/release/openshell-gateway --version)" + echo "$OUTPUT" + grep -q '^openshell-gateway ' <<<"$OUTPUT" + + - name: sccache stats + if: always() + run: mise x -- sccache --show-stats + + - name: Package binary + run: | + set -euo pipefail + mkdir -p artifacts + tar -czf artifacts/openshell-gateway-${{ matrix.target }}.tar.gz \ + -C target/${{ matrix.target }}/release openshell-gateway + ls -lh artifacts/ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: gateway-binary-linux-${{ matrix.arch }} + path: artifacts/*.tar.gz + retention-days: 5 + + # --------------------------------------------------------------------------- + # Build standalone supervisor binaries (Linux GNU — native on each arch) + # --------------------------------------------------------------------------- + build-supervisor-binary-linux: + name: Build Supervisor Binary (Linux ${{ matrix.arch }}) + needs: [compute-versions] + strategy: + matrix: + include: + - arch: amd64 + runner: build-amd64 + target: x86_64-unknown-linux-gnu + - arch: arm64 + runner: build-arm64 + target: aarch64-unknown-linux-gnu + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + container: + image: ghcr.io/nvidia/openshell/ci:latest + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --privileged + env: + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SCCACHE_MEMCACHED_ENDPOINT: ${{ vars.SCCACHE_MEMCACHED_ENDPOINT }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag || github.ref }} + fetch-depth: 0 + + - name: Mark workspace safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Fetch tags + run: git fetch --tags --force + + - name: Install tools + run: mise install + + - name: Cache Rust target and registry + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 + with: + shared-key: supervisor-binary-gnu-${{ matrix.arch }} + cache-directories: .cache/sccache + cache-targets: "true" + + - name: Patch workspace version + if: needs.compute-versions.outputs.cargo_version != '' + run: | + set -euo pipefail + sed -i -E '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*".*"/version = "'"${{ needs.compute-versions.outputs.cargo_version }}"'"/}' Cargo.toml + + - name: Build ${{ matrix.target }} + run: | + set -euo pipefail + mise x -- cargo build --release --target ${{ matrix.target }} -p openshell-sandbox --bin openshell-sandbox + + - name: Verify packaged binary + run: | + set -euo pipefail + OUTPUT="$(target/${{ matrix.target }}/release/openshell-sandbox --version)" + echo "$OUTPUT" + grep -q '^openshell-sandbox ' <<<"$OUTPUT" + + - name: sccache stats + if: always() + run: mise x -- sccache --show-stats + + - name: Package binary + run: | + set -euo pipefail + mkdir -p artifacts + tar -czf artifacts/openshell-sandbox-${{ matrix.target }}.tar.gz \ + -C target/${{ matrix.target }}/release openshell-sandbox + ls -lh artifacts/ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: supervisor-binary-linux-${{ matrix.arch }} + path: artifacts/*.tar.gz + retention-days: 5 + + # --------------------------------------------------------------------------- + # Build standalone gateway binary (macOS aarch64 via osxcross) + # --------------------------------------------------------------------------- + build-gateway-binary-macos: + name: Build Gateway Binary (macOS) + needs: [compute-versions] + runs-on: build-amd64 + timeout-minutes: 60 + container: + image: ghcr.io/nvidia/openshell/ci:latest + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + options: --privileged + volumes: + - /var/run/docker.sock:/var/run/docker.sock + env: + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SCCACHE_MEMCACHED_ENDPOINT: ${{ vars.SCCACHE_MEMCACHED_ENDPOINT }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag || github.ref }} + fetch-depth: 0 + + - name: Mark workspace safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Fetch tags + run: git fetch --tags --force + + - name: Log in to GHCR + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + + - name: Set up Docker Buildx + uses: ./.github/actions/setup-buildx + + - name: Build macOS binary via Docker + run: | + set -euo pipefail + docker buildx build \ + --file deploy/docker/Dockerfile.gateway-macos \ + --build-arg OPENSHELL_CARGO_VERSION="${{ needs.compute-versions.outputs.cargo_version }}" \ + --build-arg CARGO_TARGET_CACHE_SCOPE="${{ github.sha }}" \ + --target binary \ + --output type=local,dest=out/ \ + . + + - name: Verify packaged binary shape + run: | + set -euo pipefail + test -x out/openshell-gateway + + - name: Package binary + run: | + set -euo pipefail + mkdir -p artifacts + tar -czf artifacts/openshell-gateway-aarch64-apple-darwin.tar.gz \ + -C out openshell-gateway + ls -lh artifacts/ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: gateway-binary-macos + path: artifacts/*.tar.gz + retention-days: 5 + # --------------------------------------------------------------------------- # Create a tagged GitHub Release with CLI binaries and wheels # --------------------------------------------------------------------------- release: name: Release - needs: [compute-versions, build-cli-linux, build-cli-macos, build-python-wheels-linux, build-python-wheel-macos, tag-ghcr-release] + needs: [compute-versions, build-cli-linux, build-cli-macos, build-gateway-binary-linux, build-gateway-binary-macos, build-supervisor-binary-linux, build-python-wheels-linux, build-python-wheel-macos, tag-ghcr-release] runs-on: build-amd64 timeout-minutes: 10 outputs: @@ -424,6 +664,20 @@ jobs: path: release/ merge-multiple: true + - name: Download gateway binary artifacts + uses: actions/download-artifact@v4 + with: + pattern: gateway-binary-* + path: release/ + merge-multiple: true + + - name: Download supervisor binary artifacts + uses: actions/download-artifact@v4 + with: + pattern: supervisor-binary-* + path: release/ + merge-multiple: true + - name: Download wheel artifacts uses: actions/download-artifact@v4 with: @@ -443,8 +697,21 @@ jobs: run: | set -euo pipefail cd release - sha256sum *.tar.gz *.whl > openshell-checksums-sha256.txt + sha256sum \ + openshell-x86_64-unknown-linux-musl.tar.gz \ + openshell-aarch64-unknown-linux-musl.tar.gz \ + openshell-aarch64-apple-darwin.tar.gz \ + *.whl > openshell-checksums-sha256.txt cat openshell-checksums-sha256.txt + sha256sum \ + openshell-gateway-x86_64-unknown-linux-gnu.tar.gz \ + openshell-gateway-aarch64-unknown-linux-gnu.tar.gz \ + openshell-gateway-aarch64-apple-darwin.tar.gz > openshell-gateway-checksums-sha256.txt + cat openshell-gateway-checksums-sha256.txt + sha256sum \ + openshell-sandbox-x86_64-unknown-linux-gnu.tar.gz \ + openshell-sandbox-aarch64-unknown-linux-gnu.tar.gz > openshell-sandbox-checksums-sha256.txt + cat openshell-sandbox-checksums-sha256.txt - name: Create GitHub Release uses: softprops/action-gh-release@v2 @@ -466,8 +733,15 @@ jobs: release/openshell-x86_64-unknown-linux-musl.tar.gz release/openshell-aarch64-unknown-linux-musl.tar.gz release/openshell-aarch64-apple-darwin.tar.gz + release/openshell-gateway-x86_64-unknown-linux-gnu.tar.gz + release/openshell-gateway-aarch64-unknown-linux-gnu.tar.gz + release/openshell-gateway-aarch64-apple-darwin.tar.gz + release/openshell-sandbox-x86_64-unknown-linux-gnu.tar.gz + release/openshell-sandbox-aarch64-unknown-linux-gnu.tar.gz release/*.whl release/openshell-checksums-sha256.txt + release/openshell-gateway-checksums-sha256.txt + release/openshell-sandbox-checksums-sha256.txt publish-fern-docs: name: Publish Fern Docs diff --git a/architecture/build-containers.md b/architecture/build-containers.md index 493e7207a..196663e7a 100644 --- a/architecture/build-containers.md +++ b/architecture/build-containers.md @@ -9,7 +9,7 @@ The gateway runs the control plane API server. It is deployed as a StatefulSet i - **Docker target**: `gateway` in `deploy/docker/Dockerfile.images` - **Registry**: `ghcr.io/nvidia/openshell/gateway:latest` - **Pulled when**: Cluster startup (the Helm chart triggers the pull) -- **Entrypoint**: `openshell-server --port 8080` (gRPC + HTTP, mTLS) +- **Entrypoint**: `openshell-gateway --port 8080` (gRPC + HTTP, mTLS) ## Cluster (`openshell/cluster`) @@ -21,6 +21,18 @@ The cluster image is a single-container Kubernetes distribution that bundles the The supervisor binary (`openshell-sandbox`) is built by the shared `supervisor-builder` stage in `deploy/docker/Dockerfile.images` and placed at `/opt/openshell/bin/openshell-sandbox`. It is exposed to sandbox pods at runtime via a read-only `hostPath` volume mount — it is not baked into sandbox images. +## Standalone Gateway Binary + +OpenShell also publishes a standalone `openshell-gateway` binary as a GitHub release asset. + +- **Source crate**: `crates/openshell-server` +- **Artifact name**: `openshell-gateway-.tar.gz` +- **Targets**: `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `aarch64-apple-darwin` +- **Release workflows**: `.github/workflows/release-dev.yml`, `.github/workflows/release-tag.yml` +- **Installer**: None yet. The binary is a manual-download asset. + +Both the standalone artifact and the deployed container image use the `openshell-gateway` binary. + ## Python Wheels OpenShell also publishes Python wheels for `linux/amd64`, `linux/arm64`, and macOS ARM64. diff --git a/crates/openshell-server/Cargo.toml b/crates/openshell-server/Cargo.toml index b4e8b9e2f..33e354247 100644 --- a/crates/openshell-server/Cargo.toml +++ b/crates/openshell-server/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true repository.workspace = true [[bin]] -name = "openshell-server" +name = "openshell-gateway" path = "src/main.rs" [dependencies] diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs new file mode 100644 index 000000000..9509fe84b --- /dev/null +++ b/crates/openshell-server/src/cli.rs @@ -0,0 +1,247 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Shared CLI entrypoint for the gateway binaries. + +use clap::{Command, CommandFactory, FromArgMatches, Parser}; +use miette::{IntoDiagnostic, Result}; +use openshell_core::ComputeDriverKind; +use std::net::SocketAddr; +use std::path::PathBuf; +use tracing::info; +use tracing_subscriber::EnvFilter; + +use crate::{run_server, tracing_bus::TracingLogBus}; + +/// `OpenShell` gateway process - gRPC and HTTP server with protocol multiplexing. +#[derive(Parser, Debug)] +#[command(version = openshell_core::VERSION)] +#[command(about = "OpenShell gRPC/HTTP server", long_about = None)] +struct Args { + /// Port to bind the server to (all interfaces). + #[arg(long, default_value_t = 8080, env = "OPENSHELL_SERVER_PORT")] + port: u16, + + /// Log level (trace, debug, info, warn, error). + #[arg(long, default_value = "info", env = "OPENSHELL_LOG_LEVEL")] + log_level: String, + + /// Path to TLS certificate file (required unless --disable-tls). + #[arg(long, env = "OPENSHELL_TLS_CERT")] + tls_cert: Option, + + /// Path to TLS private key file (required unless --disable-tls). + #[arg(long, env = "OPENSHELL_TLS_KEY")] + tls_key: Option, + + /// Path to CA certificate for client certificate verification (mTLS). + #[arg(long, env = "OPENSHELL_TLS_CLIENT_CA")] + tls_client_ca: Option, + + /// Database URL for persistence. + #[arg(long, env = "OPENSHELL_DB_URL", required = true)] + db_url: String, + + /// Compute drivers configured for this gateway. + /// + /// Accepts a comma-delimited list such as `kubernetes` or + /// `kubernetes,podman`. The configuration format is future-proofed for + /// multiple drivers, but the gateway currently requires exactly one. + #[arg( + long, + alias = "driver", + env = "OPENSHELL_DRIVERS", + value_delimiter = ',', + default_value = "kubernetes", + value_parser = parse_compute_driver + )] + drivers: Vec, + + /// Kubernetes namespace for sandboxes. + #[arg(long, env = "OPENSHELL_SANDBOX_NAMESPACE", default_value = "default")] + sandbox_namespace: String, + + /// Default container image for sandboxes. + #[arg(long, env = "OPENSHELL_SANDBOX_IMAGE")] + sandbox_image: Option, + + /// Kubernetes imagePullPolicy for sandbox pods (Always, IfNotPresent, Never). + #[arg(long, env = "OPENSHELL_SANDBOX_IMAGE_PULL_POLICY")] + sandbox_image_pull_policy: Option, + + /// gRPC endpoint for sandboxes to callback to `OpenShell`. + /// This should be reachable from within the Kubernetes cluster. + #[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")] + grpc_endpoint: Option, + + /// Public host for the SSH gateway. + #[arg(long, env = "OPENSHELL_SSH_GATEWAY_HOST", default_value = "127.0.0.1")] + ssh_gateway_host: String, + + /// Public port for the SSH gateway. + #[arg(long, env = "OPENSHELL_SSH_GATEWAY_PORT", default_value_t = 8080)] + ssh_gateway_port: u16, + + /// HTTP path for SSH CONNECT/upgrade. + #[arg( + long, + env = "OPENSHELL_SSH_CONNECT_PATH", + default_value = "/connect/ssh" + )] + ssh_connect_path: String, + + /// SSH port inside sandbox pods. + #[arg(long, env = "OPENSHELL_SANDBOX_SSH_PORT", default_value_t = 2222)] + sandbox_ssh_port: u16, + + /// Shared secret for gateway-to-sandbox SSH handshake. + #[arg(long, env = "OPENSHELL_SSH_HANDSHAKE_SECRET")] + ssh_handshake_secret: Option, + + /// Allowed clock skew in seconds for SSH handshake. + #[arg(long, env = "OPENSHELL_SSH_HANDSHAKE_SKEW_SECS", default_value_t = 300)] + ssh_handshake_skew_secs: u64, + + /// Kubernetes secret name containing client TLS materials for sandbox pods. + #[arg(long, env = "OPENSHELL_CLIENT_TLS_SECRET_NAME")] + client_tls_secret_name: Option, + + /// Host gateway IP for sandbox pod hostAliases. + /// When set, sandbox pods get hostAliases entries mapping + /// host.docker.internal and host.openshell.internal to this IP. + #[arg(long, env = "OPENSHELL_HOST_GATEWAY_IP")] + host_gateway_ip: Option, + + /// Disable TLS entirely — listen on plaintext HTTP. + /// Use this when the gateway sits behind a reverse proxy or tunnel + /// (e.g. Cloudflare Tunnel) that terminates TLS at the edge. + #[arg(long, env = "OPENSHELL_DISABLE_TLS")] + disable_tls: bool, + + /// Disable gateway authentication (mTLS client certificate requirement). + /// When set, the TLS handshake accepts connections without a client + /// certificate. Ignored when --disable-tls is set. + #[arg(long, env = "OPENSHELL_DISABLE_GATEWAY_AUTH")] + disable_gateway_auth: bool, +} + +pub fn command() -> Command { + Args::command() + .name("openshell-gateway") + .bin_name("openshell-gateway") +} + +pub async fn run_cli() -> Result<()> { + rustls::crypto::ring::default_provider() + .install_default() + .map_err(|e| miette::miette!("failed to install rustls crypto provider: {e:?}"))?; + + let args = Args::from_arg_matches(&command().get_matches()).expect("clap validated args"); + + run_from_args(args).await +} + +async fn run_from_args(args: Args) -> Result<()> { + let tracing_log_bus = TracingLogBus::new(); + tracing_log_bus.install_subscriber( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)), + ); + + let bind = SocketAddr::from(([0, 0, 0, 0], args.port)); + + let tls = if args.disable_tls { + None + } else { + let cert_path = args.tls_cert.ok_or_else(|| { + miette::miette!( + "--tls-cert is required when TLS is enabled (use --disable-tls to skip)" + ) + })?; + let key_path = args.tls_key.ok_or_else(|| { + miette::miette!("--tls-key is required when TLS is enabled (use --disable-tls to skip)") + })?; + let client_ca_path = args.tls_client_ca.ok_or_else(|| { + miette::miette!( + "--tls-client-ca is required when TLS is enabled (use --disable-tls to skip)" + ) + })?; + Some(openshell_core::TlsConfig { + cert_path, + key_path, + client_ca_path, + allow_unauthenticated: args.disable_gateway_auth, + }) + }; + + let mut config = openshell_core::Config::new(tls) + .with_bind_address(bind) + .with_log_level(&args.log_level); + + config = config + .with_database_url(args.db_url) + .with_compute_drivers(args.drivers) + .with_sandbox_namespace(args.sandbox_namespace) + .with_ssh_gateway_host(args.ssh_gateway_host) + .with_ssh_gateway_port(args.ssh_gateway_port) + .with_ssh_connect_path(args.ssh_connect_path) + .with_sandbox_ssh_port(args.sandbox_ssh_port) + .with_ssh_handshake_skew_secs(args.ssh_handshake_skew_secs); + + if let Some(image) = args.sandbox_image { + config = config.with_sandbox_image(image); + } + + if let Some(policy) = args.sandbox_image_pull_policy { + config = config.with_sandbox_image_pull_policy(policy); + } + + if let Some(endpoint) = args.grpc_endpoint { + config = config.with_grpc_endpoint(endpoint); + } + + if let Some(secret) = args.ssh_handshake_secret { + config = config.with_ssh_handshake_secret(secret); + } + + if let Some(name) = args.client_tls_secret_name { + config = config.with_client_tls_secret_name(name); + } + + if let Some(ip) = args.host_gateway_ip { + config = config.with_host_gateway_ip(ip); + } + + if args.disable_tls { + info!("TLS disabled — listening on plaintext HTTP"); + } else if args.disable_gateway_auth { + info!("Gateway auth disabled — accepting connections without client certificates"); + } + + info!(bind = %config.bind_address, "Starting OpenShell server"); + + run_server(config, tracing_log_bus).await.into_diagnostic() +} + +fn parse_compute_driver(value: &str) -> std::result::Result { + value.parse() +} + +#[cfg(test)] +mod tests { + use super::command; + + #[test] + fn command_uses_gateway_binary_name() { + let mut help = Vec::new(); + command().write_long_help(&mut help).unwrap(); + let help = String::from_utf8(help).unwrap(); + assert!(help.contains("openshell-gateway")); + } + + #[test] + fn command_exposes_version() { + let cmd = command(); + let version = cmd.get_version().unwrap(); + assert_eq!(version.to_string(), openshell_core::VERSION); + } +} diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index a8d820b4d..7549a1774 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -10,6 +10,7 @@ //! - mTLS support mod auth; +pub mod cli; mod compute; mod grpc; mod http; diff --git a/crates/openshell-server/src/main.rs b/crates/openshell-server/src/main.rs index ed6c73825..0f33c685f 100644 --- a/crates/openshell-server/src/main.rs +++ b/crates/openshell-server/src/main.rs @@ -1,221 +1,11 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! `OpenShell` Server - gRPC/HTTP server with protocol multiplexing. +//! `OpenShell` Gateway binary entrypoint. -use clap::Parser; -use miette::{IntoDiagnostic, Result}; -use openshell_core::ComputeDriverKind; -use std::net::SocketAddr; -use std::path::PathBuf; -use tracing::info; -use tracing_subscriber::EnvFilter; - -use openshell_server::{run_server, tracing_bus::TracingLogBus}; - -/// `OpenShell` Server - gRPC and HTTP server with protocol multiplexing. -#[derive(Parser, Debug)] -#[command(name = "openshell-server")] -#[command(version = openshell_core::VERSION)] -#[command(about = "OpenShell gRPC/HTTP server", long_about = None)] -struct Args { - /// Port to bind the server to (all interfaces). - #[arg(long, default_value_t = 8080, env = "OPENSHELL_SERVER_PORT")] - port: u16, - - /// Log level (trace, debug, info, warn, error). - #[arg(long, default_value = "info", env = "OPENSHELL_LOG_LEVEL")] - log_level: String, - - /// Path to TLS certificate file (required unless --disable-tls). - #[arg(long, env = "OPENSHELL_TLS_CERT")] - tls_cert: Option, - - /// Path to TLS private key file (required unless --disable-tls). - #[arg(long, env = "OPENSHELL_TLS_KEY")] - tls_key: Option, - - /// Path to CA certificate for client certificate verification (mTLS). - #[arg(long, env = "OPENSHELL_TLS_CLIENT_CA")] - tls_client_ca: Option, - - /// Database URL for persistence. - #[arg(long, env = "OPENSHELL_DB_URL", required = true)] - db_url: String, - - /// Compute drivers configured for this gateway. - /// - /// Accepts a comma-delimited list such as `kubernetes` or - /// `kubernetes,podman`. The configuration format is future-proofed for - /// multiple drivers, but the gateway currently requires exactly one. - #[arg( - long, - alias = "driver", - env = "OPENSHELL_DRIVERS", - value_delimiter = ',', - default_value = "kubernetes", - value_parser = parse_compute_driver - )] - drivers: Vec, - - /// Kubernetes namespace for sandboxes. - #[arg(long, env = "OPENSHELL_SANDBOX_NAMESPACE", default_value = "default")] - sandbox_namespace: String, - - /// Default container image for sandboxes. - #[arg(long, env = "OPENSHELL_SANDBOX_IMAGE")] - sandbox_image: Option, - - /// Kubernetes imagePullPolicy for sandbox pods (Always, IfNotPresent, Never). - #[arg(long, env = "OPENSHELL_SANDBOX_IMAGE_PULL_POLICY")] - sandbox_image_pull_policy: Option, - - /// gRPC endpoint for sandboxes to callback to `OpenShell`. - /// This should be reachable from within the Kubernetes cluster. - #[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")] - grpc_endpoint: Option, - - /// Public host for the SSH gateway. - #[arg(long, env = "OPENSHELL_SSH_GATEWAY_HOST", default_value = "127.0.0.1")] - ssh_gateway_host: String, - - /// Public port for the SSH gateway. - #[arg(long, env = "OPENSHELL_SSH_GATEWAY_PORT", default_value_t = 8080)] - ssh_gateway_port: u16, - - /// HTTP path for SSH CONNECT/upgrade. - #[arg( - long, - env = "OPENSHELL_SSH_CONNECT_PATH", - default_value = "/connect/ssh" - )] - ssh_connect_path: String, - - /// SSH port inside sandbox pods. - #[arg(long, env = "OPENSHELL_SANDBOX_SSH_PORT", default_value_t = 2222)] - sandbox_ssh_port: u16, - - /// Shared secret for gateway-to-sandbox SSH handshake. - #[arg(long, env = "OPENSHELL_SSH_HANDSHAKE_SECRET")] - ssh_handshake_secret: Option, - - /// Allowed clock skew in seconds for SSH handshake. - #[arg(long, env = "OPENSHELL_SSH_HANDSHAKE_SKEW_SECS", default_value_t = 300)] - ssh_handshake_skew_secs: u64, - - /// Kubernetes secret name containing client TLS materials for sandbox pods. - #[arg(long, env = "OPENSHELL_CLIENT_TLS_SECRET_NAME")] - client_tls_secret_name: Option, - - /// Host gateway IP for sandbox pod hostAliases. - /// When set, sandbox pods get hostAliases entries mapping - /// host.docker.internal and host.openshell.internal to this IP. - #[arg(long, env = "OPENSHELL_HOST_GATEWAY_IP")] - host_gateway_ip: Option, - - /// Disable TLS entirely — listen on plaintext HTTP. - /// Use this when the gateway sits behind a reverse proxy or tunnel - /// (e.g. Cloudflare Tunnel) that terminates TLS at the edge. - #[arg(long, env = "OPENSHELL_DISABLE_TLS")] - disable_tls: bool, - - /// Disable gateway authentication (mTLS client certificate requirement). - /// When set, the TLS handshake accepts connections without a client - /// certificate. Ignored when --disable-tls is set. - #[arg(long, env = "OPENSHELL_DISABLE_GATEWAY_AUTH")] - disable_gateway_auth: bool, -} +use miette::Result; #[tokio::main] async fn main() -> Result<()> { - rustls::crypto::ring::default_provider() - .install_default() - .map_err(|e| miette::miette!("failed to install rustls crypto provider: {e:?}"))?; - - let args = Args::parse(); - - // Initialize tracing - let tracing_log_bus = TracingLogBus::new(); - tracing_log_bus.install_subscriber( - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)), - ); - - // Build configuration - let bind = SocketAddr::from(([0, 0, 0, 0], args.port)); - - let tls = if args.disable_tls { - None - } else { - let cert_path = args.tls_cert.ok_or_else(|| { - miette::miette!( - "--tls-cert is required when TLS is enabled (use --disable-tls to skip)" - ) - })?; - let key_path = args.tls_key.ok_or_else(|| { - miette::miette!("--tls-key is required when TLS is enabled (use --disable-tls to skip)") - })?; - let client_ca_path = args.tls_client_ca.ok_or_else(|| { - miette::miette!( - "--tls-client-ca is required when TLS is enabled (use --disable-tls to skip)" - ) - })?; - Some(openshell_core::TlsConfig { - cert_path, - key_path, - client_ca_path, - allow_unauthenticated: args.disable_gateway_auth, - }) - }; - - let mut config = openshell_core::Config::new(tls) - .with_bind_address(bind) - .with_log_level(&args.log_level); - - config = config - .with_database_url(args.db_url) - .with_compute_drivers(args.drivers) - .with_sandbox_namespace(args.sandbox_namespace) - .with_ssh_gateway_host(args.ssh_gateway_host) - .with_ssh_gateway_port(args.ssh_gateway_port) - .with_ssh_connect_path(args.ssh_connect_path) - .with_sandbox_ssh_port(args.sandbox_ssh_port) - .with_ssh_handshake_skew_secs(args.ssh_handshake_skew_secs); - - if let Some(image) = args.sandbox_image { - config = config.with_sandbox_image(image); - } - - if let Some(policy) = args.sandbox_image_pull_policy { - config = config.with_sandbox_image_pull_policy(policy); - } - - if let Some(endpoint) = args.grpc_endpoint { - config = config.with_grpc_endpoint(endpoint); - } - - if let Some(secret) = args.ssh_handshake_secret { - config = config.with_ssh_handshake_secret(secret); - } - - if let Some(name) = args.client_tls_secret_name { - config = config.with_client_tls_secret_name(name); - } - - if let Some(ip) = args.host_gateway_ip { - config = config.with_host_gateway_ip(ip); - } - - if args.disable_tls { - info!("TLS disabled — listening on plaintext HTTP"); - } else if args.disable_gateway_auth { - info!("Gateway auth disabled — accepting connections without client certificates"); - } - - info!(bind = %config.bind_address, "Starting OpenShell server"); - - run_server(config, tracing_log_bus).await.into_diagnostic() -} - -fn parse_compute_driver(value: &str) -> std::result::Result { - value.parse() + openshell_server::cli::run_cli().await } diff --git a/crates/openshell-server/src/persistence/postgres.rs b/crates/openshell-server/src/persistence/postgres.rs index 4b62516c4..509b028d7 100644 --- a/crates/openshell-server/src/persistence/postgres.rs +++ b/crates/openshell-server/src/persistence/postgres.rs @@ -7,7 +7,8 @@ use super::{ use openshell_core::Result; use sqlx::postgres::PgPoolOptions; use sqlx::{PgPool, Row}; -use std::path::PathBuf; + +static POSTGRES_MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations/postgres"); #[derive(Debug, Clone)] pub struct PostgresStore { @@ -26,13 +27,7 @@ impl PostgresStore { } pub async fn migrate(&self) -> Result<()> { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("migrations") - .join("postgres"); - let migrator = sqlx::migrate::Migrator::new(path) - .await - .map_err(|e| map_migrate_error(&e))?; - migrator + POSTGRES_MIGRATOR .run(&self.pool) .await .map_err(|e| map_migrate_error(&e)) diff --git a/crates/openshell-server/src/persistence/sqlite.rs b/crates/openshell-server/src/persistence/sqlite.rs index 167f3521d..3ee7799d7 100644 --- a/crates/openshell-server/src/persistence/sqlite.rs +++ b/crates/openshell-server/src/persistence/sqlite.rs @@ -7,9 +7,10 @@ use super::{ use openshell_core::Result; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use sqlx::{Row, SqlitePool}; -use std::path::PathBuf; use std::str::FromStr; +static SQLITE_MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations/sqlite"); + #[derive(Debug, Clone)] pub struct SqliteStore { pool: SqlitePool, @@ -38,13 +39,7 @@ impl SqliteStore { } pub async fn migrate(&self) -> Result<()> { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("migrations") - .join("sqlite"); - let migrator = sqlx::migrate::Migrator::new(path) - .await - .map_err(|e| map_migrate_error(&e))?; - migrator + SQLITE_MIGRATOR .run(&self.pool) .await .map_err(|e| map_migrate_error(&e)) diff --git a/crates/openshell-server/src/persistence/tests.rs b/crates/openshell-server/src/persistence/tests.rs index a5223a9f4..b3bfe2fa4 100644 --- a/crates/openshell-server/src/persistence/tests.rs +++ b/crates/openshell-server/src/persistence/tests.rs @@ -22,6 +22,16 @@ async fn sqlite_put_get_round_trip() { assert_eq!(record.payload, b"payload"); } +#[tokio::test] +async fn sqlite_connect_runs_embedded_migrations() { + let store = Store::connect("sqlite::memory:?cache=shared") + .await + .unwrap(); + + let records = store.list("sandbox", 10, 0).await.unwrap(); + assert!(records.is_empty()); +} + #[tokio::test] async fn sqlite_updates_timestamp() { let store = Store::connect("sqlite::memory:?cache=shared") diff --git a/deploy/docker/Dockerfile.gateway-macos b/deploy/docker/Dockerfile.gateway-macos new file mode 100644 index 000000000..b0ac282fc --- /dev/null +++ b/deploy/docker/Dockerfile.gateway-macos @@ -0,0 +1,105 @@ +# syntax=docker/dockerfile:1.6 + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Cross-compile the standalone openshell-gateway binary for macOS aarch64 +# (Apple Silicon) using the osxcross toolchain. + +ARG OSXCROSS_IMAGE=crazymax/osxcross:latest + +FROM ${OSXCROSS_IMAGE} AS osxcross + +FROM python:3.12-slim AS builder + +ARG CARGO_TARGET_CACHE_SCOPE=default + +ENV PATH="/root/.cargo/bin:/usr/local/bin:/osxcross/bin:${PATH}" +ENV LD_LIBRARY_PATH="/osxcross/lib" + +COPY --from=osxcross /osxcross /osxcross + +RUN SDKROOT="$(echo /osxcross/SDK/MacOSX*.sdk)" && ln -sfn "${SDKROOT}" /osxcross/SDK/MacOSX.sdk + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + clang \ + cmake \ + curl \ + libclang-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.88.0 + +RUN ln -sf /osxcross/bin/arm64-apple-darwin25.1-ld /usr/local/bin/arm64-apple-macosx-ld + +RUN rustup target add aarch64-apple-darwin + +WORKDIR /build + +ENV CC_aarch64_apple_darwin=oa64-clang +ENV CXX_aarch64_apple_darwin=oa64-clang++ +ENV AR_aarch64_apple_darwin=aarch64-apple-darwin25.1-ar +ENV CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=oa64-clang +ENV CARGO_TARGET_AARCH64_APPLE_DARWIN_AR=aarch64-apple-darwin25.1-ar +ENV SDKROOT=/osxcross/SDK/MacOSX.sdk +ENV MACOSX_DEPLOYMENT_TARGET=13.3 +ENV CFLAGS_aarch64_apple_darwin=--target=arm64-apple-macosx\ -mmacosx-version-min=13.3 +ENV CXXFLAGS_aarch64_apple_darwin=--target=arm64-apple-macosx\ -mmacosx-version-min=13.3 +ENV BINDGEN_EXTRA_CLANG_ARGS_aarch64_apple_darwin=--target=arm64-apple-macosx\ -isysroot\ ${SDKROOT} + +COPY Cargo.toml Cargo.lock ./ +COPY crates/openshell-core/Cargo.toml crates/openshell-core/Cargo.toml +COPY crates/openshell-driver-kubernetes/Cargo.toml crates/openshell-driver-kubernetes/Cargo.toml +COPY crates/openshell-policy/Cargo.toml crates/openshell-policy/Cargo.toml +COPY crates/openshell-router/Cargo.toml crates/openshell-router/Cargo.toml +COPY crates/openshell-server/Cargo.toml crates/openshell-server/Cargo.toml +COPY crates/openshell-core/build.rs crates/openshell-core/build.rs +COPY proto/ proto/ + +RUN sed -i 's|members = \["crates/\*"\]|members = ["crates/openshell-server", "crates/openshell-core", "crates/openshell-driver-kubernetes", "crates/openshell-policy", "crates/openshell-router"]|' Cargo.toml + +RUN mkdir -p crates/openshell-core/src \ + crates/openshell-driver-kubernetes/src \ + crates/openshell-policy/src \ + crates/openshell-router/src \ + crates/openshell-server/src && \ + touch crates/openshell-core/src/lib.rs && \ + touch crates/openshell-driver-kubernetes/src/lib.rs && \ + printf 'fn main() {}\n' > crates/openshell-driver-kubernetes/src/main.rs && \ + touch crates/openshell-policy/src/lib.rs && \ + touch crates/openshell-router/src/lib.rs && \ + touch crates/openshell-server/src/lib.rs && \ + printf 'fn main() {}\n' > crates/openshell-server/src/main.rs + +RUN --mount=type=cache,id=cargo-registry-gateway-macos,sharing=locked,target=/root/.cargo/registry \ + --mount=type=cache,id=cargo-git-gateway-macos,sharing=locked,target=/root/.cargo/git \ + --mount=type=cache,id=cargo-target-gateway-macos-${CARGO_TARGET_CACHE_SCOPE},sharing=locked,target=/build/target \ + cargo build --release --target aarch64-apple-darwin -p openshell-server 2>/dev/null || true + +COPY crates/ crates/ + +RUN touch crates/openshell-core/src/lib.rs \ + crates/openshell-driver-kubernetes/src/lib.rs \ + crates/openshell-driver-kubernetes/src/main.rs \ + crates/openshell-policy/src/lib.rs \ + crates/openshell-router/src/lib.rs \ + crates/openshell-server/src/lib.rs \ + crates/openshell-server/src/main.rs \ + crates/openshell-core/build.rs \ + proto/*.proto + +ARG OPENSHELL_CARGO_VERSION +RUN --mount=type=cache,id=cargo-registry-gateway-macos,sharing=locked,target=/root/.cargo/registry \ + --mount=type=cache,id=cargo-git-gateway-macos,sharing=locked,target=/root/.cargo/git \ + --mount=type=cache,id=cargo-target-gateway-macos-${CARGO_TARGET_CACHE_SCOPE},sharing=locked,target=/build/target \ + if [ -n "${OPENSHELL_CARGO_VERSION:-}" ]; then \ + sed -i -E '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*".*"/version = "'"${OPENSHELL_CARGO_VERSION}"'"/}' Cargo.toml; \ + fi && \ + cargo build --release --target aarch64-apple-darwin -p openshell-server && \ + cp target/aarch64-apple-darwin/release/openshell-gateway /openshell-gateway + +FROM scratch AS binary +COPY --from=builder /openshell-gateway /openshell-gateway diff --git a/deploy/docker/Dockerfile.images b/deploy/docker/Dockerfile.images index b7e854677..d29fa3d7f 100644 --- a/deploy/docker/Dockerfile.images +++ b/deploy/docker/Dockerfile.images @@ -7,8 +7,9 @@ # # Targets: # gateway Final gateway image +# supervisor Final supervisor image # cluster Final cluster image -# gateway-builder Release openshell-server binary +# gateway-builder Release openshell-gateway binary # supervisor-builder Release openshell-sandbox binary # supervisor-output Minimal stage exporting only the supervisor binary @@ -138,7 +139,7 @@ RUN --mount=type=cache,id=cargo-registry-${TARGETARCH},sharing=locked,target=/us . cross-build.sh && \ cargo_cross_build --release -p openshell-server ${EXTRA_CARGO_FEATURES:+--features "$EXTRA_CARGO_FEATURES"} && \ mkdir -p /build/out && \ - cp "$(cross_output_dir release)/openshell-server" /build/out/ + cp "$(cross_output_dir release)/openshell-gateway" /build/out/ FROM rust-deps AS supervisor-workspace ARG OPENSHELL_CARGO_VERSION @@ -190,7 +191,7 @@ RUN useradd --create-home --user-group openshell WORKDIR /app -COPY --from=gateway-builder /build/out/openshell-server /usr/local/bin/ +COPY --from=gateway-builder /build/out/openshell-gateway /usr/local/bin/ RUN mkdir -p /build/crates/openshell-server COPY --chmod=755 crates/openshell-server/migrations /build/crates/openshell-server/migrations @@ -198,9 +199,29 @@ COPY --chmod=755 crates/openshell-server/migrations /build/crates/openshell-serv USER openshell EXPOSE 8080 -ENTRYPOINT ["openshell-server"] +ENTRYPOINT ["openshell-gateway"] CMD ["--port", "8080"] +# --------------------------------------------------------------------------- +# Final supervisor image +# --------------------------------------------------------------------------- +FROM nvcr.io/nvidia/base/ubuntu:noble-20251013 AS supervisor + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates && \ + apt-get install -y --only-upgrade gpgv && \ + rm -rf /var/lib/apt/lists/* + +RUN useradd --create-home --user-group openshell + +WORKDIR /app + +COPY --from=supervisor-builder /build/out/openshell-sandbox /usr/local/bin/ + +USER openshell + +ENTRYPOINT ["openshell-sandbox"] + # --------------------------------------------------------------------------- # Cluster asset stages # --------------------------------------------------------------------------- diff --git a/docs/reference/support-matrix.mdx b/docs/reference/support-matrix.mdx index 744fba53c..c5eeee567 100644 --- a/docs/reference/support-matrix.mdx +++ b/docs/reference/support-matrix.mdx @@ -10,7 +10,7 @@ This page lists the platform, software, runtime, and kernel requirements for run ## Supported Platforms -OpenShell publishes multi-architecture container images for `linux/amd64` and `linux/arm64`. The CLI is supported on the following host platforms: +OpenShell publishes multi-architecture container images for `linux/amd64` and `linux/arm64`. The CLI and standalone gateway binary are supported on the following host platforms: | Platform | Architecture | Status | | -------------------------------- | --------------------- | --------- | @@ -19,6 +19,18 @@ OpenShell publishes multi-architecture container images for `linux/amd64` and `l | macOS (Docker Desktop) | Apple Silicon (arm64) | Supported | | Windows (WSL 2 + Docker Desktop) | x86_64 | Experimental | +## Standalone Gateway Binary + +OpenShell publishes standalone `openshell-gateway` release assets for manual download on these platforms: + +| Platform | Artifact pattern | +| --------------------- | ---------------------------------------------- | +| Linux x86_64 (amd64) | `openshell-gateway-x86_64-unknown-linux-gnu` | +| Linux aarch64 (arm64) | `openshell-gateway-aarch64-unknown-linux-gnu` | +| macOS Apple Silicon | `openshell-gateway-aarch64-apple-darwin` | + +These artifacts are attached to GitHub releases. `openshell gateway start` continues to use the published cluster and gateway container images. + ## Software Prerequisites The following software must be installed on the host before using the OpenShell CLI: diff --git a/tasks/docker.toml b/tasks/docker.toml index b4e370ff3..f05c8aab1 100644 --- a/tasks/docker.toml +++ b/tasks/docker.toml @@ -11,29 +11,19 @@ depends = [ ] hide = true -["docker:build"] -description = "Alias for build:docker" -depends = ["build:docker"] -hide = true - ["build:docker:ci"] description = "Build the CI Docker image" run = "tasks/scripts/docker-build-ci.sh" hide = true -["docker:build:ci"] -description = "Alias for build:docker:ci" -depends = ["build:docker:ci"] -hide = true - ["build:docker:gateway"] description = "Build the gateway Docker image" run = "tasks/scripts/docker-build-image.sh gateway" hide = true -["docker:build:gateway"] -description = "Alias for build:docker:gateway" -depends = ["build:docker:gateway"] +["build:docker:supervisor"] +description = "Build the supervisor Docker image" +run = "tasks/scripts/docker-build-image.sh supervisor" hide = true ["build:docker:cluster"] @@ -41,6 +31,11 @@ description = "Build the k3s cluster image (component images pulled at runtime f run = "tasks/scripts/docker-build-image.sh cluster" hide = true +["docker:build:gateway"] +description = "Alias for build:docker:gateway" +depends = ["build:docker:gateway"] +hide = true + ["docker:build:cluster"] description = "Alias for build:docker:cluster" depends = ["build:docker:cluster"] @@ -51,11 +46,6 @@ description = "Build multi-arch cluster image and push to a registry" run = "tasks/scripts/docker-publish-multiarch.sh" hide = true -["docker:build:cluster:multiarch"] -description = "Alias for build:docker:cluster:multiarch" -depends = ["build:docker:cluster:multiarch"] -hide = true - ["docker:cleanup"] description = "Remove stale images, volumes, and build cache not used by the current cluster" run = "scripts/docker-cleanup.sh --force" diff --git a/tasks/scripts/docker-build-image.sh b/tasks/scripts/docker-build-image.sh index 3675d7549..e429d0339 100755 --- a/tasks/scripts/docker-build-image.sh +++ b/tasks/scripts/docker-build-image.sh @@ -38,7 +38,7 @@ detect_rust_scope() { echo "no-rust" } -TARGET=${1:?"Usage: docker-build-image.sh [extra-args...]"} +TARGET=${1:?"Usage: docker-build-image.sh [extra-args...]"} shift DOCKERFILE="deploy/docker/Dockerfile.images" @@ -56,6 +56,11 @@ case "${TARGET}" in IMAGE_NAME="openshell/gateway" DOCKER_TARGET="gateway" ;; + supervisor) + IS_FINAL_IMAGE=1 + IMAGE_NAME="openshell/supervisor" + DOCKER_TARGET="supervisor" + ;; cluster) IS_FINAL_IMAGE=1 IMAGE_NAME="openshell/cluster"