Skip to content

Commit 9992d8d

Browse files
committed
integration: add the k3s Tailscale Kubernetes operator dimension
Add a k3s plus Tailscale Kubernetes operator integration dimension and run TestK8sOperator in a dedicated CI job.
1 parent 622e08f commit 9992d8d

8 files changed

Lines changed: 1302 additions & 2 deletions

File tree

.github/workflows/gh-action-integration-generator.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,27 @@ var testsToSplit = map[string][]string{
3939
},
4040
}
4141

42+
// testsExcludedFromMatrix are tests that must NOT be added to the shared
43+
// sqlite/postgres matrices because they have special runner requirements and
44+
// run in their own dedicated job. TestK8sOperator needs a runner that permits
45+
// privileged cgroup-v2 containers (it runs a real k3s cluster), which the
46+
// shared integration-test-template job is not guaranteed to provide.
47+
var testsExcludedFromMatrix = map[string]bool{
48+
"TestK8sOperator": true,
49+
}
50+
51+
// filterExcluded drops tests that run in their own dedicated job.
52+
func filterExcluded(tests []string) []string {
53+
var kept []string
54+
for _, test := range tests {
55+
if testsExcludedFromMatrix[test] {
56+
continue
57+
}
58+
kept = append(kept, test)
59+
}
60+
return kept
61+
}
62+
4263
// expandTests takes a list of test names and expands any that need splitting
4364
// into multiple subtest patterns.
4465
func expandTests(tests []string) []string {
@@ -117,6 +138,9 @@ func updateYAML(tests []string, jobName string, testPath string) {
117138
func main() {
118139
tests := findTests()
119140

141+
// Drop tests that run in their own dedicated job (special runner needs).
142+
tests = filterExcluded(tests)
143+
120144
// Expand tests that should be split into multiple jobs
121145
expandedTests := expandTests(tests)
122146

.github/workflows/integration-test-template.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ jobs:
7878
echo '{"storage-driver":"overlay2"}' | sudo tee /etc/docker/daemon.json
7979
sudo systemctl restart docker
8080
docker version
81+
- name: Load br_netfilter for in-cluster service routing
82+
# TestK8sOperator runs k3s in a container; without br_netfilter on the
83+
# host, bridged pod-to-pod traffic skips kube-proxy's ClusterIP DNAT and
84+
# in-cluster DNS (kube-dns) is unreachable. The module cannot be loaded
85+
# from inside the unprivileged-module rancher/k3s image, so load it here.
86+
run: sudo modprobe br_netfilter
8187
- name: Login to Docker Hub
8288
env:
8389
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_CI_USERNAME }}

.github/workflows/test-integration.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,3 +403,25 @@ jobs:
403403
test: ${{ matrix.test }}
404404
postgres_flag: "--postgres=1"
405405
database_name: "postgres"
406+
# k8s-operator: TestK8sOperator stands up a real k3s cluster (rancher/k3s) as
407+
# a privileged sibling container and installs the Tailscale Kubernetes
408+
# operator via Helm against an in-test Headscale.
409+
#
410+
# PRIVILEGED-CI REQUIREMENT: this job MUST run on a runner that permits
411+
# privileged containers with a writable cgroup-v2 hierarchy (k3s runs
412+
# containerd, manages iptables/ipvs and mounts /sys/fs/cgroup). The standard
413+
# GitHub-hosted ubuntu-24.04-arm runner used by the shared template supports
414+
# this; self-hosted or hardened runners that disallow --privileged or remount
415+
# /sys/fs/cgroup read-only will fail this test. It is kept out of the
416+
# generator-managed sqlite/postgres matrices (see
417+
# .github/workflows/gh-action-integration-generator.go) precisely because of
418+
# this special requirement.
419+
k8s-operator:
420+
needs: [build, build-tailscale-released]
421+
if: needs.build.outputs.files-changed == 'true'
422+
uses: ./.github/workflows/integration-test-template.yml
423+
secrets: inherit
424+
with:
425+
test: "TestK8sOperator"
426+
postgres_flag: "--postgres=0"
427+
database_name: "sqlite"

cmd/hi/cleanup.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ func isTestContainerName(names []string) bool {
194194
if strings.Contains(name, "headscale-test-suite") ||
195195
strings.Contains(name, "hs-") ||
196196
strings.Contains(name, "ts-") ||
197-
strings.Contains(name, "derp-") {
197+
strings.Contains(name, "derp-") ||
198+
strings.Contains(name, "k3s-") {
198199
return true
199200
}
200201
}

cmd/hi/docker.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,9 @@ func getCurrentTestContainers(containers []container.Summary, testContainerID st
735735
for _, cont := range containers {
736736
for _, name := range cont.Names {
737737
containerName := strings.TrimPrefix(name, "/")
738-
if strings.HasPrefix(containerName, "hs-") || strings.HasPrefix(containerName, "ts-") {
738+
if strings.HasPrefix(containerName, "hs-") ||
739+
strings.HasPrefix(containerName, "ts-") ||
740+
strings.HasPrefix(containerName, "k3s-") {
739741
// Check if container has matching run ID label
740742
if cont.Labels != nil && cont.Labels["hi.run-id"] == runID {
741743
testRunContainers = append(testRunContainers, testContainer{

cmd/hi/doctor.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111

1212
"github.com/juanfont/headscale/integration/dockertestutil"
13+
"github.com/juanfont/headscale/integration/k3sic"
1314
)
1415

1516
const (
@@ -21,6 +22,7 @@ const (
2122
nameDockerContext = "Docker Context"
2223
nameDockerSocket = "Docker Socket"
2324
nameGolangImage = "Golang Image"
25+
nameK3sImage = "K3s Image"
2426
nameGoInstall = "Go Installation"
2527
)
2628

@@ -66,6 +68,7 @@ func runDoctorCheck(ctx context.Context) error {
6668
results = append(results, checkDockerSocket(ctx))
6769
results = append(results, checkDockerHubCredentials())
6870
results = append(results, checkGolangImage(ctx))
71+
results = append(results, checkK3sImage(ctx))
6972
}
7073

7174
// Check 3: Go installation
@@ -242,6 +245,47 @@ func checkGolangImage(ctx context.Context) DoctorResult {
242245
return pass(nameGolangImage, fmt.Sprintf("Golang image %s is now available", imageName))
243246
}
244247

248+
// checkK3sImage verifies the rancher/k3s image used by TestK8sOperator is
249+
// available locally or can be pulled. Like the golang image, the public
250+
// rancher/k3s image is rate-limited on anonymous Docker Hub pulls, so this
251+
// check doubles as a reminder to configure Docker Hub credentials.
252+
func checkK3sImage(ctx context.Context) DoctorResult {
253+
cli, err := createDockerClient(ctx)
254+
if err != nil {
255+
return fail(nameK3sImage, "Cannot create Docker client for image check")
256+
}
257+
defer cli.Close()
258+
259+
imageName := k3sic.K3sImage
260+
261+
available, err := checkImageAvailableLocally(ctx, cli, imageName)
262+
if err != nil {
263+
return fail(
264+
nameK3sImage,
265+
fmt.Sprintf("Cannot check k3s image %s: %v", imageName, err),
266+
"Check Docker daemon status",
267+
"Try: docker images | grep rancher/k3s",
268+
)
269+
}
270+
271+
if available {
272+
return pass(nameK3sImage, fmt.Sprintf("K3s image %s is available locally", imageName))
273+
}
274+
275+
err = ensureImageAvailable(ctx, cli, imageName, false)
276+
if err != nil {
277+
return warn(
278+
nameK3sImage,
279+
fmt.Sprintf("K3s image %s not available locally and could not pull: %v", imageName, err),
280+
"Only TestK8sOperator needs this image; other tests are unaffected",
281+
"Configure Docker Hub credentials (docker login) to avoid rate limits",
282+
"Try: docker pull "+imageName,
283+
)
284+
}
285+
286+
return pass(nameK3sImage, fmt.Sprintf("K3s image %s is now available", imageName))
287+
}
288+
245289
// checkGoInstallation verifies Go is installed and working.
246290
func checkGoInstallation(ctx context.Context) DoctorResult {
247291
_, err := exec.LookPath("go")

0 commit comments

Comments
 (0)