diff --git a/.gitattributes b/.gitattributes index 65f0cf8c6..3bd3ea991 100644 --- a/.gitattributes +++ b/.gitattributes @@ -43,6 +43,11 @@ *.fsx text=auto *.hs text=auto +# Shell scripts must keep LF endings even when checked out on Windows; a CRLF +# turns `set -euxo pipefail` into `pipefail\r` (invalid option) once the file is +# copied into WSL. +*.sh text eol=lf + *.csproj text=auto *.vbproj text=auto *.fsproj text=auto diff --git a/.github/e2e/bootstrap-kind.sh b/.github/e2e/bootstrap-kind.sh new file mode 100755 index 000000000..f1ceba1b4 --- /dev/null +++ b/.github/e2e/bootstrap-kind.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Bring up a single node Kubernetes control plane inside the kindest/node rootfs +# that oci-to-wsl imported into WSL. kind normally runs this image as a Docker +# container and drives kubeadm from the host through `docker exec`; here WSL +# boots the image's systemd directly, so the equivalent bootstrap is performed +# in place. +set -euxo pipefail + +# The kindest/node entrypoint (which WSL does not run) makes the root mount +# shared and enables IPv4 forwarding; reproduce the parts kubeadm and the +# kubelet rely on. +mount --make-rshared / 2>/dev/null || true +sysctl -w net.ipv4.ip_forward=1 || true + +# containerd ships as a systemd unit in the node image. +systemctl enable --now containerd + +# Wait for the container runtime to accept requests. +for _ in {1..60}; do + if ctr --namespace k8s.io version >/dev/null 2>&1; then + break + fi + sleep 2 +done + +# Initialize the control plane. Preflight errors are ignored because the WSL +# environment intentionally differs from a vanilla node (swap on, kernel +# modules provided by the Windows-side kernel, and so on). +kubeadm init \ + --ignore-preflight-errors=all \ + --pod-network-cidr=10.244.0.0/16 \ + --apiserver-cert-extra-sans=127.0.0.1,localhost + +export KUBECONFIG=/etc/kubernetes/admin.conf + +# Single node cluster: allow workloads to schedule on the control-plane node. +kubectl taint nodes --all node-role.kubernetes.io/control-plane- 2>/dev/null || true + +# kindest/node bundles the default CNI (kindnet) manifest. +kubectl apply -f /kind/manifests/default-cni.yaml + +kubectl wait --for=condition=Ready nodes --all --timeout=180s diff --git a/.github/e2e/kind-wsl.yaml b/.github/e2e/kind-wsl.yaml new file mode 100644 index 000000000..bf6a71c22 --- /dev/null +++ b/.github/e2e/kind-wsl.yaml @@ -0,0 +1,21 @@ +# oci-to-wsl profile that imports the kindest/node image into WSL as a +# systemd-enabled distribution. Unlike the regular "kind in Docker" flow, the +# Kubernetes node runs natively inside WSL, so no Docker daemon or nested +# containers are required on the Windows runner. +name: kind +image: kindest/node:v1.32.2 +install_dir: C:\WSL\kind + +# kindest/node expects an init system; WSL boots systemd when configured. +wsl_conf: + mode: merge + content: | + [boot] + systemd=true + +# Stage the bootstrap script that turns the imported rootfs into a running +# single node control plane. Path is resolved relative to this profile. +files: + - src: ./bootstrap-kind.sh + dst: /usr/local/bin/bootstrap-kind.sh + mode: "0755" diff --git a/.github/workflows/buildtest.yaml b/.github/workflows/buildtest.yaml index 19c13b36b..994ff5b1c 100644 --- a/.github/workflows/buildtest.yaml +++ b/.github/workflows/buildtest.yaml @@ -87,6 +87,103 @@ jobs: exit 1 fi + # Run the same end to end suite as the Linux `e2e` job, but on Windows. There + # is no Linux Kubernetes distribution that runs natively on a Windows runner, + # so the kindest/node image (which already bundles containerd, the kubelet and + # the control-plane components) is imported straight into WSL with oci-to-wsl. + # WSL2 is enabled by default on the runner, so no separate WSL provisioning is + # needed, and the API server is reachable from the Windows host (which runs the + # test process) through WSL2 localhost forwarding. + e2e-windows-wsl: + runs-on: windows-latest + name: E2E (kind in WSL) + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - name: Setup dotnet + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + - name: Install oci-to-wsl + shell: pwsh + run: | + $url = "https://github.com/tg123/oci-to-wsl/releases/download/v0.0.1/oci-to-wsl_windows_x86_64.zip" + $expected = "79772846F1CE4A38E5B2841EA2406A070F4DB64F3DBF3EA32F04B519111FD003" + Invoke-WebRequest $url -OutFile oci-to-wsl.zip + $actual = (Get-FileHash oci-to-wsl.zip -Algorithm SHA256).Hash + if ($actual -ne $expected) { + throw "oci-to-wsl checksum mismatch: expected $expected, got $actual" + } + Expand-Archive -Force oci-to-wsl.zip -DestinationPath . + - name: Start kind cluster in WSL + shell: pwsh + run: | + # Import kindest/node into WSL as a systemd-enabled distro named "kind". + .\oci-to-wsl.exe --profile .github/e2e/kind-wsl.yaml + + # Reboot the distro so the systemd configuration written by the profile + # takes effect. + wsl --shutdown + + # WSL terminates the lightweight VM once its last session exits, which + # tears down the API server between steps and makes localhost:6443 + # connections fail with "actively refused". Hold an idle session open in + # the background so the cluster (and WSL2 localhost forwarding) stay up + # for the duration of the host-side test steps. + Start-Process -FilePath wsl -ArgumentList '-d','kind','-u','root','--','sleep','infinity' + + # Bring up the single node control plane in place. + wsl -d kind -u root -- bash /usr/local/bin/bootstrap-kind.sh + + # Export a kubeconfig for the Windows host. kubeadm points it at the WSL + # eth0 address; rewrite it to localhost, which WSL2 forwards into WSL. + wsl -d kind -u root -- cat /etc/kubernetes/admin.conf | Out-File -Encoding ascii kind.kubeconfig + (Get-Content kind.kubeconfig) -replace 'server: https://[^ ]+', 'server: https://127.0.0.1:6443' | Set-Content kind.kubeconfig + + # Wait until the API server is reachable from the Windows host through + # the forwarded port before handing off to the test steps. + $ok = $false + foreach ($i in 1..60) { + if ((Test-NetConnection -ComputerName 127.0.0.1 -Port 6443 -WarningAction SilentlyContinue).TcpTestSucceeded) { + $ok = $true + break + } + Start-Sleep -Seconds 5 + } + if (-not $ok) { + throw "API server on 127.0.0.1:6443 was not reachable from the Windows host" + } + - name: Test + shell: pwsh + env: + K8S_E2E_MINIKUBE: "1" + KUBECONFIG: ${{ github.workspace }}\kind.kubeconfig + run: | + "" | Out-File -Encoding ascii skip.log + dotnet test tests/E2E.Tests --logger "SkipTestLogger;file=$PWD/skip.log" -p:BuildInParallel=false + if ((Get-Item skip.log).Length -gt 0) { + Get-Content skip.log + Write-Error "CASES MUST NOT BE SKIPPED" + exit 1 + } + - name: AOT Test + shell: pwsh + env: + K8S_E2E_MINIKUBE: "1" + KUBECONFIG: ${{ github.workspace }}\kind.kubeconfig + run: | + "" | Out-File -Encoding ascii skip.log + dotnet test tests/E2E.Aot.Tests --logger "SkipTestLogger;file=$PWD/skip.log" -p:BuildInParallel=false + if ((Get-Item skip.log).Length -gt 0) { + Get-Content skip.log + Write-Error "CASES MUST NOT BE SKIPPED" + exit 1 + } + on: pull_request: types: [assigned, opened, synchronize, reopened]