From bb0acc68c47d9f05d771e98b0a258e2ccaecc684 Mon Sep 17 00:00:00 2001 From: phil Date: Thu, 30 Apr 2026 11:57:38 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat(controller):=20Status.Conditions=20rea?= =?UTF-8?q?son=20=EC=96=B4=ED=9C=98=20=ED=99=95=EC=9E=A5=20(P0-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PGO Reason SOT 패턴 차용. P2-T3 failover controller가 phase 전이를 노출할 표준 reason 6종 추가: - ReasonPromoting / ReasonDemoting (replica↔primary 전환) - ReasonElectionWon / ReasonElectionLost (lease holder 변화) - ReasonTopologyDrift (P11-M1 pg_dist_node drift) - ReasonRotating (P7 secret/credential 회전) ADR 0005 §변경 정책 "추가만 허용" 부합. 시그니처 변경 0, 기존 호출자 (refreshStatus의 6개 site) 무영향. P2-T3 implementation 시 자연스럽게 활용. 검증: - make lint: 0 issues - make test: internal/controller 81.0% 커버리지 유지 (회귀 0) Refs: 권장 P0-1 (plan 1-https-artifacthub-io-packages-helm-bit-sunny-wozniak.md) Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/controller/status.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/internal/controller/status.go b/internal/controller/status.go index 509723ed..b197d1eb 100644 --- a/internal/controller/status.go +++ b/internal/controller/status.go @@ -27,6 +27,19 @@ import ( // // Condition Reason은 본 파일의 상수 집합으로 통일한다. 새 reason 추가는 본 // 파일에 추가하는 것이 단일 출처(SOT) 규약이다. +// +// Reason 카탈로그 (사용 영역): +// - 일반 lifecycle: Reconciling / Available / Progressing / NotApplicable +// - 입력 검증: VersionRejected / ResourcesCreated +// - HA / Failover (P2-T3 이후 사용): +// Promoting — replica → primary 전환 중 +// Demoting — primary → replica 강등 중 +// ElectionWon — election lease holder 획득 +// ElectionLost — election lease holder 상실(다른 후보로 전환) +// - Citus topology (P11-M1 이후 사용): +// TopologyDrift — pg_dist_node ↔ desired 사이 drift 검출 +// - Auth / 인증 (P7 이후 사용): +// Rotating — Secret/credential 회전 진행 중 const ( // Condition types @@ -36,13 +49,25 @@ const ( ConditionRoutersReady = "RoutersReady" ConditionMetadataInSync = "MetadataInSync" - // Reasons + // Reasons — 일반 lifecycle ReasonReconciling = "Reconciling" ReasonResourcesCreated = "ResourcesCreated" ReasonVersionRejected = "VersionRejected" ReasonAvailable = "Available" ReasonProgressing = "Progressing" ReasonNotApplicable = "NotApplicable" + + // Reasons — HA / Failover (P2-T3 이후 활성) + ReasonPromoting = "Promoting" + ReasonDemoting = "Demoting" + ReasonElectionWon = "ElectionWon" + ReasonElectionLost = "ElectionLost" + + // Reasons — Citus topology (P11-M1 이후 활성) + ReasonTopologyDrift = "TopologyDrift" + + // Reasons — Auth / 인증 (P7 이후 활성) + ReasonRotating = "Rotating" ) // setCondition은 지정된 type/status/reason/message로 Condition을 추가/갱신한다. From b719a6ec42574d2bf6d6d2a08058a7eeefae0c51 Mon Sep 17 00:00:00 2001 From: phil Date: Thu, 30 Apr 2026 11:57:38 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat(plugin):=20AuthPlugin.RotateSecret=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(P0-5,=20additive)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0005 §변경 정책 "alpha 단계 추가 메서드(non-breaking) 허용"에 부합한 비파괴 추가. 시그니처: RotateSecret(ctx, target, oldRef *corev1.SecretReference) (newRef *corev1.SecretReference, err error) 의미론: - oldRef == nil: 초기 생성 (첫 부트스트랩) - oldRef != nil: 회전 (caller가 oldRef cleanup 책임) dummyAuth(api_test.go)에 1줄 추가로 var _ AuthPlugin = (*dummyAuth)(nil) 컴파일 가드 유지. 첫 구현은 Pillar P7(Security/TLS) reconciler가 SCRAM-SHA-256 회전을 위해 추가 예정. 검증: - make lint: 0 issues - make test: internal/plugin 93.0% 커버리지 유지 - TestRegistry_RegisterAndLookup PASS (회귀 0) Refs: 권장 P0-5 (plan 1-https-artifacthub-io-packages-helm-bit-sunny-wozniak.md) Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/plugin/api.go | 18 ++++++++++++++++++ internal/plugin/api_test.go | 3 +++ 2 files changed, 21 insertions(+) diff --git a/internal/plugin/api.go b/internal/plugin/api.go index 351b5bb7..743e4657 100644 --- a/internal/plugin/api.go +++ b/internal/plugin/api.go @@ -213,4 +213,22 @@ type AuthPlugin interface { // 반환한다. webhook이 사용자가 작성한 Secret을 검증할 때 사용한다. // 빈 슬라이스는 "Secret 불요" 의미. SecretSchemaJSON() ([]byte, error) + + // RotateSecret은 oldRef의 인증 자격(password/cert/token 등)을 새로 발급하여 + // newRef로 반환한다. 운영 자동화(Bitnami 의 update-password CronJob 패턴을 + // operator 내재화)의 SDK 토대다. + // + // 의미론: + // - oldRef == nil: 초기 생성(첫 부트스트랩). plugin이 새 Secret을 만들고 + // newRef로 가리킨다. + // - oldRef != nil: 회전. plugin이 새 자격을 발급하고 *기존 oldRef는 caller + // 가 책임지고 cleanup* 한다(즉시 삭제 / grace period 보존 등 정책은 본 + // SDK 외부의 P7 reconciler 결정). + // + // 멱등성은 plugin의 책임이다. 본 인터페이스는 단일 호출만 보장한다. + // + // 본 메서드는 ADR 0005 §변경 정책의 "alpha 단계 추가 메서드(non-breaking) 허용" + // 에 부합하도록 후속 추가됐다(P0-5 권장, 2026-04-30). 첫 구현은 P7 reconciler + // 가 SCRAM-SHA-256 회전을 위해 추가하며, 인터페이스는 본 시점에 동결된다. + RotateSecret(ctx context.Context, target ClusterTarget, oldRef *corev1.SecretReference) (newRef *corev1.SecretReference, err error) } diff --git a/internal/plugin/api_test.go b/internal/plugin/api_test.go index b8f893cc..4c25ece3 100644 --- a/internal/plugin/api_test.go +++ b/internal/plugin/api_test.go @@ -57,6 +57,9 @@ type dummyAuth struct{ name string } func (d *dummyAuth) Name() string { return d.name } func (d *dummyAuth) Configure(_ context.Context, _ ClusterTarget) error { return nil } func (d *dummyAuth) SecretSchemaJSON() ([]byte, error) { return nil, nil } +func (d *dummyAuth) RotateSecret(_ context.Context, _ ClusterTarget, _ *corev1.SecretReference) (*corev1.SecretReference, error) { + return nil, nil +} // ---------------------------------------------------------------------------- // 인터페이스 만족 컴파일 가드 (interface satisfaction guards) From 62499844af9c976010ed2d6a3e9dfb0164056dcd Mon Sep 17 00:00:00 2001 From: phil Date: Thu, 30 Apr 2026 11:57:38 +0900 Subject: [PATCH 3/5] =?UTF-8?q?docs(plan):=2019=20=EA=B6=8C=EC=9E=A5=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20+=20ADR=200006~0008=20=EC=8B=A0=EC=84=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bitnami PostgreSQL Helm Chart + Crunchy PGO v5.8.7과의 교차검증 결과를 기반으로 19개 품질 개선 권장사항을 P0(6) + P1(6) + P2(7)로 분해 등록. 거버넌스 산출물: - TASKS.md: "품질 개선 plan" 섹션 추가, 19 ID를 F01~F14 Pillar에 매핑. P0-1, P0-5는 본 세션 PR에서 완료 마킹. - docs/roadmap.md: P14 Pillar에서 Helm chart을 분리하여 P1 트랙으로 이동 (ADR 0007 참조). P14에는 install.yaml/OLM/multi-arch만 잔존. - docs/adr/0006-security-defaults-rationale.md: 데이터플레인 PodSecurityContext defaults 결정 (P0-2 트리거). Bitnami 패턴 차용 (runAsUser=70, readOnlyRootFs, seccomp RuntimeDefault, capabilities drop). - docs/adr/0007-helm-chart-promoted-to-p1.md: Helm chart을 P14에서 P1로 앞당김 (P1-4 트리거). alpha 사용자 채널 조기 확보. - docs/adr/0008-finalizer-avoidance-policy.md: Finalizer 회피 정책 명문화 (P0-4 트리거). ControllerReference + K8s GC만 사용, 외부 자원은 별도 Job CRD로 처리. ADR 0002 "K8s API as DCS" 원칙 보존. 후속 작업: 17개 권장 (P0-2/3/4/6, P1-1~6, P2-1~7)은 별도 PR. Refs: /Users/phil/.claude/plans/1-https-artifacthub-io-packages-helm-bit-sunny-wozniak.md Co-Authored-By: Claude Opus 4.7 (1M context) --- TASKS.md | 60 +++++++++++++++ docs/adr/0006-security-defaults-rationale.md | 73 +++++++++++++++++++ docs/adr/0007-helm-chart-promoted-to-p1.md | 77 ++++++++++++++++++++ docs/adr/0008-finalizer-avoidance-policy.md | 74 +++++++++++++++++++ docs/roadmap.md | 2 +- 5 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0006-security-defaults-rationale.md create mode 100644 docs/adr/0007-helm-chart-promoted-to-p1.md create mode 100644 docs/adr/0008-finalizer-avoidance-policy.md diff --git a/TASKS.md b/TASKS.md index 351aea97..e410cf8c 100644 --- a/TASKS.md +++ b/TASKS.md @@ -40,3 +40,63 @@ 4. F07 P5-M1 — PgBouncer 사이드카 + 독립 Deployment. 5. F08 P6-M1 — exporter + Grafana + PrometheusRule. 6. F02 P10-M1 — extension lifecycle e2e. + +--- + +## 품질 개선 plan (2026-04-30 — Bitnami + Crunchy PGO 교차검증) + +> 출처: `/Users/phil/.claude/plans/1-https-artifacthub-io-packages-helm-bit-sunny-wozniak.md` (사용자 승인 plan) +> 19 권장사항을 P0(즉시) / P1(중기) / P2(장기) 우선순위로 분해. +> *영향 Pillar* 컬럼은 기존 F01~F14에 매핑. + +### P0 (1-2 sprint, 즉시) — 6개 + +| ID | 권장 | 영향 Pillar | 단계 | 의존 | 비고 | +|----|------|------------|------|------|------| +| P0-1 | Status.Conditions reason 어휘 확장 (Promoting/Demoting/Election*/TopologyDrift/Rotating) | F04(P2), F03(P11), F09(P7) | **완료** | — | `internal/controller/status.go` 본 PR. P2-T3 신호 채널. | +| P0-2 | 데이터플레인 PodSecurityContext defaults (runAsUser=70, readOnlyRootFs, seccomp RuntimeDefault) | F01(P1) | 설계 | — | ADR 0006. `builders.go:184-198, 243-256`. | +| P0-3 | NetworkPolicy 데이터플레인 표준 템플릿 (coordinator↔workers, router→coordinator/workers) | F01(P1), F09(P7) | 설계 | P0-2 | RFC 0006 §NetworkPolicy. `config/network-policy/`. | +| P0-4 | Cascade Delete 회귀 테스트 (Finalizer 회피 정책) | F01(P1) | 설계 | — | ADR 0008. `test/e2e/cascade_delete_test.go`. | +| P0-5 | AuthPlugin.RotateSecret 인터페이스 추가 (additive, ADR 0005 §alpha rule) | F13(P13), F09(P7) | **완료** | — | `internal/plugin/api.go:205-` 본 PR. RFC 0006 §Auth Rotation Hook. | +| P0-6 | LibPQExecutor 구현 (Citus 차별화 코드 차원 잠금, P2 → P0 승격) | F03(P11) | 설계 | P0-1 | RFC 0002 Draft → Implemented. `internal/citus/exec.go`. | + +### P1 (3-6 sprint, 중기) — 6개 + +| ID | 권장 | 영향 Pillar | 단계 | 의존 | +|----|------|------------|------|------| +| P1-1 | BackupJob CRD + reconciler (BackupPlugin 첫 호출자) | F06(P4) | 설계 | P0-2 | +| P1-2 | PgBouncer 사이드카 + cmd/router 통합 | F07(P5), F12(P12) | 설계 | P0-2 | +| P1-3 | Monitoring/Exporter 표준 통합 (ExporterPlugin 호출자) | F08(P6) | 설계 | P0-2 | +| P1-4 | Helm chart 패키징 (P14 → P1 앞당김, ADR 0007) | **신규 P1 트랙** (F14에서 분리) | 설계 | — | +| P1-5 | ClusterUpgrade CRD 시그니처 (in-place + blue/green) | F11(P9) | 설계 | P1-1 | +| P1-6 | pgBackRest 실행 모델 (BackupOptions.ExecutionMode: sidecar\|job) | F06(P4), F13(P13) | 설계 | P1-1 | + +### P2 (6+ sprint, 장기 차별화) — 7개 + +| ID | 권장 | 영향 Pillar | 단계 | 의존 | +|----|------|------------|------|------| +| P2-1 | Citus rebalance / RebalanceJob CRD | F03(P11) | 설계 | P0-6 | +| P2-2 | Worker pool zero-downtime scale | F03(P11) | 설계 | P0-6, P2-1 | +| P2-3 | Plugin SDK wire-format golden test (reflect 기반 시그니처 hash) | F13(P13) | 설계 | — | +| P2-4 | gRPC out-of-process plugin + reference plugin (UDS, cosign) | F13(P13) | 설계 | P2-3, P0-2 | +| P2-5 | Declarative PgDatabase / PgRole CRD (PGO 미지원 차별화) | F10(P8), F03(P11) | 설계 | P0-5, P0-6 | +| P2-6 | Multi-region Standby Cluster (PGO Standby 차용 + Citus geo) | F14(P14)→독립 | 설계 | P1-1, P1-6 | +| P2-7 | Citus PGUpgrade orchestration | F11(P9), F03(P11) | 설계 | P1-5, P1-1, P2-6 | + +### 거버넌스 산출물 매트릭스 + +| ID | 제목 | 트리거 권장 | 시작 상태 | +|----|------|-----------|----------| +| RFC 0002 | metadata-sync (기존) | P0-6 | Draft → **Implemented 예정** | +| RFC 0004 | Backup/PITR | P1-1, P1-6 | Draft (작성 예정) | +| RFC 0005 | QueryRouter | P1-2 | Draft (작성 예정) | +| RFC 0006 | Security/TLS (NetworkPolicy + Auth Rotation + Role/RBAC) | P0-3, P0-5, P2-5 | Draft (작성 예정) | +| RFC 0007 | Observability | P1-3 | Draft (작성 예정) | +| RFC 0008 | DistributedTable 의미론 | P2-1 | Draft (작성 예정) | +| RFC 0010 | Upgrade (Citus 절 포함) | P1-5, P2-2, P2-7 | Draft (작성 예정) | +| RFC 0012 | Plugin SDK 안정화 | P2-3, P2-4 | Draft (작성 예정) | +| RFC 0013 | Declarative DB/Role | P2-5 | Draft (작성 예정) | +| RFC 0014 | Multi-region & Standby | P2-6 | Draft (작성 예정) | +| ADR 0006 | Security Defaults Rationale | P0-2 | **Accepted (본 PR)** | +| ADR 0007 | Helm을 P14에서 P1로 분리 | P1-4 | **Accepted (본 PR)** | +| ADR 0008 | Finalizer 회피 정책 | P0-4 | **Accepted (본 PR)** | diff --git a/docs/adr/0006-security-defaults-rationale.md b/docs/adr/0006-security-defaults-rationale.md new file mode 100644 index 00000000..b6310b99 --- /dev/null +++ b/docs/adr/0006-security-defaults-rationale.md @@ -0,0 +1,73 @@ +# ADR 0006 — 데이터플레인 PodSecurityContext 기본값 (Security Defaults Rationale) + +- **상태**: Accepted +- **날짜**: 2026-04-30 +- **결정자**: @keiailab/maintainers +- **관련**: ADR 0001 v2 (PGO-class 패리티), Bitnami PostgreSQL Helm Chart 비교 (`/Users/phil/.claude/plans/1-https-artifacthub-io-packages-helm-bit-sunny-wozniak.md` §4 P0-2) + +## 컨텍스트 + +본 프로젝트의 *manager Pod*(operator 자체)은 `config/manager/manager.yaml:53-74`에서 강한 SecurityContext를 적용한다 — `runAsNonRoot=true`, `readOnlyRootFilesystem=true`, `seccompProfile=RuntimeDefault`, `capabilities.drop=[ALL]`. 그러나 *데이터플레인 Pod*(`buildPGStatefulSet:184-198`, `buildRouterDeployment:243-256`)에는 SecurityContext가 0개다. + +이는 **비대칭 보안 부채**: +- PSS(Pod Security Standards) `restricted` 정책이 적용된 클러스터에서 admission 거부 가능 +- `runAsNonRoot=false`(기본) 상태에서 PG 컨테이너가 root로 기동될 수 있음 — 호스트 escape 위험 +- Bitnami PostgreSQL Helm Chart가 *기본값*으로 제공하는 보안 설정에 미달 → "PGO-class 패리티" 약속과 모순 + +## 결정 + +`buildPGStatefulSet`과 `buildRouterDeployment`가 생성하는 모든 데이터플레인 Pod에 다음 SecurityContext 기본값을 *항상* 적용한다: + +```yaml +podSecurityContext: + runAsNonRoot: true + runAsUser: 70 # PG 표준 postgres user UID + runAsGroup: 70 + fsGroup: 70 + seccompProfile: + type: RuntimeDefault +container.securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: [ALL] +``` + +`readOnlyRootFilesystem=true` 동반 변경: +- emptyDir 마운트 추가 — `/tmp`, `/run`, `/var/run/postgresql` (PG가 lock/socket 작성 필요) +- `/var/lib/postgresql/data`는 PVC 마운트라 별도 emptyDir 불필요 + +## 근거 + +### 왜 UID 70인가 +PostgreSQL 공식 컨테이너 이미지(postgres:*)가 `postgres` user를 UID/GID 70으로 정의. 본 프로젝트의 향후 cmd/instance 이미지도 동일 규약 따름. + +### 왜 `readOnlyRootFilesystem=true`인가 +- 컨테이너 내 임의 바이너리 작성 차단 → 공급망 공격 완화 +- Bitnami chart 기본값 +- emptyDir 마운트 비용은 미미(memory backed) → 트레이드오프 ↑ + +### 왜 *기본값*으로 강제인가 +PostgresCluster CR에 `Spec.SecurityContext` override 필드를 두면 *opt-in 보안*이 됨 — 운영자가 잊으면 root 가능 상태. 본 ADR은 *opt-out*으로 강제: 기본값은 항상 위 설정, override는 webhook이 검증. + +## 트레이드오프 + +- **`readOnlyRootFilesystem` 호환성**: 일부 PG extension(예: pg_cron, pg_stat_statements)이 디스크 임시 파일 생성. 해결: `/tmp` emptyDir + extension 별 PVC subpath. 본 ADR은 기본 emptyDir 3개로 충분, extension별 추가는 P10에서 처리. +- **사용자 정의 UID 요구**: 일부 K8s 환경(OpenShift 일부 SCC)이 random UID 강제. 해결: webhook이 `runAsUser`를 nil로 두면 K8s SCC가 채우도록 허용 (P0-2 implementation 시 webhook 추가). +- **기존 PVC 데이터의 ownership**: 기존 PG가 root로 쓴 데이터를 UID 70으로 읽으려면 fsGroup 변경. K8s `fsGroup`이 자동 처리하나 첫 transition은 시간 걸릴 수 있음. + +## 결과 + +- `internal/controller/builders.go`의 `buildPGStatefulSet`과 `buildRouterDeployment`에 SecurityContext 주입 (P0-2 권장 적용 시). +- envtest assertion: 생성된 Pod에 `runAsNonRoot=true`, `runAsUser=70` 확인. +- e2e 회귀: restricted PSA 적용 namespace에서 admission 통과 검증. +- 본 ADR 변경(UID 변경, capabilities 추가)은 RFC 0006 "Security/TLS"의 일부로 처리. + +## 강제 메커니즘 + +| 메커니즘 | 위치 | 도입 시점 | +|---|---|---| +| 기본값 주입 | `internal/controller/builders.go` | P0-2 implementation | +| webhook 검증 | `internal/webhook/v1alpha1/postgrescluster_webhook.go` | P0-2 후속 (override 시 최소값 강제) | +| envtest 회귀 | `internal/controller/builders_test.go` | P0-2 implementation | +| e2e 회귀 | `test/e2e/security_test.go` (신규) | P0-2 implementation | diff --git a/docs/adr/0007-helm-chart-promoted-to-p1.md b/docs/adr/0007-helm-chart-promoted-to-p1.md new file mode 100644 index 00000000..f5f55110 --- /dev/null +++ b/docs/adr/0007-helm-chart-promoted-to-p1.md @@ -0,0 +1,77 @@ +# ADR 0007 — Helm chart을 P14에서 P1 트랙으로 분리 + +- **상태**: Accepted +- **날짜**: 2026-04-30 +- **결정자**: @keiailab/maintainers +- **관련**: roadmap.md (14 Pillar), Bitnami PostgreSQL Helm Chart 비교 (`/Users/phil/.claude/plans/1-https-artifacthub-io-packages-helm-bit-sunny-wozniak.md` §5 P1-4) + +## 컨텍스트 + +`docs/roadmap.md`는 Helm chart을 **P14 Distribution**에 배치 — install.yaml, OLM bundle, multi-arch image와 묶음. P14는 *v1.0 GA 마지막 단계*로 정의되어, 다른 모든 Pillar(P1~P13) M3 통과 후 진입. + +문제: alpha/beta 단계에서 사용자가 operator를 *어떻게 설치*하는가? 현재는 Kustomize manifests만 제공 (`config/default`). 실제 사용자 채널은 *Helm chart가 사실상 표준*이라: + +- alpha 단계 사용자가 시도조차 못 함 → feedback loop 차단 +- "PGO-class" 패리티 약속에 *배포 채널 패리티*도 포함되나 P14까지 부재 +- Bitnami는 chart가 *유일한 채널*이며 매우 mature — 같은 시장 사용자에게 "Kustomize 외 없음"은 진입 마찰 + +## 결정 + +Helm chart을 *P14에서 분리*하여 P1 Core Lifecycle 트랙의 후속 task(P1-T5)로 재배치한다. P14에는 *나머지* distribution 산출물만 남긴다: + +| 변경 전 (P14) | 변경 후 | +|---|---| +| Helm chart | **P1 트랙으로 이동** (alpha 사용자 채널) | +| install.yaml | P14 유지 | +| OLM bundle | P14 유지 | +| multi-arch image | P14 유지 | + +### chart 분리 모델 + +`charts/` 아래 두 chart 별도 패키징: + +- `charts/postgresql-operator/` — operator 자체 (Deployment + RBAC + CRD + NetworkPolicy + ServiceAccount) +- `charts/postgrescluster/` — PostgresCluster CR 인스턴스 (선택, P1-T5 sub-task) + +이는 Bitnami의 `postgresql` (CR 인스턴스) + 별도 operator chart 패턴의 *역배치* — operator chart가 *상위*, CR chart가 *옵션*. 사유: 사용자가 operator는 한 번 설치, CR은 namespace당 N개. + +## 근거 + +### 왜 P14 전체 이동이 아닌 *분리*인가 +P14의 install.yaml, OLM, multi-arch는 *후행 산출물* — 모든 CRD가 동결된 후에 생성하는 것이 안전. 그러나 Helm chart은 *현재 CRD 상태*를 패키징하면 되므로 *지금* 가능하다. 무리해서 OLM/multi-arch까지 앞당기면 *모든* CRD가 unstable한 alpha 상태에서 매번 재패키징. + +### 왜 chart 두 개로 분리인가 +- *operator*는 cluster-scope 한 번 설치 +- *PostgresCluster CR*은 namespace당 N개 — chart가 다중 인스턴스를 지원하려면 helm release 별로 분리 필요 +- Bitnami가 `postgresql` (CR 인스턴스 chart)을 메인으로 두는 이유와 동일 + +### 왜 *지금*인가 — 기존 P14 유지의 비용 + +| 비용 | 영향 | +|---|---| +| alpha 사용자 부재 | 실 사용 feedback 0, 회귀 발견 늦음 | +| Bitnami로 사용자 이탈 | "PGO-class" 약속의 인지도 손실 | +| chart 작성을 v1.0 직전에 모아서 처리 시 burden | 각 CRD별 template + values default 동시 결정 → 회귀 위험 | + +## 트레이드오프 + +- **두 경로 동시 유지**: Kustomize(`config/default`) + Helm(`charts/postgresql-operator`) 동시 운영 → 변경 시 두 곳 갱신 부담. 완화: chart template이 `config/`의 manifests를 generate (Makefile 타겟에서 `kustomize build config/default | helmify`로 자동화 검토). +- **chart 안정성 약속**: alpha chart는 `Chart.yaml: appVersion`이 v0.x이므로 breaking change 가능. 사용자가 이를 인지해야 함. 완화: chart README에 "alpha 단계, breaking 가능" 명시. +- **OLM bundle 분리 유지**: OperatorHub 사용자는 여전히 P14까지 대기 — 사용자 세그먼트 차이를 인정하고 P14 우선순위는 유지. + +## 결과 + +- `docs/roadmap.md`에서 P14 정의 갱신 — Helm chart을 *P1 트랙*으로 이동. +- 신규 디렉토리 `charts/postgresql-operator/` (P1-4 권장 implementation 시). +- Makefile에 `chart-package`, `chart-lint` 타겟 추가. +- TASKS.md에 P1-4 권장 등록. +- 본 ADR은 v1.0 GA 시점에 *재평가* — Helm chart 안정성과 OLM bundle 통합 시점 검토. + +## 강제 메커니즘 + +| 메커니즘 | 위치 | 도입 시점 | +|---|---|---| +| roadmap.md 갱신 | `docs/roadmap.md` §14 Pillar | 본 ADR 동시 | +| chart 골격 | `charts/postgresql-operator/` | P1-4 | +| chart lint CI 단계 | Makefile + 로컬 hook | P1-4 | +| `helm install --dry-run` 회귀 | e2e | P1-4 | diff --git a/docs/adr/0008-finalizer-avoidance-policy.md b/docs/adr/0008-finalizer-avoidance-policy.md new file mode 100644 index 00000000..119a2c62 --- /dev/null +++ b/docs/adr/0008-finalizer-avoidance-policy.md @@ -0,0 +1,74 @@ +# ADR 0008 — Finalizer 회피 정책 (Cascade Delete via OwnerReference) + +- **상태**: Accepted +- **날짜**: 2026-04-30 +- **결정자**: @keiailab/maintainers +- **관련**: ADR 0002 (Patroni 미사용 — K8s API as DCS), Crunchy PGO 비교 (`/Users/phil/.claude/plans/1-https-artifacthub-io-packages-helm-bit-sunny-wozniak.md` §4 P0-4) + +## 컨텍스트 + +본 프로젝트는 PostgresCluster reconciler가 모든 하위 자원(StatefulSet, Service, ConfigMap, PVC)에 `controllerutil.SetControllerReference`를 호출 (`internal/controller/postgrescluster_controller.go:262`). 이는 K8s GC가 PostgresCluster 삭제 시 *자동 cascade delete*하는 표준 패턴. + +그러나: +- *Finalizer 도입 유혹*이 향후 PR에서 발생 가능 — "외부 자원(backup repo, S3 prefix, certificate) cleanup도 해야 하지 않나?" +- Finalizer는 *추가 reconcile loop* + *K8s API 의존도 증가* + *deletion 지연* 비용 +- ADR 0002 "K8s API as DCS" 원칙이 *분산 합의 단순성*을 보존 — Finalizer는 그 원칙과 충돌 가능 +- Crunchy PGO도 동일 패턴(controllerutil + GC TTL)을 ADR로 못박음 — 본 ADR이 그 결정을 *명시적으로 기록* + +## 결정 + +본 프로젝트는 **Finalizer를 신규 도입하지 않는다.** 다음 두 메커니즘만 사용: + +1. **OwnerReference + K8s GC**: 하위 자원에 `controllerutil.SetControllerReference`로 OwnerReference 설정. PostgresCluster 삭제 시 K8s GC가 cascade delete. +2. **외부 자원 cleanup은 별도 Job CRD**: backup repo cleanup, S3 prefix 정리 등 외부 자원이 PostgresCluster 삭제와 *별도 lifecycle*을 가질 때, 별도 `BackupCleanupJob` 같은 CRD에서 처리. PostgresCluster는 외부 자원을 *알지 못하는 상태*로 삭제 가능. + +### 강제 회귀 테스트 + +`test/e2e/cascade_delete_test.go`에 다음 시나리오 추가: +1. PostgresCluster 생성 → StatefulSet/Service/ConfigMap 생성 확인 +2. PostgresCluster 삭제 +3. **60초 내** 모든 하위 자원이 GC 됨을 검증 + +`internal/controller/postgrescluster_controller_test.go`에 envtest 어셔션: +- 모든 하위 자원이 `controllerutil.HasControllerReference == true` + +## 근거 + +### 왜 Finalizer가 *기본*에서 제외되는가 +Finalizer는 다음 4가지 비용을 만든다: + +1. **Deletion 지연**: 사용자가 `kubectl delete` 후 *Finalizer가 처리될 때까지* PostgresCluster 자원이 K8s API에 남음. 운영자 혼란. +2. **Stuck 위험**: Finalizer 처리 중 reconciler 다운 → 자원 영구 stuck. 강제 제거 시 외부 자원 leak. +3. **Reconcile 복잡도**: 모든 reconcile cycle에서 `if obj.DeletionTimestamp != nil { ... } else { ... }` 분기 추가. +4. **분산 합의 충돌**: ADR 0002 "K8s API as DCS"는 etcd가 단일 진실. Finalizer는 그 진실을 *지연* — 분산 모델에서 timing 가정 추가. + +### 왜 외부 자원은 별도 Job CRD인가 +PostgresCluster 삭제 ≠ backup repo 삭제. 사용자는 PostgresCluster를 삭제해도 *backup은 보존*하고 싶을 수 있음. 또는 *cluster 재생성*을 위해 backup만 보존. 이 분리를 강제하면: + +- PostgresCluster lifecycle 단순화 (외부 의존 0) +- 사용자가 의도적으로 cleanup CRD 생성해야 외부 자원 삭제 → *명시적 의도 표현* +- Finalizer 부재로 deletion 즉시 완료 + +### 왜 *지금* ADR을 작성하는가 +P0-4 회귀 테스트 추가 시점에 *왜 이 테스트가 존재하는가*의 근거를 ADR로 못박음. 향후 PR이 "외부 자원 cleanup 위해 Finalizer 추가"를 시도하면 본 ADR이 Reject 근거. + +## 트레이드오프 + +- **외부 자원 leak 가능성**: Finalizer 부재 → PostgresCluster 삭제 후 외부 backup repo가 leak될 수 있음. 완화: 운영 가이드에서 *별도 cleanup CRD* 사용 권장. 또한 cloud provider 측 lifecycle policy (S3 expiration 등) 활용. +- **kubectl-cnpg 같은 도구의 *graceful drain* 부재**: PGO는 Finalizer로 PG가 graceful shutdown 후 삭제되도록 보장. 본 프로젝트는 fail-fast (ADR 0002 + RFC 0003 부록 A) 모델이라 graceful drain은 *재기동 신호로 운영자가 처리* — 동일 분산 합의 원칙. + +## 결과 + +- 본 ADR은 *향후 모든 PR*의 Finalizer 도입을 reject할 근거. +- P0-4 권장 implementation 시 회귀 테스트 추가. +- 외부 자원 cleanup 필요한 시점(P4 Backup, P7 Security)에 *별도 Job CRD*로 설계. +- 본 ADR 변경(Finalizer 예외 도입)은 RFC 필수 — *광범위 영향* 관점에서 RFC 의무. + +## 강제 메커니즘 + +| 메커니즘 | 위치 | 도입 시점 | +|---|---|---| +| 회귀 테스트 (e2e cascade delete) | `test/e2e/cascade_delete_test.go` | P0-4 implementation | +| envtest 어셔션 (OwnerReference) | `internal/controller/postgrescluster_controller_test.go` | P0-4 | +| golangci-lint 정책 (가능 시 — Finalizer import 차단) | `.custom-gcl.yml` | P13-T2 후속 | +| PR 리뷰 체크리스트 | `standards/checklist.md §3` | 본 ADR 채택과 동시 | diff --git a/docs/roadmap.md b/docs/roadmap.md index 775ea959..7bd170fd 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -34,7 +34,7 @@ title: "Roadmap" | **P11** | ⭐ Citus Topology | coord+workers, `pg_dist_node` sync, 4종 분산 CRD | P1, P2, P10 | | **P12** | ⭐ QueryRouter | stateless 라우터 풀, PgBouncer 사이드카, HPA, metadata lag 메트릭 | P11, P5, P6 | | **P13** | ⭐ Plugin SDK | 5종 Go 인터페이스 + gRPC out-of-process | P4, P6, P10 | -| **P14** | Distribution | Helm, install.yaml, OLM bundle, multi-arch 이미지 | 모두 | +| **P14** | Distribution | install.yaml, OLM bundle, multi-arch 이미지 (※ Helm chart는 ADR 0007에 따라 **P1 트랙으로 분리 — alpha 사용자 채널 조기 확보**) | 모두 | ## 의존 그래프 From ff78ad33dc34cb77cbaa788def91fecb4e835689 Mon Sep 17 00:00:00 2001 From: phil Date: Thu, 30 Apr 2026 12:14:08 +0900 Subject: [PATCH 4/5] fix(deps): otel v1.43.0 + grpc v1.79.3 (CVE-2026-24051/-39883/-33186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #1 CI Trivy fs scan fail을 해결하기 위한 의존성 업데이트. 해소 CVE: - go.opentelemetry.io/otel/sdk: v1.36.0 → v1.43.0 - CVE-2026-24051 (HIGH): Arbitrary Code Execution via PATH Hijacking - CVE-2026-39883 (HIGH): BSD kenv command not using absolute path - google.golang.org/grpc: v1.72.2 → v1.79.3 - CVE-2026-33186 (CRITICAL): authz bypass via improper HTTP/2 path validation 부수 변경 (go mod tidy의 결과): otel/auto/sdk v1.1.0→v1.2.1, golang.org/x/* minor bump, google.golang.org/genproto bump, protobuf v1.36.8→v1.36.11. 모두 indirect 의존성, 코드 변경 0. 검증: - make test: 모든 패키지 PASS - internal/controller 81.0% (회귀 0) - internal/plugin 93.0% (회귀 0) - internal/citus 95.4% (회귀 0) - internal/instance/election 97.4% (회귀 0) - internal/instance/fencing 89.7% (회귀 0) - make lint: 0 issues Refs: PR #1 CI run 25145046256 (Security scan trivy fs) Co-Authored-By: Claude Opus 4.7 (1M context) --- go.mod | 36 +++++++++++++-------------- go.sum | 78 ++++++++++++++++++++++++++++++---------------------------- 2 files changed, 58 insertions(+), 56 deletions(-) diff --git a/go.mod b/go.mod index 98d69506..a91c54d5 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( ) require ( - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -55,34 +55,34 @@ require ( github.com/spf13/pflag v1.0.9 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/sdk v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/tools v0.43.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/grpc v1.72.2 // indirect - google.golang.org/protobuf v1.36.8 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 93c9a0ac..acacca55 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= @@ -153,24 +153,24 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -185,34 +185,36 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= -google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 7bdca05763ecc32e8ef107513ce40aa275ab5691 Mon Sep 17 00:00:00 2001 From: phil Date: Thu, 30 Apr 2026 12:18:31 +0900 Subject: [PATCH 5/5] =?UTF-8?q?test(e2e):=20manager=20log=20capture?= =?UTF-8?q?=EC=97=90=20--previous=20+=20--all-containers=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CrashLoopBackOff 상태의 manager 컨테이너 startup error를 진단하는 인프라. 기존 AfterEach의 `kubectl logs`는 *current container*에만 동작 → BackOff 상태에서는 current container가 not running이라 빈 결과 반환. `--previous` flag로 직전 instance의 startup log 확보, `--all-containers`로 사이드카(있다면) log도 포함, `--tail=500`으로 적절한 크기 제한. PR #1 e2e (kind, PG 16/17, p1) fail의 root cause 진단 단계 1 (인프라 추가). 다음 step: CI 재실행으로 manager startup error 확보 → 분석 → 진짜 fix. 검증: - go vet -tags=e2e ./test/e2e/...: PASS - make lint: 0 issues Refs: PR #1 CI run 25145046256 (e2e fail Exit Code 1) Co-Authored-By: Claude Opus 4.7 (1M context) --- test/e2e/e2e_test.go | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index c46fdfb0..3ad69fa2 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -103,13 +103,29 @@ var _ = Describe("Manager", Ordered, Label("p1"), func() { AfterEach(func() { specReport := CurrentSpecReport() if specReport.Failed() { - By("Fetching controller manager pod logs") - cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) + // 현재 컨테이너 + 이전 컨테이너 log를 모두 시도. CrashLoopBackOff + // 상태에서는 *current* container가 not running이라 `kubectl logs`가 + // 빈 결과를 줌 — `--previous`로 직전 instance의 startup error를 확보. + By("Fetching controller manager pod logs (current)") + cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace, + "--all-containers=true", "--tail=500") controllerLogs, err := utils.Run(cmd) - if err == nil { - _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) + if err == nil && controllerLogs != "" { + _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs (current):\n%s\n", controllerLogs) + } else { + _, _ = fmt.Fprintf(GinkgoWriter, "Current container logs unavailable: err=%v len=%d\n", + err, len(controllerLogs)) + } + + By("Fetching controller manager pod logs (previous instance)") + cmd = exec.Command("kubectl", "logs", controllerPodName, "-n", namespace, + "--previous", "--all-containers=true", "--tail=500") + prevLogs, perr := utils.Run(cmd) + if perr == nil && prevLogs != "" { + _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs (previous instance — CrashLoopBackOff startup):\n%s\n", prevLogs) } else { - _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err) + _, _ = fmt.Fprintf(GinkgoWriter, "Previous container logs unavailable: err=%v len=%d\n", + perr, len(prevLogs)) } By("Fetching Kubernetes events")