diff --git a/.github/workflows/build-wasi-oci.yml b/.github/workflows/build-wasi-oci.yml new file mode 100644 index 0000000000..d7c62b5c68 --- /dev/null +++ b/.github/workflows/build-wasi-oci.yml @@ -0,0 +1,117 @@ +name: Build WASI OCI artifact + +on: + push: + branches: [master] + paths: + - 'Dockerfile.wasi' + - '.github/workflows/build-wasi-oci.yml' + - 'CMakeLists.txt' + - 'include/**' + - 'modules/**' + - 'src/**' + - 'tools/wasi/**' + - 'tools/runtime/normalize_oci_reference.py' + pull_request: + paths: + - 'Dockerfile.wasi' + - '.github/workflows/build-wasi-oci.yml' + - 'CMakeLists.txt' + - 'include/**' + - 'modules/**' + - 'src/**' + - 'tools/wasi/**' + - 'tools/runtime/normalize_oci_reference.py' + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Bootstrap digest-pinned tools + run: python3 tools/wasi/bootstrap.py + # Publish only the exact module that passed the full local WASI suite. + # verify.sh builds build-wasi/src/inspircd.wasm before exercising it. + - name: Build and fully validate the command module + env: + STRESS: '1' + run: sh tools/wasi/verify.sh + - name: Verify the WebAssembly module identity + run: | + set -eu + test "$(head -c 8 build-wasi/src/inspircd.wasm | od -An -t x1 | tr -d ' \n')" = 0061736d01000000 + sha256sum build-wasi/src/inspircd.wasm | tee build-wasi/inspircd.wasm.sha256 + - name: Set up isolated Buildx builder + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - name: Build OCI layout without publishing + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + with: + context: . + file: Dockerfile.wasi + outputs: type=oci,dest=/tmp/inspircd-wasi.oci.tar + - name: Normalize OCI import reference + run: python3 tools/runtime/normalize_oci_reference.py /tmp/inspircd-wasi.oci.tar + - name: Retain deployable artifact evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: inspircd-wasi-oci + path: | + /tmp/inspircd-wasi.oci.tar + build-wasi/inspircd.wasm.sha256 + retention-days: 7 + + publish: + needs: validate + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + permissions: + contents: read + packages: write + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Bootstrap digest-pinned tools + run: python3 tools/wasi/bootstrap.py + # Revalidate in this privileged job so the image is built from the exact + # module that passed WASI acceptance immediately before publication. + - name: Build and fully validate the command module + env: + STRESS: '1' + run: sh tools/wasi/verify.sh + - name: Verify the WebAssembly module identity + run: | + set -eu + test "$(head -c 8 build-wasi/src/inspircd.wasm | od -An -t x1 | tr -d ' \n')" = 0061736d01000000 + sha256sum build-wasi/src/inspircd.wasm | tee build-wasi/inspircd.wasm.sha256 + - name: Set up isolated Buildx builder + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - name: Publish immutable OCI artifact + id: publish + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + with: + context: . + file: Dockerfile.wasi + push: true + tags: ghcr.io/${{ github.repository_owner }}/inspircd:${{ github.sha }} + - name: Make this package public + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + await github.rest.packages.updatePackageForOrg({ + package_type: 'container', + package_name: 'inspircd', + org: context.repo.owner, + visibility: 'public', + }); + - name: Record immutable image digest + run: | + set -eu + test -n '${{ steps.publish.outputs.digest }}' + printf 'image=ghcr.io/%s/inspircd@%s\n' '${{ github.repository_owner }}' '${{ steps.publish.outputs.digest }}' >> "$GITHUB_STEP_SUMMARY" + printf 'wasm_sha256=' >> "$GITHUB_STEP_SUMMARY" + cat build-wasi/inspircd.wasm.sha256 >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/ci-oci-import-probe.yml b/.github/workflows/ci-oci-import-probe.yml new file mode 100644 index 0000000000..47969bf2c1 --- /dev/null +++ b/.github/workflows/ci-oci-import-probe.yml @@ -0,0 +1,102 @@ +name: Probe OCI import naming + +on: + pull_request: + paths: + - '.github/workflows/ci-oci-import-probe.yml' + - 'tools/runtime/oci-import-probe/**' + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + probe: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Set up isolated Buildx builder + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - name: Build small OCI layouts with documented name variants + run: | + set -euxo pipefail + docker buildx build --file tools/runtime/oci-import-probe/Dockerfile --output type=oci,dest=/tmp/unnamed.oci.tar tools/runtime/oci-import-probe + docker buildx build --file tools/runtime/oci-import-probe/Dockerfile --output type=oci,name=probe/inspircd:latest,dest=/tmp/named.oci.tar tools/runtime/oci-import-probe + - name: Add a spec-defined descriptor reference variant + run: | + set -euxo pipefail + python3 - <<'PY' + import io + import json + import tarfile + + source = "/tmp/unnamed.oci.tar" + destination = "/tmp/ref-name-latest.oci.tar" + with tarfile.open(source) as incoming, tarfile.open(destination, "w") as outgoing: + for member in incoming: + content = incoming.extractfile(member) if member.isfile() else None + if member.name == "index.json": + index = json.load(content) + descriptor = index["manifests"][0] + descriptor.setdefault("annotations", {})["org.opencontainers.image.ref.name"] = "latest" + payload = json.dumps(index, separators=(",", ":")).encode() + member.size = len(payload) + outgoing.addfile(member, io.BytesIO(payload)) + elif content is None: + outgoing.addfile(member) + else: + outgoing.addfile(member, content) + PY + - name: Inspect layouts and containerd import results + run: | + set -euxo pipefail + python3 - <<'PY' + import json + import subprocess + import tarfile + + reports = [] + for variant in ("unnamed", "named", "ref-name-latest"): + archive_path = f"/tmp/{variant}.oci.tar" + with tarfile.open(archive_path) as archive: + index = json.load(archive.extractfile("index.json")) + descriptor = index["manifests"][0] + blobs = sorted(member.name for member in archive if member.name.startswith("blobs/sha256/")) + namespace = f"oci-probe-{variant}" + imported = subprocess.run( + ["sudo", "ctr", "--namespace", namespace, "images", "import", "--base-name", "probe/inspircd", archive_path], + text=True, + capture_output=True, + ) + listed = subprocess.run( + ["sudo", "ctr", "--namespace", namespace, "images", "list", "-q"], + text=True, + check=True, + capture_output=True, + ) + reports.append({ + "variant": variant, + "descriptor_annotations": descriptor.get("annotations", {}), + "blob_digests": blobs, + "import_exit_code": imported.returncode, + "import_stdout": imported.stdout, + "import_stderr": imported.stderr, + "imported_images": listed.stdout.splitlines(), + }) + # Changing index.json must not alter any content-addressed image blob. + assert reports[0]["blob_digests"] == reports[2]["blob_digests"] + with open("oci-import-probe.json", "w") as report: + json.dump(reports, report, indent=2, sort_keys=True) + print(json.dumps(reports, indent=2, sort_keys=True)) + PY + - name: Retain OCI probe evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: oci-import-probe + path: | + oci-import-probe.json + /tmp/unnamed.oci.tar + /tmp/named.oci.tar + /tmp/ref-name-latest.oci.tar + retention-days: 7 diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml new file mode 100644 index 0000000000..e6327e3d1e --- /dev/null +++ b/.github/workflows/ci-runwasi-rc.yml @@ -0,0 +1,171 @@ +name: Validate runwasi against pinned WasmEdge RC + +on: + pull_request: + paths: + - '.github/workflows/ci-runwasi-rc.yml' + - 'tools/wasi/toolchain.json' + - 'tools/runtime/**' + - 'Dockerfile.wasi' + - 'clusters/kopf-k3s/inspircd/configmap.yaml' + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + build-shim: + runs-on: ubuntu-24.04 + timeout-minutes: 45 + env: + CANDIDATE: runwasi/target/release/containerd-shim-wasmedge-v1 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Bootstrap the checksum-pinned WasmEdge RC + run: python3 tools/wasi/bootstrap.py + - name: Fetch pinned runwasi source + run: | + set -eu + git init runwasi + git -C runwasi remote add origin https://github.com/containerd/runwasi.git + git -C runwasi fetch --depth=1 origin 5a8fc8e9ee0df28b0485da7419ee74c72127dfba + git -C runwasi checkout --detach FETCH_HEAD + test "$(git -C runwasi rev-parse HEAD)" = 5a8fc8e9ee0df28b0485da7419ee74c72127dfba + - name: Fetch and patch the official WasmEdge Rust SDK + run: | + set -euxo pipefail + git clone --depth=1 --branch 0.14.0 https://github.com/WasmEdge/WasmEdge-rust-sdk.git wasmedge-rust-sdk + test "$(git -C wasmedge-rust-sdk rev-parse HEAD)" = e7a74ab1837c43f609f26510cf3c456e9b9c2026 + git -C wasmedge-rust-sdk apply "$GITHUB_WORKSPACE/tools/runtime/patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch" + python3 - <<'PY' + from pathlib import Path + path = Path("runwasi/crates/containerd-shim-wasmedge/Cargo.toml") + source = path.read_text() + old = 'wasmedge-sdk = { version = "0.14.0", default-features = false }' + new = 'wasmedge-sdk = { path = "../../../wasmedge-rust-sdk", default-features = false }' + assert old in source + path.write_text(source.replace(old, new)) + PY + - name: Install the native protobuf compiler required by runwasi + run: | + sudo apt-get update + sudo apt-get install --yes protobuf-compiler libseccomp-dev + protoc --version + - name: Build dynamic shim against exactly the pinned RC + env: + WASMEDGE_DIR: ${{ github.workspace }}/.wasi-tools + LD_LIBRARY_PATH: ${{ github.workspace }}/.wasi-tools/lib64 + run: | + set -euxo pipefail + cargo build --manifest-path runwasi/Cargo.toml --release -p containerd-shim-wasmedge --no-default-features 2>&1 | tee runwasi-build.log + test -x "$CANDIDATE" + ldd "$CANDIDATE" | tee runwasi-ldd.log + grep -F "$WASMEDGE_DIR/lib64/libwasmedge.so.0" runwasi-ldd.log + test "$(readlink -f "$WASMEDGE_DIR/lib64/libwasmedge.so.0")" = "$WASMEDGE_DIR/lib64/libwasmedge.so.0.1.1" + - name: Set up isolated Buildx builder + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - name: Build the validated InspIRCd OCI layout + env: + STRESS: '1' + run: | + set -euxo pipefail + sh tools/wasi/verify.sh + docker buildx build --file Dockerfile.wasi --output type=oci,dest=/tmp/inspircd-wasi.oci.tar . + python3 tools/runtime/normalize_oci_reference.py /tmp/inspircd-wasi.oci.tar + python3 - /tmp/inspircd-wasi.oci.tar <<'PY' + import json + import tarfile + import sys + + with tarfile.open(sys.argv[1]) as archive: + index = json.load(archive.extractfile("index.json")) + manifests = index["manifests"] + assert len(manifests) == 1 + annotations = manifests[0]["annotations"] + assert annotations["org.opencontainers.image.ref.name"] == "latest" + assert "io.containerd.image.name" not in annotations + PY + test -x "$CANDIDATE" + test -s /tmp/inspircd-wasi.oci.tar + sha256sum "$CANDIDATE" /tmp/inspircd-wasi.oci.tar | tee runtime-artifact.sha256 + - name: Install the isolated runtime handler into the runner containerd + env: + WASMEDGE_DIR: ${{ github.workspace }}/.wasi-tools + run: | + set -euxo pipefail + test -x "$CANDIDATE" + test -s /tmp/inspircd-wasi.oci.tar + sudo install -m 0755 "$CANDIDATE" /usr/local/bin/containerd-shim-wasmedge-v1 + sudo install -m 0755 "$WASMEDGE_DIR/lib64/libwasmedge.so.0.1.1" /usr/local/lib/libwasmedge.so.0.1.1 + sudo ln -sf libwasmedge.so.0.1.1 /usr/local/lib/libwasmedge.so.0 + sudo ldconfig + sudo ctr version + sudo ctr images import --base-name local/inspircd /tmp/inspircd-wasi.oci.tar + sudo ctr images list -q | grep -Fx local/inspircd:latest + - name: Exercise OCI lifecycle, invalid configuration, and two IRC clients + run: | + set -euxo pipefail + mkdir -p /tmp/inspircd-irc + sed -n '/^ inspircd.conf: |$/,$p' clusters/kopf-k3s/inspircd/configmap.yaml | sed '1d;s/^ //' > /tmp/inspircd-irc/inspircd.conf + test -s /tmp/inspircd-irc/inspircd.conf + mkdir -p /tmp/inspircd-invalid + printf ' /tmp/inspircd-invalid/inspircd.conf + if timeout 20 sudo ctr run --rm --net-host --runtime=io.containerd.wasmedge.v1 --mount type=bind,src=/tmp/inspircd-invalid,dst=/irc,options=rbind:rw local/inspircd:latest invalid-config; then + echo 'invalid config unexpectedly started' >&2 + exit 1 + fi + sudo ctr run --net-host --runtime=io.containerd.wasmedge.v1 --mount type=bind,src=/tmp/inspircd-irc,dst=/irc,options=rbind:rw local/inspircd:latest inspircd-acceptance > inspircd-containerd.log 2>&1 & + runner=$! + trap 'sudo ctr tasks kill -s SIGKILL inspircd-acceptance 2>/dev/null || true; wait "$runner" || true' EXIT + for _ in $(seq 1 30); do python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' && break || sleep 1; done + python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' + python3 tools/runtime/containerd_irc_acceptance.py | tee irc-acceptance.log + sudo ctr tasks kill -s SIGTERM inspircd-acceptance + if timeout 10 bash -c 'while kill -0 "$1" 2>/dev/null; do sleep 0.2; done' _ "$runner"; then + echo 'SIGTERM stopped the runtime task' | tee lifecycle.log + else + # WasmEdge guest execution is non-interruptible through this shim's + # signal path. Bound this local ctr cleanup and record forced stop. + sudo ctr tasks list -q | grep -Fx inspircd-acceptance + echo 'SIGTERM grace period expired; forcing SIGKILL cleanup' | tee lifecycle.log + sudo ctr tasks kill -s SIGKILL inspircd-acceptance + timeout 15 bash -c 'while kill -0 "$1" 2>/dev/null; do sleep 0.2; done' _ "$runner" + fi + wait "$runner" || true + trap - EXIT + task_list="$(sudo ctr tasks list -q)" + ! printf '%s\n' "$task_list" | grep -Fx inspircd-acceptance + container_list="$(sudo ctr containers list -q)" + ! printf '%s\n' "$container_list" | grep -Fx inspircd-acceptance + ! python3 -c 'import socket; socket.create_connection(("127.0.0.1", 6667), 1)' + sudo ctr run --rm --net-host --runtime=io.containerd.wasmedge.v1 --mount type=bind,src=/tmp/inspircd-irc,dst=/irc,options=rbind:rw local/inspircd:latest inspircd-restart > inspircd-restart.log 2>&1 & + restart_runner=$! + trap 'sudo ctr tasks kill -s SIGKILL inspircd-restart 2>/dev/null || true; wait "$restart_runner" || true' EXIT + for _ in $(seq 1 30); do python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' && break || sleep 1; done + python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' + python3 tools/runtime/containerd_irc_acceptance.py | tee -a irc-acceptance.log + sudo ctr tasks kill -s SIGKILL inspircd-restart + timeout 15 bash -c 'while kill -0 "$1" 2>/dev/null; do sleep 0.2; done' _ "$restart_runner" + wait "$restart_runner" || true + trap - EXIT + task_list="$(sudo ctr tasks list -q)" + ! printf '%s\n' "$task_list" | grep -Fx inspircd-restart + container_list="$(sudo ctr containers list -q)" + ! printf '%s\n' "$container_list" | grep -Fx inspircd-restart + ! python3 -c 'import socket; socket.create_connection(("127.0.0.1", 6667), 1)' + - name: Retain shim build evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: runwasi-wasmedge-0.17.2-rc.1-evidence + path: | + runwasi-build.log + runwasi-ldd.log + runtime-artifact.sha256 + inspircd-containerd.log + inspircd-restart.log + irc-acceptance.log + lifecycle.log + runwasi/target/release/containerd-shim-wasmedge-v1 + if-no-files-found: warn + retention-days: 7 diff --git a/Dockerfile.wasi b/Dockerfile.wasi new file mode 100644 index 0000000000..6cefcb9677 --- /dev/null +++ b/Dockerfile.wasi @@ -0,0 +1,8 @@ +# This image contains only the WASI command module. The host-selected runwasi +# handler supplies WasmEdge; do not add a native fallback runtime here. +FROM scratch + +COPY build-wasi/src/inspircd.wasm /inspircd.wasm + +ENTRYPOINT ["/inspircd.wasm"] +CMD ["--nofork", "--config", "/irc/inspircd.conf"] diff --git a/clusters/kopf-k3s/inspircd/configmap.yaml b/clusters/kopf-k3s/inspircd/configmap.yaml new file mode 100644 index 0000000000..99a51546c5 --- /dev/null +++ b/clusters/kopf-k3s/inspircd/configmap.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: inspircd-wasi-config + namespace: gvisor-dev + labels: + app.kubernetes.io/name: inspircd-wasi + app.kubernetes.io/part-of: inspircd-wasi-experiment +data: + inspircd.conf: | + + + + + + diff --git a/clusters/kopf-k3s/inspircd/deployment.yaml b/clusters/kopf-k3s/inspircd/deployment.yaml new file mode 100644 index 0000000000..8333164f5e --- /dev/null +++ b/clusters/kopf-k3s/inspircd/deployment.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: inspircd-wasi + namespace: gvisor-dev + labels: + app.kubernetes.io/name: inspircd-wasi + app.kubernetes.io/part-of: inspircd-wasi-experiment + annotations: + inspircd.trycua.ai/status: "disabled pending a verified WasmEdge 0.17.2-rc.1 handler and immutable OCI digest" +spec: + # Keep the rendered canary inert until the runtime and OCI digest gates pass. + replicas: 0 + selector: + matchLabels: + app.kubernetes.io/name: inspircd-wasi + template: + metadata: + labels: + app.kubernetes.io/name: inspircd-wasi + app.kubernetes.io/part-of: inspircd-wasi-experiment + spec: + runtimeClassName: wasmedge-cua + automountServiceAccountToken: false + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: inspircd + # Replaced only with an OCI digest and matching WASM SHA-256. + image: registry.invalid/trycua/inspircd@sha256:0000000000000000000000000000000000000000000000000000000000000000 + imagePullPolicy: IfNotPresent + args: ["--nofork", "--config", "/irc/inspircd.conf"] + ports: + - name: irc + containerPort: 6667 + protocol: TCP + resources: + requests: + cpu: 10m + memory: 64Mi + limits: + cpu: 250m + memory: 256Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + volumeMounts: + - name: runtime + mountPath: /irc + - name: config + mountPath: /irc/inspircd.conf + subPath: inspircd.conf + readOnly: true + volumes: + - name: runtime + emptyDir: {} + - name: config + configMap: + name: inspircd-wasi-config diff --git a/clusters/kopf-k3s/inspircd/kustomization.yaml b/clusters/kopf-k3s/inspircd/kustomization.yaml new file mode 100644 index 0000000000..a7ccd01f0f --- /dev/null +++ b/clusters/kopf-k3s/inspircd/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - configmap.yaml + - service.yaml + - deployment.yaml diff --git a/clusters/kopf-k3s/inspircd/service.yaml b/clusters/kopf-k3s/inspircd/service.yaml new file mode 100644 index 0000000000..2a4541c3a7 --- /dev/null +++ b/clusters/kopf-k3s/inspircd/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: inspircd-wasi + namespace: gvisor-dev + labels: + app.kubernetes.io/name: inspircd-wasi + app.kubernetes.io/part-of: inspircd-wasi-experiment +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: inspircd-wasi + ports: + - name: irc + port: 6667 + protocol: TCP + targetPort: irc diff --git a/tools/runtime/README.md b/tools/runtime/README.md new file mode 100644 index 0000000000..275c915ecd --- /dev/null +++ b/tools/runtime/README.md @@ -0,0 +1,9 @@ +# Runtime Compatibility Experiments + +This directory holds reproducible, isolated compatibility work for the experimental RuntimeClass deployment path. It does not modify a cluster node or enable Flux. + +## WasmEdge Rust binding patch + +`patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch` applies to the official WasmEdge Rust SDK tag `0.14.0` at commit `e7a74ab1837c43f609f26510cf3c456e9b9c2026` (Apache-2.0). It adapts the C binding layer to the checksum-pinned WasmEdge `0.17.2-rc.1` headers selected by `tools/wasi/toolchain.json`. + +The patch preserves caller ownership of new `WasmEdge_LimitContext` values, destroys each temporary limit after the C type constructor returns, and uses checked conversions where the Rust SDK remains limited to wasm32 page counts. The CI workflow builds it only with pinned source and runtime inputs, then records its build and dynamic-link evidence. diff --git a/tools/runtime/containerd_irc_acceptance.py b/tools/runtime/containerd_irc_acceptance.py new file mode 100644 index 0000000000..b1cb604cda --- /dev/null +++ b/tools/runtime/containerd_irc_acceptance.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import socket +import sys +import time + + +def recv_until(sock, marker, deadline=15): + end = time.monotonic() + deadline + data = b"" + while marker not in data: + if time.monotonic() >= end: + raise RuntimeError("did not receive %r; got %r" % (marker, data)) + sock.settimeout(max(0.1, end - time.monotonic())) + data += sock.recv(4096) + return data + + +def client(nick): + sock = socket.create_connection(("127.0.0.1", 6667), timeout=15) + sock.sendall(("NICK %s\r\nUSER %s 0 * :%s\r\n" % (nick, nick, nick)).encode()) + recv_until(sock, (" 001 %s " % nick).encode()) + sock.sendall(b"JOIN #wasmedge\r\n") + recv_until(sock, b"JOIN :#wasmedge") + return sock + + +first = client("alpha") +second = client("beta") +first.sendall(b"PRIVMSG #wasmedge :alpha-to-beta\r\n") +recv_until(second, b"alpha-to-beta") +second.sendall(b"PRIVMSG #wasmedge :beta-to-alpha\r\n") +recv_until(first, b"beta-to-alpha") +for sock in (first, second): + sock.sendall(b"QUIT :integration-complete\r\n") + sock.close() +print("two-client IRC registration, join, and bidirectional messaging passed") diff --git a/tools/runtime/normalize_oci_reference.py b/tools/runtime/normalize_oci_reference.py new file mode 100644 index 0000000000..8bd3c7c8bf --- /dev/null +++ b/tools/runtime/normalize_oci_reference.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Add the OCI reference metadata containerd needs when importing a layout.""" + +import argparse +import io +import json +import os +import tarfile +import tempfile +from pathlib import Path + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("archive", type=Path) + parser.add_argument("--reference", default="latest") + args = parser.parse_args() + + with tarfile.open(args.archive) as incoming: + members = incoming.getmembers() + index_member = next((member for member in members if member.name == "index.json"), None) + if index_member is None: + raise SystemExit("OCI archive does not contain index.json") + index_stream = incoming.extractfile(index_member) + if index_stream is None: + raise SystemExit("OCI index.json is unreadable") + index = json.load(index_stream) + + manifests = index.get("manifests") + if not isinstance(manifests, list) or len(manifests) != 1: + raise SystemExit("OCI archive must have exactly one top-level manifest") + descriptor = manifests[0] + annotations = descriptor.setdefault("annotations", {}) + annotations["org.opencontainers.image.ref.name"] = args.reference + # BuildKit's containerd-private image-name annotation prevents ctr from + # materializing the OCI ref-name entry during import. + annotations.pop("io.containerd.image.name", None) + encoded_index = json.dumps(index, separators=(",", ":")).encode() + + with tempfile.NamedTemporaryFile(dir=args.archive.parent, delete=False) as temporary: + temporary_path = Path(temporary.name) + try: + with tarfile.open(args.archive) as incoming, tarfile.open(temporary_path, "w") as outgoing: + for member in incoming: + content = incoming.extractfile(member) if member.isfile() else None + if member.name == "index.json": + member.size = len(encoded_index) + outgoing.addfile(member, io.BytesIO(encoded_index)) + elif content is None: + outgoing.addfile(member) + else: + outgoing.addfile(member, content) + os.replace(temporary_path, args.archive) + finally: + temporary_path.unlink(missing_ok=True) + + +if __name__ == "__main__": + main() diff --git a/tools/runtime/oci-import-probe/Dockerfile b/tools/runtime/oci-import-probe/Dockerfile new file mode 100644 index 0000000000..62cb469112 --- /dev/null +++ b/tools/runtime/oci-import-probe/Dockerfile @@ -0,0 +1,3 @@ +FROM scratch + +LABEL org.opencontainers.image.title="inspircd-oci-import-probe" diff --git a/tools/runtime/patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch b/tools/runtime/patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch new file mode 100644 index 0000000000..1836425e3a --- /dev/null +++ b/tools/runtime/patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch @@ -0,0 +1,296 @@ +diff --git a/crates/wasmedge-sys/src/ast_module.rs b/crates/wasmedge-sys/src/ast_module.rs +index fd9f4f5..434d734 100644 +--- a/crates/wasmedge-sys/src/ast_module.rs ++++ b/crates/wasmedge-sys/src/ast_module.rs +@@ -181,7 +181,7 @@ impl ImportType<'_> { + )))), + false => { + let limit = unsafe { ffi::WasmEdge_MemoryTypeGetLimit(ctx_mem_ty) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + + Ok(ExternalInstanceType::Memory(MemoryType::new( + limit.min(), +@@ -206,7 +206,7 @@ impl ImportType<'_> { + + // get the limit + let limit = unsafe { ffi::WasmEdge_TableTypeGetLimit(ctx_tab_ty) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + + Ok(ExternalInstanceType::Table(TableType::new( + elem_ty, +@@ -321,7 +321,7 @@ impl ExportType<'_> { + + // get the limit + let limit = unsafe { ffi::WasmEdge_TableTypeGetLimit(ctx_tab_ty) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + + Ok(ExternalInstanceType::Table(TableType::new( + elem_ty, +@@ -341,7 +341,7 @@ impl ExportType<'_> { + )))), + false => { + let limit = unsafe { ffi::WasmEdge_MemoryTypeGetLimit(ctx_mem_ty) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + + Ok(ExternalInstanceType::Memory(MemoryType::new( + limit.min(), +diff --git a/crates/wasmedge-sys/src/config.rs b/crates/wasmedge-sys/src/config.rs +index 5993ced..42143ac 100644 +--- a/crates/wasmedge-sys/src/config.rs ++++ b/crates/wasmedge-sys/src/config.rs +@@ -154,12 +154,12 @@ impl Config { + /// + /// * `count` - The page count (64KB per page). + pub fn set_max_memory_pages(&mut self, count: u32) { +- unsafe { ffi::WasmEdge_ConfigureSetMaxMemoryPage(self.inner.0, count) } ++ unsafe { ffi::WasmEdge_ConfigureSetMaxMemoryPage(self.inner.0, count.into()) } + } + + /// Returns the number of the memory pages available. + pub fn get_max_memory_pages(&self) -> u32 { +- unsafe { ffi::WasmEdge_ConfigureGetMaxMemoryPage(self.inner.0) } ++ unsafe { u32::try_from(ffi::WasmEdge_ConfigureGetMaxMemoryPage(self.inner.0)).expect("WasmEdge memory64 page count is not representable by this SDK API") } + } + + /// Enables or disables the ImportExportMutGlobals option. By default, the option is enabled. +diff --git a/crates/wasmedge-sys/src/instance/memory.rs b/crates/wasmedge-sys/src/instance/memory.rs +index ff76b6b..98fe86d 100644 +--- a/crates/wasmedge-sys/src/instance/memory.rs ++++ b/crates/wasmedge-sys/src/instance/memory.rs +@@ -73,8 +73,8 @@ impl Memory { + check(ffi::WasmEdge_MemoryInstanceGetData( + self.inner.0, + data.as_mut_ptr(), +- offset, +- len, ++ offset.into(), ++ len.into(), + ))?; + data.set_len(len as usize); + } +@@ -100,8 +100,8 @@ impl Memory { + check(ffi::WasmEdge_MemoryInstanceSetData( + self.inner.0, + data.as_ref().as_ptr(), +- offset, +- data.as_ref().len() as u32, ++ offset.into(), ++ data.as_ref().len() as u64, + )) + } + } +@@ -124,7 +124,7 @@ impl Memory { + /// The lifetime of the returned pointer must not exceed that of the object itself. + /// + pub unsafe fn data_pointer(&self, offset: u32, len: u32) -> WasmEdgeResult<*const u8> { +- let ptr = unsafe { ffi::WasmEdge_MemoryInstanceGetPointerConst(self.inner.0, offset, len) }; ++ let ptr = unsafe { ffi::WasmEdge_MemoryInstanceGetPointerConst(self.inner.0, offset.into(), len.into()) }; + match ptr.is_null() { + true => Err(Box::new(WasmEdgeError::Mem(MemError::ConstPtr))), + false => Ok(ptr), +@@ -148,7 +148,7 @@ impl Memory { + /// The lifetime of the returned pointer must not exceed that of the object itself. + /// + pub unsafe fn data_pointer_mut(&mut self, offset: u32, len: u32) -> WasmEdgeResult<*mut u8> { +- let ptr = unsafe { ffi::WasmEdge_MemoryInstanceGetPointer(self.inner.0, offset, len) }; ++ let ptr = unsafe { ffi::WasmEdge_MemoryInstanceGetPointer(self.inner.0, offset.into(), len.into()) }; + match ptr.is_null() { + true => Err(Box::new(WasmEdgeError::Mem(MemError::MutPtr))), + false => Ok(ptr), +@@ -157,7 +157,7 @@ impl Memory { + + /// Returns the size, in WebAssembly pages (64 KiB of each page), of this wasm memory. + pub fn size(&self) -> u32 { +- unsafe { ffi::WasmEdge_MemoryInstanceGetPageSize(self.inner.0) } ++ unsafe { u32::try_from(ffi::WasmEdge_MemoryInstanceGetPageSize(self.inner.0)).expect("WasmEdge memory64 page count is not representable by this SDK API") } + } + + /// Grows this WebAssembly memory by `count` pages. +@@ -171,7 +171,7 @@ impl Memory { + /// If fail to grow the page count, then an error is returned. + /// + pub fn grow(&mut self, count: u32) -> WasmEdgeResult<()> { +- unsafe { check(ffi::WasmEdge_MemoryInstanceGrowPage(self.inner.0, count)) } ++ unsafe { check(ffi::WasmEdge_MemoryInstanceGrowPage(self.inner.0, count.into())) } + } + + /// # Safety +@@ -264,8 +264,9 @@ impl MemType { + if shared && max.is_none() { + return Err(Box::new(WasmEdgeError::Mem(MemError::CreateSharedType))); + } +- let ctx = +- unsafe { ffi::WasmEdge_MemoryTypeCreate(WasmEdgeLimit::new(min, max, shared).into()) }; ++ let limit = WasmEdgeLimit::new(min, max, shared); ++ let limit_ctx = limit.as_context(); ++ let ctx = unsafe { ffi::WasmEdge_MemoryTypeCreate(limit_ctx.as_ptr()) }; + match ctx.is_null() { + true => Err(Box::new(WasmEdgeError::MemTypeCreate)), + false => Ok(Self { +@@ -277,21 +278,21 @@ impl MemType { + /// Returns the initial size of a [Memory]. + pub(crate) fn min(&self) -> u32 { + let limit = unsafe { ffi::WasmEdge_MemoryTypeGetLimit(self.inner.0) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + limit.min() + } + + /// Returns the maximum size of a [Memory] allowed to grow. + pub(crate) fn max(&self) -> Option { + let limit = unsafe { ffi::WasmEdge_MemoryTypeGetLimit(self.inner.0) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + limit.max() + } + + /// Returns whether the memory is shared or not. + pub(crate) fn shared(&self) -> bool { + let limit = unsafe { ffi::WasmEdge_MemoryTypeGetLimit(self.inner.0) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + limit.shared() + } + } +diff --git a/crates/wasmedge-sys/src/instance/table.rs b/crates/wasmedge-sys/src/instance/table.rs +index b3e6520..21f7497 100644 +--- a/crates/wasmedge-sys/src/instance/table.rs ++++ b/crates/wasmedge-sys/src/instance/table.rs +@@ -79,7 +79,7 @@ impl Table { + check(ffi::WasmEdge_TableInstanceGetData( + self.inner.0, + &mut data as *mut _, +- idx, ++ idx.into(), + ))?; + data + }; +@@ -102,7 +102,7 @@ impl Table { + check(ffi::WasmEdge_TableInstanceSetData( + self.inner.0, + data.as_raw(), +- idx, ++ idx.into(), + )) + } + } +@@ -125,7 +125,7 @@ impl Table { + /// + /// If fail to increase the size of the [Table], then an error is returned. + pub fn grow(&mut self, size: u32) -> WasmEdgeResult<()> { +- unsafe { check(ffi::WasmEdge_TableInstanceGrow(self.inner.0, size)) } ++ unsafe { check(ffi::WasmEdge_TableInstanceGrow(self.inner.0, size.into())) } + } + + /// # Safety +@@ -190,9 +190,9 @@ impl TableType { + /// + pub(crate) fn create(elem_ty: RefType, min: u32, max: Option) -> WasmEdgeResult { + let ty: ValType = elem_ty.into(); +- let ctx = unsafe { +- ffi::WasmEdge_TableTypeCreate(ty.into(), WasmEdgeLimit::new(min, max, false).into()) +- }; ++ let limit = WasmEdgeLimit::new(min, max, false); ++ let limit_ctx = limit.as_context(); ++ let ctx = unsafe { ffi::WasmEdge_TableTypeCreate(ty.into(), limit_ctx.as_ptr()) }; + if ctx.is_null() { + Err(Box::new(WasmEdgeError::TableTypeCreate)) + } else { +@@ -212,14 +212,14 @@ impl TableType { + /// Returns the initial size of the [Table]. + pub(crate) fn min(&self) -> u32 { + let limit = unsafe { ffi::WasmEdge_TableTypeGetLimit(self.inner.0) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + limit.min() + } + + /// Returns the maximum size of the [Table]. + pub(crate) fn max(&self) -> Option { + let limit = unsafe { ffi::WasmEdge_TableTypeGetLimit(self.inner.0) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + limit.max() + } + } +diff --git a/crates/wasmedge-sys/src/types.rs b/crates/wasmedge-sys/src/types.rs +index 603ae56..bb6e724 100644 +--- a/crates/wasmedge-sys/src/types.rs ++++ b/crates/wasmedge-sys/src/types.rs +@@ -11,6 +11,7 @@ pub(crate) struct WasmEdgeLimit { + max: Option, + shared: bool, + } ++ + impl WasmEdgeLimit { + pub(crate) fn new(min: u32, max: Option, shared: bool) -> Self { + Self { min, max, shared } +@@ -27,26 +28,48 @@ impl WasmEdgeLimit { + pub(crate) fn shared(&self) -> bool { + self.shared + } ++ ++ // WasmEdge 0.17 owns limit contexts at the call site; type constructors ++ // borrow them, so retain this guard until the constructor returns. ++ pub(crate) fn as_context(&self) -> WasmEdgeLimitContext { ++ let ctx = unsafe { ++ match self.max { ++ Some(max) => ffi::WasmEdge_LimitCreateWithMax( ++ self.min.into(), max.into(), false, self.shared, ++ ), ++ None => ffi::WasmEdge_LimitCreate(self.min.into(), false), ++ } ++ }; ++ assert!(!ctx.is_null(), "WasmEdge failed to create a limit context"); ++ WasmEdgeLimitContext(ctx) ++ } ++ ++ pub(crate) unsafe fn from_context(ctx: *const ffi::WasmEdge_LimitContext) -> Self { ++ let min = u32::try_from(ffi::WasmEdge_LimitGetMin(ctx)) ++ .expect("WasmEdge memory64 limit is not representable by this SDK API"); ++ let max = if ffi::WasmEdge_LimitHasMax(ctx) { ++ Some( ++ u32::try_from(ffi::WasmEdge_LimitGetMax(ctx)) ++ .expect("WasmEdge memory64 limit is not representable by this SDK API"), ++ ) ++ } else { ++ None ++ }; ++ Self::new(min, max, ffi::WasmEdge_LimitIsShared(ctx)) ++ } + } +-impl From for ffi::WasmEdge_Limit { +- fn from(limit: WasmEdgeLimit) -> Self { +- let max = limit.max().unwrap_or(u32::MAX); + +- Self { +- Min: limit.min(), +- Max: max, +- HasMax: limit.max().is_some(), +- Shared: limit.shared, +- } ++pub(crate) struct WasmEdgeLimitContext(*mut ffi::WasmEdge_LimitContext); ++ ++impl WasmEdgeLimitContext { ++ pub(crate) fn as_ptr(&self) -> *const ffi::WasmEdge_LimitContext { ++ self.0 + } + } +-impl From for WasmEdgeLimit { +- fn from(limit: ffi::WasmEdge_Limit) -> Self { +- let max = match limit.HasMax { +- true => Some(limit.Max), +- false => None, +- }; +- WasmEdgeLimit::new(limit.Min, max, limit.Shared) ++ ++impl Drop for WasmEdgeLimitContext { ++ fn drop(&mut self) { ++ unsafe { ffi::WasmEdge_LimitDelete(self.0) } + } + } +