diff --git a/test/e2e/backup_restore_test.go b/test/e2e/backup_restore_test.go index 3996f22..7c6b8e8 100644 --- a/test/e2e/backup_restore_test.go +++ b/test/e2e/backup_restore_test.go @@ -29,6 +29,13 @@ const ( brValkey = "vk-br-test" brBackup = "vk-br-backup-1" brRestore = "vk-br-restore-1" + + // 승급 대상 버전은 상수 하나로 둔다 — patch 본문과 이미지·CR 단언이 각자 + // 리터럴을 들고 있으면 한 곳만 고쳤을 때 나머지가 조용히 어긋난다 + // (형제 e2e_test.go 에서 실제로 그렇게 어긋나 status 단언이 타임아웃했다). + // major 승급은 webhook 이 금지하므로 patch 승급만 쓴다. + brUpgradeVersion = "8.1.7" + brUpgradeImage = "docker.io/valkey/valkey:" + brUpgradeVersion ) var _ = Describe("ValkeyBackup + ValkeyRestore (Standalone PVC)", Ordered, func() { @@ -196,7 +203,7 @@ spec: // version patch 시 STS image propagate + Pod rotation + RDB 호환성 회귀 가드. Context("Restored 인스턴스의 8.1.6 → 8.1.7 version patch chain (V2)", func() { It("spec.version.version 8.1.6 → 8.1.7 patch (restored 후)", func() { - patch := `{"spec":{"version":{"version":"8.1.7","image":"docker.io/valkey/valkey"}}}` + patch := fmt.Sprintf(`{"spec":{"version":{"version":%q,"image":"docker.io/valkey/valkey"}}}`, brUpgradeVersion) _, err := utils.Run(exec.Command("kubectl", "patch", "valkey", brValkey, "-n", brNamespace, "--type=merge", "-p", patch)) @@ -210,7 +217,7 @@ spec: "-o", "jsonpath={.spec.template.spec.containers[0].image}")) return out }, 60*time.Second, 5*time.Second).Should( - Equal("docker.io/valkey/valkey:8.1.7"), + Equal(brUpgradeImage), "restored 인스턴스의 STS image 가 8.1.7 로 propagate (가설 A)") }) @@ -221,7 +228,7 @@ spec: "-o", "jsonpath={.spec.containers[0].image}")) return out }, 3*time.Minute, 10*time.Second).Should( - Equal("docker.io/valkey/valkey:8.1.7"), + Equal(brUpgradeImage), "restored 인스턴스의 Pod 가 8.1.7 로 재생성 (가설 C)") }) @@ -232,7 +239,7 @@ spec: "-o", "jsonpath={.spec.version.version}")) return out }, 30*time.Second, 5*time.Second).Should( - Equal("8.1.7"), + Equal(brUpgradeVersion), "webhook defaulter 가 8.1.7 → 8.1.6 reverting 안함 (가설 B)") }) diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 5780c52..91b59c1 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -90,16 +90,41 @@ var _ = BeforeSuite(func() { "deploy/valkey-operator-controller-manager", "--timeout=180s")) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "controller-manager rollout") - By("waiting for webhook endpoint to be serving") + // endpoint 에 주소가 잡혔다는 것만으로는 부족하다 — readiness probe 는 healthz(8081) + // 를 볼 뿐 webhook TLS 포트(9443)를 보지 않으므로, Pod 가 Ready 이고 endpoint 에 + // 주소가 있어도 webhook 서버가 아직 안 듣고 있을 수 있다. 실측: endpoint 대기 통과 + // **0.5초 뒤** 첫 CR apply 가 `connection refused` 로 죽었다. + // + // 그래서 **실제로 webhook 을 호출해** 응답하는지 본다. server-side dry-run 은 + // admission 체인을 그대로 태우면서 아무것도 저장하지 않으므로 정확한 신호다. + // 검증 거부(4xx)는 "webhook 이 응답했다"는 뜻이라 성공으로 친다 — 연결 실패만 재시도. + By("waiting for the webhook to actually answer (server-side dry-run)") + probe := `apiVersion: cache.keiailab.io/v1alpha1 +kind: Valkey +metadata: + name: webhook-readiness-probe + namespace: default +spec: + mode: Standalone + replicas: 1 +` EventuallyWithOffset(1, func() string { - out, err := utils.Run(exec.Command("kubectl", "-n", "valkey-operator-system", "get", - "endpoints", "valkey-operator-webhook-service", - "-o", "jsonpath={.subsets[0].addresses[0].ip}")) - if err != nil { + cmd := exec.Command("kubectl", "apply", "--dry-run=server", "-f", "-") + cmd.Stdin = strings.NewReader(probe) + out, err := utils.Run(cmd) + if err == nil { return "" } - return strings.TrimSpace(out) - }, 2*time.Minute, 3*time.Second).ShouldNot(BeEmpty(), "webhook endpoint never became ready") + msg := out + err.Error() + // webhook 에 닿지 못한 경우만 재시도 대상. + if strings.Contains(msg, "failed calling webhook") || + strings.Contains(msg, "connection refused") || + strings.Contains(msg, "context deadline exceeded") { + return msg + } + // 그 외(검증 거부 등)는 webhook 이 살아서 응답한 것 — 준비 완료로 본다. + return "" + }, 3*time.Minute, 3*time.Second).Should(BeEmpty(), "webhook never became reachable") }) var _ = AfterSuite(func() { diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index d541eac..2ba2114 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -328,8 +328,13 @@ var _ = Describe("Manager", Ordered, func() { const ( upgradeNamespace = "test-valkey-upgrade-20260507" upgradeName = "vk-upgrade-test" - oldImage = "docker.io/valkey/valkey:8.1.6" - newImage = "docker.io/valkey/valkey:8.1.7" + // 승급 대상 버전을 상수 하나로 둔다 — patch 본문·기대 이미지·status 단언이 + // 각자 리터럴을 들고 있으면 한 곳만 고쳤을 때 나머지가 조용히 어긋난다 + // (실제로 그렇게 어긋나 status 단언이 옛 9.0.4 를 기다리며 타임아웃했다). + oldVersion = "8.1.6" + newVersion = "8.1.7" + oldImage = "docker.io/valkey/valkey:" + oldVersion + newImage = "docker.io/valkey/valkey:" + newVersion ) By("creating a dedicated namespace for upgrade test data") @@ -409,11 +414,11 @@ spec: // 정당하게 거부한다 — "manual major version upgrade is prohibited; // AutoUpdate automates patch/minor only". spec 의 목적(version patch 가 STS // template image 로 전파되고 Pod 가 롤링되는지)은 patch 승급으로도 동일 달성. - By("patching spec.version.version to 8.1.7") + By("patching spec.version.version to " + newVersion) cmd = exec.Command("kubectl", "patch", "valkey", upgradeName, "-n", upgradeNamespace, "--type=merge", - "-p", `{"spec":{"version":{"version":"8.1.7"}}}`) + "-p", fmt.Sprintf(`{"spec":{"version":{"version":%q}}}`, newVersion)) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred()) @@ -452,7 +457,7 @@ spec: upgradeName, "-n", upgradeNamespace, "-o", "jsonpath={.status.version}")) return out - }, 2*time.Minute, 5*time.Second).Should(Equal("9.0.4")) + }, 2*time.Minute, 5*time.Second).Should(Equal(newVersion)) By("cleaning up upgrade test data before the operator is undeployed") _, _ = utils.Run(exec.Command("kubectl", "delete", "valkey",