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] =?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() {