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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
154 changes: 154 additions & 0 deletions .github/e2e/bootstrap-kind.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/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

# --------------------------------------------------------------------------
# Reproduce the setup that kind's container entrypoint (/usr/local/bin/entrypoint)
# performs when running kindest/node inside Docker. WSL boots systemd directly
# from the image's rootfs, so these steps must be done explicitly.
# --------------------------------------------------------------------------

# 1. Mount propagation — kubeadm and kubelet need shared mounts.
mount --make-rshared / 2>/dev/null || true

# 2. /dev/kmsg — kubelet reads kernel messages from this device and exits
# immediately if it is missing. WSL2 does not create it by default.
if [ ! -e /dev/kmsg ]; then
ln -sf /dev/console /dev/kmsg
fi

# 3. Kernel parameters required by kube-proxy and networking.
sysctl -w net.ipv4.ip_forward=1 || true
sysctl -w net.ipv4.conf.all.forwarding=1 || true
sysctl -w net.ipv6.conf.all.forwarding=1 2>/dev/null || true

# 4. Ensure /run is a tmpfs (systemd usually handles this, but be safe).
if ! mountpoint -q /run; then
mount -t tmpfs tmpfs /run
mkdir -p /run/lock
fi

# 5. Ensure a machine-id exists (kubelet uses it as node identity).
if [ ! -s /etc/machine-id ]; then
systemd-machine-id-setup 2>/dev/null || uuidgen | tr -d '-' > /etc/machine-id
fi

# 6. containerd configuration — use cgroupfs driver. WSL2's systemd has limited
# cgroup delegation, so the cgroupfs driver is more reliable here.
mkdir -p /etc/containerd
containerd config default \
| sed 's/SystemdCgroup = true/SystemdCgroup = false/' \
> /etc/containerd/config.toml

# 7. Start containerd.
systemctl enable --now containerd
systemctl restart containerd

# Wait for the container runtime to accept requests.
containerd_ready=0
for _ in {1..60}; do
if ctr --namespace k8s.io version >/dev/null 2>&1; then
containerd_ready=1
break
fi
sleep 2
done
if [ "$containerd_ready" -ne 1 ]; then
echo "containerd did not become ready after 120s" >&2
systemctl status containerd --no-pager || true
journalctl -xeu containerd --no-pager -n 80 || true
exit 1
fi

# 8. The kindest/node image ships a kubelet drop-in (11-kind.conf) whose
# ExecStartPre runs /kind/bin/create-kubelet-cgroup-v2.sh. That script
# expects Docker-managed cgroup namespaces and fails in bare WSL2.
# Patch it out so the kubelet service can start.
if [ -f /etc/systemd/system/kubelet.service.d/11-kind.conf ]; then
sed -i '/create-kubelet-cgroup-v2/d' /etc/systemd/system/kubelet.service.d/11-kind.conf
systemctl daemon-reload
fi
systemctl stop kubelet 2>/dev/null || true

# Initialize the control plane using a kubeadm config that forces cgroupfs and
# disables swap checking.
set +e
kubeadm init \
--ignore-preflight-errors=all \
--config /dev/stdin <<'KUBEADM_CONFIG'
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
nodeRegistration:
criSocket: unix:///run/containerd/containerd.sock
ignorePreflightErrors:
- all
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
networking:
podSubnet: 10.244.0.0/16
apiServer:
certSANs:
- "127.0.0.1"
- "localhost"
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: cgroupfs
failSwapOn: false
KUBEADM_CONFIG
rc=$?
set -e

if [ $rc -ne 0 ]; then
echo "=== kubeadm init failed (rc=$rc) — collecting diagnostics ===" >&2
systemctl status kubelet --no-pager 2>&1 || true
journalctl -xeu kubelet --no-pager -n 80 2>&1 || true
echo "=== cgroup info ===" >&2
mount | grep cgroup || true
cat /proc/self/cgroup 2>/dev/null || true
ls /sys/fs/cgroup/ 2>/dev/null || true
exit 1
fi

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

# The bundled kindnet manifest (/kind/manifests/default-cni.yaml) contains Go
# template placeholders (e.g. {{ .PodSubnet }}) that kind normally renders at
# cluster creation time. Render the template and apply; if that fails, install
# a standalone CNI config for host-local networking on this single node.
if ! sed -e 's/{{ \.PodSubnet }}/10.244.0.0\/16/g' \
-e 's/{{ \.PodSubnet}}/10.244.0.0\/16/g' \
-e 's/{{\.PodSubnet}}/10.244.0.0\/16/g' \
-e '/^{{/d' -e '/^}}/d' \
/kind/manifests/default-cni.yaml | kubectl apply -f - 2>&1; then
echo "kindnet manifest apply failed; installing host-local CNI config directly"
mkdir -p /etc/cni/net.d
cat > /etc/cni/net.d/10-kindnet.conflist <<'CNI'
{
"cniVersion": "0.4.0",
"name": "kindnet",
"plugins": [
{
"type": "ptp",
"ipMasq": false,
"ipam": { "type": "host-local", "dataDir": "/run/cni-ipam-state", "routes": [{"dst": "0.0.0.0/0"}], "ranges": [[{"subnet": "10.244.0.0/24"}]] }
},
{ "type": "portmap", "capabilities": {"portMappings": true} }
]
}
CNI
fi

# Assign podCIDR to the node so kubelet can allocate pod IPs.
NODE=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl patch node "$NODE" -p '{"spec":{"podCIDR":"10.244.0.0/24","podCIDRs":["10.244.0.0/24"]}}' 2>/dev/null || true

kubectl wait --for=condition=Ready nodes --all --timeout=300s
21 changes: 21 additions & 0 deletions .github/e2e/kind-wsl.yaml
Original file line number Diff line number Diff line change
@@ -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"
78 changes: 78 additions & 0 deletions .github/workflows/buildtest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,84 @@ jobs:
exit 1
fi

# Run the .NET Framework 4.8 e2e suite (KubernetesClient.Classic) against a
# real Kubernetes cluster on Windows. There is no native Windows Kubernetes
# distribution, so the kindest/node image (which bundles containerd, kubelet
# and the control-plane components) is imported into WSL2 with oci-to-wsl.
# The Linux e2e job already covers the modern (net8/9/10) client libraries.
# WSL2 requires windows-2025 (windows-2022 only has WSL1).
e2e-windows-wsl:
runs-on: windows-2025
name: e2e classic (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: 10.0.x
- name: Import kindest/node into WSL
uses: tg123/oci-to-wsl@902e4b0d7f3e7608dbd581e9e2fd473907a279a8 # main
with:
profile: .github/e2e/kind-wsl.yaml
Comment thread
tg123 marked this conversation as resolved.
- name: Start kind cluster in WSL
shell: pwsh
run: |
# 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. Hold an idle session open in
# the background so the cluster stays up for 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

# Get the WSL2 VM's IP address. In NAT mode the host can reach this
# IP directly via the virtual switch.
$wslIp = ((wsl -d kind -- hostname -I).Trim() -split '\s+', 0, 'RegexMatch' | Where-Object { $_ -match '^\d+\.\d+\.\d+\.\d+$' } | Select-Object -First 1)
if (-not $wslIp) { throw "Could not determine WSL IP" }
Write-Output "WSL IP: $wslIp"

# Export a kubeconfig for the Windows host, rewriting the server URL
# to the WSL VM's IP address reachable from Windows.
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://${wslIp}:6443" | Set-Content kind.kubeconfig

# .NET Framework 4.8 does not support custom trust stores, so skip TLS
# verification for the self-signed K8s CA (this is a CI-only cluster).
(Get-Content kind.kubeconfig) -replace 'certificate-authority-data:.*', 'insecure-skip-tls-verify: true' | Set-Content kind.kubeconfig

# Wait until the API server is reachable from the Windows host.
$ok = $false
foreach ($i in 1..60) {
if ((Test-NetConnection -ComputerName $wslIp -Port 6443 -WarningAction SilentlyContinue).TcpTestSucceeded) {
$ok = $true
break
}
Start-Sleep -Seconds 5
}
if (-not $ok) {
throw "API server on ${wslIp}:6443 was not reachable from the Windows host"
}
- name: Test
shell: pwsh
env:
K8S_E2E_MINIKUBE: "1"
KUBECONFIG: ${{ github.workspace }}\kind.kubeconfig
run: |
[IO.File]::WriteAllText("$PWD/skip.log", "")
dotnet test tests/E2E.Classic.Tests --logger "SkipTestLogger;file=$PWD/skip.log" -p:BuildInParallel=false
Comment thread
tg123 marked this conversation as resolved.
Comment thread
tg123 marked this conversation as resolved.
$skipContent = [IO.File]::ReadAllText("$PWD/skip.log").Trim()
if ($skipContent.Length -gt 0) {
Write-Output $skipContent
Write-Error "CASES MUST NOT BE SKIPPED"
exit 1
}

on:
pull_request:
types: [assigned, opened, synchronize, reopened]
3 changes: 2 additions & 1 deletion kubernetes-client.proj
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.Build.Traversal">
<ItemGroup>
<ProjectReference Include="src/**/*.csproj" />
<ProjectReference Include="tests/**/*.csproj" />
<ProjectReference Include="tests/**/*.csproj" Exclude="tests/E2E.Classic.Tests/**/*.csproj" />
<ProjectReference Include="tests/E2E.Classic.Tests/**/*.csproj" Condition="'$(OS)' == 'Windows_NT'" />
<ProjectReference Include="examples/**/*.csproj" />
</ItemGroup>
</Project>
Expand Down
30 changes: 30 additions & 0 deletions tests/E2E.Classic.Tests/E2E.Classic.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<RootNamespace>k8s.E2E</RootNamespace>
<TargetFramework>net48</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />

<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\KubernetesClient.Classic\KubernetesClient.Classic.csproj" />
<ProjectReference Include="..\SkipTestLogger\SkipTestLogger.csproj" />
</ItemGroup>

<ItemGroup>
<Reference Include="System.Net.Http" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\E2E.Tests\MinikubeFactAttribute.cs" />
<Compile Include="..\E2E.Tests\Onebyone.cs" />
</ItemGroup>
</Project>
Loading
Loading