From 69228811b41d192cf57f04599d527caec17d39a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=83=9C=ED=99=98=20=EB=B0=95?= Date: Sat, 1 Aug 2026 19:52:49 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(e2e):=20webhook=20=EC=A4=80=EB=B9=84=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=EC=9D=84=20endpoint=20=EC=A1=B4=EC=9E=AC=20?= =?UTF-8?q?=E2=86=92=20=EC=8B=A4=EC=A0=9C=20=ED=98=B8=EC=B6=9C=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=9C=BC=EB=A1=9C=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 직전 수정(rollout + endpoint 주소 확보 대기)이 불충분했다. 실측 타임라인: 10:36:24.219 STEP: waiting for webhook endpoint to be serving 10:36:24.788 cluster_recovery BeforeAll FAIL — connection refused 대기를 통과한 **0.5초 뒤** 첫 CR apply 가 죽었다. 원인은 판정 신호가 틀린 것: readiness probe 는 healthz(8081)만 보고 **webhook TLS 포트(9443)를 보지 않는다**. 따라서 Pod=Ready + endpoint 에 주소 존재 상태에서도 webhook 서버는 아직 안 듣고 있을 수 있다 (operator 재시작 0회 — 죽은 게 아니라 아직 안 뜬 것이었다). 간접 신호 대신 **실제로 webhook 을 호출**한다. `kubectl apply --dry-run=server` 는 admission 체인을 그대로 태우면서 아무것도 저장하지 않으므로 정확한 준비 신호다. 검증 거부는 "webhook 이 응답했다"는 뜻이라 성공으로 치고, `failed calling webhook` / `connection refused` / `context deadline exceeded` 만 재시도한다. verify: go vet -tags=e2e ./test/e2e/ → exit 0 / gofmt 출력 없음 (실효 검증은 머지 후 nightly 실측) Signed-off-by: 태환 박 Co-Authored-By: Claude Opus 5 --- test/e2e/e2e_suite_test.go | 39 +++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) 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() { From 176d7ebd42b397132b5f5c8e5a189621bc669960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=83=9C=ED=99=98=20=EB=B0=95?= Date: Sat, 1 Aug 2026 20:17:54 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(e2e):=20=EC=8A=B9=EA=B8=89=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EB=A6=AC=ED=84=B0=EB=9F=B4=EC=9D=84=20=EC=83=81?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=ED=86=B5=EC=9D=BC=20(status=20=EB=8B=A8?= =?UTF-8?q?=EC=96=B8=20=EB=93=9C=EB=A6=AC=ED=94=84=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #317 에서 major 승급(9.0.4)을 patch 승급(8.1.7)으로 바꿨는데, e2e_test.go 의 **status 단언만 9.0.4 로 남아** 있었다: }, 2*time.Minute, 5*time.Second).Should(Equal("9.0.4")) patch 는 8.1.7 로 나가는데 status 가 9.0.4 가 되기를 기다리니 영원히 타임아웃한다 (실측: e2e_test.go:455, Timed out after 120s). #317 의 정적 게이트는 **patch 리터럴만** 검사해 이 단언을 못 봤다. 게이트를 하나 더 붙이는 대신 **중복 자체를 없앤다** — 승급 버전을 상수로 승격해 patch 본문·기대 이미지·status 단언이 같은 값을 공유하게 하면 어긋남이 구조적으로 불가능해진다. 형제 backup_restore_test.go 도 같은 리터럴 산포 상태라 함께 정리한다 (version_upgrade_test.go 는 이미 상수 사용). verify: go vet -tags=e2e ./test/e2e/ → exit 0 / gofmt 출력 없음 (실효 검증은 머지 후 nightly 실측) Signed-off-by: 태환 박 Co-Authored-By: Claude Opus 5 --- test/e2e/backup_restore_test.go | 15 +++++++++++---- test/e2e/e2e_test.go | 15 ++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) 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_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",