From 8830c0bc9d888e1680715e526cecbb89ab25ff9b Mon Sep 17 00:00:00 2001 From: Jean-Philippe Evrard Date: Tue, 9 Jun 2026 10:47:19 +0200 Subject: [PATCH 1/2] Add github actions linting Signed-off-by: Jean-Philippe Evrard --- .config/mise.toml | 1 + Makefile | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.config/mise.toml b/.config/mise.toml index d4b77b5..797a118 100644 --- a/.config/mise.toml +++ b/.config/mise.toml @@ -1,4 +1,5 @@ [tools] +actionlint = "latest" cosign = "3.0.6" go = "1.26.4" golangci-lint = "2.12.2" diff --git a/Makefile b/Makefile index 14a8ad3..c12aba5 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .DEFAULT: all -.PHONY: all clean install-tools dev-image release dev-manifest e2e-test minikube-publish test lint lint-docs +.PHONY: all clean install-tools dev-image release dev-manifest e2e-test minikube-publish test lint lint-go lint-sh lint-ghactions lint-docs DH_ORG ?= kubereboot IMAGE_NAME ?= $(DH_ORG)/kured @@ -52,9 +52,17 @@ test: lint @echo "Running short go tests" go test -test.short -json ./... > test.json -lint: +lint: lint-sh lint-ghactions lint-go + +lint-sh: @echo "Running shellcheck" find . -name '*.sh' | xargs -n1 shellcheck + +lint-ghactions: + @echo "Running actionlint" + actionlint -verbose + +lint-go: @echo "Running golangci-lint..." golangci-lint run --config $(GOLANGCI_CONFIG) ./... From 48999c3d867d93c13339d02b6d8095568d282f7f Mon Sep 17 00:00:00 2001 From: Jean-Philippe Evrard Date: Tue, 9 Jun 2026 10:49:49 +0200 Subject: [PATCH 2/2] tests: Cleanup the test matrix for readability Signed-off-by: Jean-Philippe Evrard --- .github/workflows/on-branch-push-and-prs.yaml | 43 +- tests/kind/main_test.go | 409 +++++++----------- 2 files changed, 174 insertions(+), 278 deletions(-) diff --git a/.github/workflows/on-branch-push-and-prs.yaml b/.github/workflows/on-branch-push-and-prs.yaml index bc226d2..49d360c 100644 --- a/.github/workflows/on-branch-push-and-prs.yaml +++ b/.github/workflows/on-branch-push-and-prs.yaml @@ -38,7 +38,7 @@ jobs: # It is only used to make us aware of any potential security failure that # should trigger a bump of the image in build/. pr-vuln-scan: - name: Build image and scan it against known vulnerabilities + name: Trivy scan # Builds image and scan it against known vulnerabilities runs-on: ubuntu-latest steps: - name: Harden Runner @@ -68,6 +68,29 @@ jobs: vuln-type: 'os,library' severity: 'CRITICAL,HIGH' + e2e-test-matrix: + name: Generate e2e test matrix + runs-on: ubuntu-latest + outputs: + testnames: ${{ steps.matrix.outputs.testnames }} + steps: + - name: Harden Runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - uses: jdx/mise-action@dba19683ed58901619b14f395a24841710cb4925 # v4.1.0 + with: + version: 2026.6.1 + + - name: Generate matrix + id: matrix + run: | + testnames=$(go test -list '^Test' ./tests/kind | jq -R -s -c 'split("\n") | map(select(startswith("Test")))') + echo "testnames=${testnames}" >> "${GITHUB_OUTPUT}" + # This ensures the latest code works with the manifests built from tree. # It is useful for two things: # - Test manifests changes (obviously), ensuring they don't break existing clusters @@ -76,23 +99,13 @@ jobs: # - Test some features are working as expected. # Helm charts are _trailing_ releases, while manifests are done during development. e2e-manifests: - name: End-to-End test with kured with code and manifests from HEAD + name: e2e #End-to-End test with kured with code and manifests from HEAD runs-on: ubuntu-latest + needs: e2e-test-matrix strategy: fail-fast: false matrix: - testname: - - "TestE2EWithCommand/current" - - "TestE2EWithCommand/previous" - - "TestE2EWithSignal/current" - - "TestE2EWithSignal/previous" - - "TestE2EConcurrentWithCommand/current" - - "TestE2EConcurrentWithCommand/previous" - - "TestE2EConcurrentWithSignal/current" - - "TestE2EConcurrentWithSignal/previous" - - "TestCordonningIsKept/concurrency1" - - "TestCordonningIsKept/concurrency2" - - "TestE2EBlocker/podblocker" + testname: ${{ fromJSON(needs.e2e-test-matrix.outputs.testnames) }} steps: - name: Harden Runner uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 @@ -109,4 +122,4 @@ jobs: uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Run specific e2e tests - run: make e2e-test ARGS="-run ^${{ matrix.testname }}" + run: make e2e-test ARGS="-run ^${{ matrix.testname }}$" diff --git a/tests/kind/main_test.go b/tests/kind/main_test.go index 1a1370a..4b85307 100644 --- a/tests/kind/main_test.go +++ b/tests/kind/main_test.go @@ -134,292 +134,175 @@ func (k *KindTest) Destroy() error { return nil } -func TestE2EWithCommand(t *testing.T) { +const ( + currentK8sClusterConfig = "../../.github/kind-cluster-current.yaml" + previousK8sClusterConfig = "../../.github/kind-cluster-previous.yaml" + rebootSentinelsScript = "testfiles/create-reboot-sentinels.sh" + coordinatedRebootScript = "testfiles/follow-coordinated-reboot.sh" +) + +type e2eScenario struct { + clusterNamePrefix string + clusterConfigFile string + manifest string + setupScript string + testScript string + failureMessage string +} + +func runE2E(t *testing.T, scenario e2eScenario) { + t.Helper() t.Parallel() + if testing.Short() { t.Skip("skipping test in short mode.") } - var kindClusterConfigs = []string{ - "previous", - "current", + randomInt := strconv.Itoa(rand.Intn(100)) + kindClusterName := fmt.Sprintf("%s-%s", scenario.clusterNamePrefix, randomInt) + kindContext := fmt.Sprintf("kind-%v", kindClusterName) + + k := NewKindTester(kindClusterName, scenario.clusterConfigFile, t, LocalImage(kuredDevImage), Deploy("../../kured-rbac.yaml"), Deploy(scenario.manifest)) + defer k.FlushLog() + + err := k.Create() + if err != nil { + t.Fatalf("Error creating cluster %v", err) } - // Iterate over each Kubernetes version - for _, version := range kindClusterConfigs { - version := version - // Define a subtest for each combination - t.Run(version, func(t *testing.T) { - t.Parallel() // Allow tests to run in parallel + defer func(k *KindTest) { + err := k.Destroy() + if err != nil { + t.Fatalf("Error destroying cluster %v", err) + } + }(k) - randomInt := strconv.Itoa(rand.Intn(100)) - kindClusterName := fmt.Sprintf("kured-e2e-command-%v-%v", version, randomInt) - kindClusterConfigFile := fmt.Sprintf("../../.github/kind-cluster-%v.yaml", version) - kindContext := fmt.Sprintf("kind-%v", kindClusterName) + k.Write([]byte("Now running e2e tests")) - k := NewKindTester(kindClusterName, kindClusterConfigFile, t, LocalImage(kuredDevImage), Deploy("../../kured-rbac.yaml"), Deploy("testfiles/kured-ds.yaml")) - defer k.FlushLog() + if scenario.setupScript != "" { + if err := k.RunCmd("bash", scenario.setupScript, kindContext); err != nil { + t.Fatalf("failed to run setup script %s: %v", scenario.setupScript, err) + } + } - err := k.Create() - if err != nil { - t.Fatalf("Error creating cluster %v", err) - } - defer func(k *KindTest) { - err := k.Destroy() - if err != nil { - t.Fatalf("Error destroying cluster %v", err) - } - }(k) - - k.Write([]byte("Now running e2e tests")) - - if err := k.RunCmd("bash", "testfiles/create-reboot-sentinels.sh", kindContext); err != nil { - t.Fatalf("failed to create sentinels: %v", err) - } - - if err := k.RunCmd("bash", "testfiles/follow-coordinated-reboot.sh", kindContext); err != nil { - t.Fatalf("failed to follow reboot: %v", err) - } - }) + if err := k.RunCmd("bash", scenario.testScript, kindContext); err != nil { + t.Fatalf("%s: %v", scenario.failureMessage, err) } } -func TestE2EWithSignal(t *testing.T) { - t.Parallel() - if testing.Short() { - t.Skip("skipping test in short mode.") - } - - var kindClusterConfigs = []string{ - "previous", - "current", - } - // Iterate over each Kubernetes version - for _, version := range kindClusterConfigs { - version := version - // Define a subtest for each combination - t.Run(version, func(t *testing.T) { - t.Parallel() // Allow tests to run in parallel - - randomInt := strconv.Itoa(rand.Intn(100)) - kindClusterName := fmt.Sprintf("kured-e2e-signal-%v-%v", version, randomInt) - kindClusterConfigFile := fmt.Sprintf("../../.github/kind-cluster-%v.yaml", version) - kindContext := fmt.Sprintf("kind-%v", kindClusterName) - - k := NewKindTester(kindClusterName, kindClusterConfigFile, t, LocalImage(kuredDevImage), Deploy("../../kured-rbac.yaml"), Deploy("testfiles/kured-ds-signal.yaml")) - defer k.FlushLog() - - err := k.Create() - if err != nil { - t.Fatalf("Error creating cluster %v", err) - } - defer func(k *KindTest) { - err := k.Destroy() - if err != nil { - t.Fatalf("Error destroying cluster %v", err) - } - }(k) - - k.Write([]byte("Now running e2e tests")) - - if err := k.RunCmd("bash", "testfiles/create-reboot-sentinels.sh", kindContext); err != nil { - t.Fatalf("failed to create sentinels: %v", err) - } - - if err := k.RunCmd("bash", "testfiles/follow-coordinated-reboot.sh", kindContext); err != nil { - t.Fatalf("failed to follow reboot: %v", err) - } - }) - } +func TestRebootCommandCurrentk8sRelease(t *testing.T) { + runE2E(t, e2eScenario{ + clusterNamePrefix: "kured-e2e-command-current", + clusterConfigFile: currentK8sClusterConfig, + manifest: "testfiles/kured-ds.yaml", + setupScript: rebootSentinelsScript, + testScript: coordinatedRebootScript, + failureMessage: "failed to follow reboot", + }) } -func TestE2EConcurrentWithCommand(t *testing.T) { - t.Parallel() - if testing.Short() { - t.Skip("skipping test in short mode.") - } - - var kindClusterConfigs = []string{ - "previous", - "current", - } - // Iterate over each Kubernetes version - for _, version := range kindClusterConfigs { - version := version - // Define a subtest for each combination - t.Run(version, func(t *testing.T) { - t.Parallel() // Allow tests to run in parallel - - randomInt := strconv.Itoa(rand.Intn(100)) - kindClusterName := fmt.Sprintf("kured-e2e-concurrentcommand-%v-%v", version, randomInt) - kindClusterConfigFile := fmt.Sprintf("../../.github/kind-cluster-%v.yaml", version) - kindContext := fmt.Sprintf("kind-%v", kindClusterName) - - k := NewKindTester(kindClusterName, kindClusterConfigFile, t, LocalImage(kuredDevImage), Deploy("../../kured-rbac.yaml"), Deploy("testfiles/kured-ds-concurrent-command.yaml")) - defer k.FlushLog() - - err := k.Create() - if err != nil { - t.Fatalf("Error creating cluster %v", err) - } - defer func(k *KindTest) { - err := k.Destroy() - if err != nil { - t.Fatalf("Error destroying cluster %v", err) - } - }(k) - - k.Write([]byte("Now running e2e tests")) - - if err := k.RunCmd("bash", "testfiles/create-reboot-sentinels.sh", kindContext); err != nil { - t.Fatalf("failed to create sentinels: %v", err) - } - - if err := k.RunCmd("bash", "testfiles/follow-coordinated-reboot.sh", kindContext); err != nil { - t.Fatalf("failed to follow reboot: %v", err) - } - }) - } +func TestRebootCommandPreviousk8sRelease(t *testing.T) { + runE2E(t, e2eScenario{ + clusterNamePrefix: "kured-e2e-command-previous", + clusterConfigFile: previousK8sClusterConfig, + manifest: "testfiles/kured-ds.yaml", + setupScript: rebootSentinelsScript, + testScript: coordinatedRebootScript, + failureMessage: "failed to follow reboot", + }) } -func TestE2EConcurrentWithSignal(t *testing.T) { - t.Parallel() - if testing.Short() { - t.Skip("skipping test in short mode.") - } - - var kindClusterConfigs = []string{ - "previous", - "current", - } - // Iterate over each Kubernetes version - for _, version := range kindClusterConfigs { - version := version - // Define a subtest for each combination - t.Run(version, func(t *testing.T) { - t.Parallel() // Allow tests to run in parallel - - randomInt := strconv.Itoa(rand.Intn(100)) - kindClusterName := fmt.Sprintf("kured-e2e-concurrentsignal-%v-%v", version, randomInt) - kindClusterConfigFile := fmt.Sprintf("../../.github/kind-cluster-%v.yaml", version) - kindContext := fmt.Sprintf("kind-%v", kindClusterName) - - k := NewKindTester(kindClusterName, kindClusterConfigFile, t, LocalImage(kuredDevImage), Deploy("../../kured-rbac.yaml"), Deploy("testfiles/kured-ds-concurrent-signal.yaml")) - defer k.FlushLog() - - err := k.Create() - if err != nil { - t.Fatalf("Error creating cluster %v", err) - } - defer func(k *KindTest) { - err := k.Destroy() - if err != nil { - t.Fatalf("Error destroying cluster %v", err) - } - }(k) - - k.Write([]byte("Now running e2e tests")) - - if err := k.RunCmd("bash", "testfiles/create-reboot-sentinels.sh", kindContext); err != nil { - t.Fatalf("failed to create sentinels: %v", err) - } - - if err := k.RunCmd("bash", "testfiles/follow-coordinated-reboot.sh", kindContext); err != nil { - t.Fatalf("failed to follow reboot: %v", err) - } - }) - } +func TestRebootSignalCurrentk8sRelease(t *testing.T) { + runE2E(t, e2eScenario{ + clusterNamePrefix: "kured-e2e-signal-current", + clusterConfigFile: currentK8sClusterConfig, + manifest: "testfiles/kured-ds-signal.yaml", + setupScript: rebootSentinelsScript, + testScript: coordinatedRebootScript, + failureMessage: "failed to follow reboot", + }) } -func TestCordonningIsKept(t *testing.T) { - t.Parallel() - if testing.Short() { - t.Skip("skipping test in short mode.") - } - - var kindClusterConfigs = []string{ - "concurrency1", - "concurrency2", - } - // Iterate over each test variant - for _, variant := range kindClusterConfigs { - variant := variant - // Define a subtest for each combination - t.Run(variant, func(t *testing.T) { - t.Parallel() // Allow tests to run in parallel - - randomInt := strconv.Itoa(rand.Intn(100)) - kindClusterName := fmt.Sprintf("kured-e2e-cordon-%v-%v", variant, randomInt) - kindClusterConfigFile := "../../.github/kind-cluster-current.yaml" - kindContext := fmt.Sprintf("kind-%v", kindClusterName) - - var manifest string - if variant == "concurrency1" { - manifest = "testfiles/kured-ds-signal.yaml" - } else { - manifest = "testfiles/kured-ds-concurrent-signal.yaml" - } - k := NewKindTester(kindClusterName, kindClusterConfigFile, t, LocalImage(kuredDevImage), Deploy("../../kured-rbac.yaml"), Deploy(manifest)) - defer k.FlushLog() - - err := k.Create() - if err != nil { - t.Fatalf("Error creating cluster %v", err) - } - defer func(k *KindTest) { - err := k.Destroy() - if err != nil { - t.Fatalf("Error destroying cluster %v", err) - } - }(k) - - k.Write([]byte("Now running e2e tests")) - - if err := k.RunCmd("bash", "testfiles/node-stays-as-cordonned.sh", kindContext); err != nil { - t.Fatalf("node did not reboot in time: %v", err) - } - }) - } +func TestRebootSignalPreviousk8sRelease(t *testing.T) { + runE2E(t, e2eScenario{ + clusterNamePrefix: "kured-e2e-signal-previous", + clusterConfigFile: previousK8sClusterConfig, + manifest: "testfiles/kured-ds-signal.yaml", + setupScript: rebootSentinelsScript, + testScript: coordinatedRebootScript, + failureMessage: "failed to follow reboot", + }) } -func TestE2EBlocker(t *testing.T) { - t.Parallel() - if testing.Short() { - t.Skip("skipping test in short mode.") - } - var kindClusterConfigs = []string{ - "podblocker", - } - // Iterate over each variant of the test - for _, variant := range kindClusterConfigs { - variant := variant - // Define a subtest for each combination - t.Run(variant, func(t *testing.T) { - t.Parallel() // Allow tests to run in parallel +func TestConcurrentRebootCommandCurrentk8sRelease(t *testing.T) { + runE2E(t, e2eScenario{ + clusterNamePrefix: "kured-e2e-concurrentcommand-current", + clusterConfigFile: currentK8sClusterConfig, + manifest: "testfiles/kured-ds-concurrent-command.yaml", + setupScript: rebootSentinelsScript, + testScript: coordinatedRebootScript, + failureMessage: "failed to follow reboot", + }) +} - randomInt := strconv.Itoa(rand.Intn(100)) - kindClusterName := fmt.Sprintf("kured-e2e-cordon-%v-%v", variant, randomInt) - kindClusterConfigFile := "../../.github/kind-cluster-current.yaml" - kindContext := fmt.Sprintf("kind-%v", kindClusterName) +func TestConcurrentRebootCommandPreviousk8sRelease(t *testing.T) { + runE2E(t, e2eScenario{ + clusterNamePrefix: "kured-e2e-concurrentcommand-previous", + clusterConfigFile: previousK8sClusterConfig, + manifest: "testfiles/kured-ds-concurrent-command.yaml", + setupScript: rebootSentinelsScript, + testScript: coordinatedRebootScript, + failureMessage: "failed to follow reboot", + }) +} - k := NewKindTester(kindClusterName, kindClusterConfigFile, t, LocalImage(kuredDevImage), Deploy("../../kured-rbac.yaml"), Deploy(fmt.Sprintf("testfiles/kured-ds-%v.yaml", variant))) - defer k.FlushLog() +func TestConcurrentRebootSignalCurrentk8sRelease(t *testing.T) { + runE2E(t, e2eScenario{ + clusterNamePrefix: "kured-e2e-concurrentsignal-current", + clusterConfigFile: currentK8sClusterConfig, + manifest: "testfiles/kured-ds-concurrent-signal.yaml", + setupScript: rebootSentinelsScript, + testScript: coordinatedRebootScript, + failureMessage: "failed to follow reboot", + }) +} - err := k.Create() - if err != nil { - t.Fatalf("Error creating cluster %v", err) - } - defer func(k *KindTest) { - err := k.Destroy() - if err != nil { - t.Fatalf("Error destroying cluster %v", err) - } - }(k) +func TestConcurrentRebootSignalPreviousk8sRelease(t *testing.T) { + runE2E(t, e2eScenario{ + clusterNamePrefix: "kured-e2e-concurrentsignal-previous", + clusterConfigFile: previousK8sClusterConfig, + manifest: "testfiles/kured-ds-concurrent-signal.yaml", + setupScript: rebootSentinelsScript, + testScript: coordinatedRebootScript, + failureMessage: "failed to follow reboot", + }) +} - k.Write([]byte("Now running e2e tests")) +func TestCordonningIsKeptWithoutConcurrency(t *testing.T) { + runE2E(t, e2eScenario{ + clusterNamePrefix: "kured-e2e-cordon-without-concurrency", + clusterConfigFile: currentK8sClusterConfig, + manifest: "testfiles/kured-ds-signal.yaml", + testScript: "testfiles/node-stays-as-cordonned.sh", + failureMessage: "node did not reboot in time", + }) +} - if err := k.RunCmd("bash", fmt.Sprintf("testfiles/%v.sh", variant), kindContext); err != nil { - t.Fatalf("node blocker test did not succeed: %v", err) - } - }) - } +func TestCordonningIsKeptWithConcurrency(t *testing.T) { + runE2E(t, e2eScenario{ + clusterNamePrefix: "kured-e2e-cordon-with-concurrency", + clusterConfigFile: currentK8sClusterConfig, + manifest: "testfiles/kured-ds-concurrent-signal.yaml", + testScript: "testfiles/node-stays-as-cordonned.sh", + failureMessage: "node did not reboot in time", + }) +} + +func TestRebootBlockedPodblocker(t *testing.T) { + runE2E(t, e2eScenario{ + clusterNamePrefix: "kured-e2e-cordon-podblocker", + clusterConfigFile: currentK8sClusterConfig, + manifest: "testfiles/kured-ds-podblocker.yaml", + testScript: "testfiles/podblocker.sh", + failureMessage: "node blocker test did not succeed", + }) }