Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ cluster via GitOps.

**Goal**: implement sharding metadata in-house, without Citus.

- [~] `ShardingMode` field (`none` / `native`) — `postgrescluster_types.go`.
- [~] `ShardsSpec` (initial shard count / replicas / storage) — `postgrescluster_types.go`.
- [~] Sharding plugin interface — `internal/plugin/sharding/api.go`.
- [x] `ShardingMode` field (`none` / `native`) — `postgrescluster_types.go`. Constants + Spec round-trip guarded by `TestShardingMode` (`api/v1alpha1/postgrescluster_types_test.go`); enum validation is enforced at the apiserver via the `+kubebuilder:validation:Enum=none;native` marker. RFC 0001 §3.1 / RFC 0002.
- [x] `ShardsSpec` (initial shard count / replicas / storage) — `postgrescluster_types.go`. Field round-trip + `DeepCopy` slice independence + `Replicas=0` (HA-off dev) guarded by `TestShardsSpec` (`api/v1alpha1/postgrescluster_types_test.go`). RFC 0001 §3.1.
- [x] Sharding plugin interface — `internal/plugin/sharding/api.go`. Compile-time interface freeze + `Registry` register/get/Names round-trip + `Capabilities` advertisement + `ErrUnsupported` sentinel guarded by `TestShardingPlugin` umbrella (`internal/plugin/sharding/api_test.go`). RFC 0001~0005 / RFC 0004 (router architecture).
- [ ] **`ShardRange` CRD** — new `api/v1alpha1/shardrange_types.go`.
- [ ] Hash-range / list / range policy branching.
- [ ] Metadata store (Postgres system catalog or sidecar).
Expand Down Expand Up @@ -210,6 +210,7 @@ cluster via GitOps.

| Date | Change |
|---|---|
| 2026-05-16 | G3 §Sharding foundation: flipped `ShardingMode` / `ShardsSpec` / `Sharding plugin interface` `[~]` → `[x]` with unit-test coverage (`TestShardingMode`, `TestShardsSpec`, `TestShardingPlugin`). Plans `2026-05-14-4-operators-100pct/P-D` §D.7. |
| 2026-05-12 | CNPG backup/restore gap closed: added `ScheduledBackup` CRD/controller, `BackupJob` creation on cron firing, `BackupJob.spec.type=restore` → `RestorePIT` call path, `executionMode=job` runner Job lifecycle, pgBackRest command-runner plugin registration, and the sidecar pod-exec path. |
| 2026-05-12 | CNPG observability gap closed: added Helm metrics Service / ServiceMonitor / PrometheusRule + `postgres_operator_backupjob_phase` Prometheus metric. |
| 2026-05-11 | G1 §Backup/Restore `BackupJob.Phase` transitions (Pending → Running → Succeeded/Failed) implemented + 8 unit tests — `[x]` (ralph-loop iter#3). |
Expand Down
124 changes: 124 additions & 0 deletions api/v1alpha1/postgrescluster_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
Copyright 2026 keiailab.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0
*/

package v1alpha1

import (
"testing"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)

// TestShardingMode 는 ShardingMode 타입의 상수 값과 필드 round-trip 을 검증한다.
// RFC 0001 §3.1, RFC 0002 §shardrange-crd 분기 정합.
//
// CRD enum validation 은 kubebuilder marker (+kubebuilder:validation:Enum=none;native)
// 으로 apiserver 단계에서 강제되므로 본 unit test 는 Go-level 식별자 + 기본값 + round-trip
// 만 검사한다 (§3 Surgical — 신규 validation 로직 도입 금지).
func TestShardingMode(t *testing.T) {
t.Run("상수 값이 RFC 0001 §3.1 와 일치한다", func(t *testing.T) {
if ShardingModeNone != "none" {
t.Errorf("ShardingModeNone = %q, want %q", ShardingModeNone, "none")
}
if ShardingModeNative != "native" {
t.Errorf("ShardingModeNative = %q, want %q", ShardingModeNative, "native")
}
})

t.Run("PostgresClusterSpec round-trip 에서 ShardingMode 가 보존된다", func(t *testing.T) {
cases := []struct {
name string
mode ShardingMode
}{
{"기본 none", ShardingModeNone},
{"native 모드", ShardingModeNative},
{"빈 값 (apiserver default 적용 전)", ShardingMode("")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
spec := PostgresClusterSpec{ShardingMode: tc.mode}
if spec.ShardingMode != tc.mode {
t.Errorf("round-trip 실패: got %q, want %q", spec.ShardingMode, tc.mode)
}
})
}
})

t.Run("ShardingMode 는 string underlying 으로 비교 가능하다", func(t *testing.T) {
// reconciler / webhook 분기에서 직접 비교가 사용된다 (RFC 0001 §3.1).
var m ShardingMode = "native"
if m != ShardingModeNative {
t.Errorf("string 비교 실패: %q != %q", m, ShardingModeNative)
}
})
}

// TestShardsSpec 는 ShardsSpec 필드 round-trip + deepcopy 동작을 검증한다.
// RFC 0001 §3.1 shard topology 정합.
func TestShardsSpec(t *testing.T) {
t.Run("필드 round-trip", func(t *testing.T) {
spec := ShardsSpec{
InitialCount: 4,
Replicas: 2,
Storage: StorageSpec{
StorageClass: "rook-ceph-block",
Size: resource.MustParse("10Gi"),
},
PriorityClassName: "postgres-shard-critical",
}
if spec.InitialCount != 4 {
t.Errorf("InitialCount round-trip 실패: %d", spec.InitialCount)
}
if spec.Replicas != 2 {
t.Errorf("Replicas round-trip 실패: %d", spec.Replicas)
}
if spec.Storage.StorageClass != "rook-ceph-block" {
t.Errorf("Storage.StorageClass round-trip 실패: %q", spec.Storage.StorageClass)
}
if spec.PriorityClassName != "postgres-shard-critical" {
t.Errorf("PriorityClassName round-trip 실패: %q", spec.PriorityClassName)
}
})

t.Run("DeepCopy 가 독립 복제본을 만든다", func(t *testing.T) {
original := ShardsSpec{
InitialCount: 3,
Replicas: 1,
Storage: StorageSpec{
StorageClass: "ceph",
Size: resource.MustParse("5Gi"),
},
Tolerations: []corev1.Toleration{
{Key: "dedicated", Operator: corev1.TolerationOpEqual, Value: "postgres"},
},
}
clone := original.DeepCopy()
if clone == nil {
t.Fatal("DeepCopy returned nil")
}
if clone.InitialCount != original.InitialCount {
t.Errorf("DeepCopy InitialCount 불일치: %d vs %d", clone.InitialCount, original.InitialCount)
}
// 슬라이스 독립성 — 원본 수정이 clone 에 영향 주지 않는다.
original.Tolerations[0].Value = "mutated"
if clone.Tolerations[0].Value == "mutated" {
t.Error("DeepCopy Tolerations 슬라이스가 공유됨 (독립 복제 실패)")
}
})

t.Run("Replicas=0 (HA 없음, dev only) 도 허용된다", func(t *testing.T) {
// RFC 0001 §3.1: Replicas=0 은 schema 상 합법 (Minimum=0).
spec := ShardsSpec{InitialCount: 1, Replicas: 0}
if spec.Replicas != 0 {
t.Errorf("Replicas=0 round-trip 실패: %d", spec.Replicas)
}
})
}
58 changes: 58 additions & 0 deletions internal/plugin/sharding/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package sharding
import (
"context"
"database/sql"
"errors"
"testing"
)

Expand Down Expand Up @@ -96,3 +97,60 @@ func TestErrUnsupported_Message(t *testing.T) {
func TestNoopPlugin_InterfaceFreezeCheck(t *testing.T) {
var _ ShardingPlugin = noopPlugin{}
}

// TestShardingPlugin 는 ShardingPlugin 인터페이스 contract 의 통합 검증이다.
// RFC 0001~0005 의 plugin 동결 합의(인터페이스 시그니처 + Registry round-trip +
// Unsupported sentinel + capability 광고) 가 한 곳에서 회귀 가드된다.
//
// 본 테스트는 위 개별 테스트(Register_Get / Get_NotFound / Names /
// InterfaceFreezeCheck / ErrUnsupported_Message)의 wrapper umbrella 로,
// `go test -run TestShardingPlugin` 호출 한 번으로 plugin foundation 의 핵심
// 계약을 모두 실행한다 (plan P-D §D.7.3 verify).
func TestShardingPlugin(t *testing.T) {
t.Run("InterfaceFreeze", func(t *testing.T) {
// 컴파일 타임 인터페이스 충족 확인 — 본 라인이 compile 되면 PASS.
var _ ShardingPlugin = noopPlugin{}
})

t.Run("RegistryRoundTrip", func(t *testing.T) {
r := NewRegistry()
r.Register(noopPlugin{})

p, ok := r.Get(testBackendNoop)
if !ok {
t.Fatal("등록한 plugin 을 Get 으로 찾지 못함")
}
if p.Name() != testBackendNoop {
t.Errorf("Name() 불일치: %q", p.Name())
}
names := r.Names()
if len(names) != 1 || names[0] != testBackendNoop {
t.Errorf("Names() 결과 불일치: %v", names)
}
if _, found := r.Get("missing-backend"); found {
t.Error("미등록 backend 가 조회됨")
}
})

t.Run("CapabilitiesAdvertise", func(t *testing.T) {
// noopPlugin 은 모든 capability=false 광고 (RFC 0001 §3.1).
caps := noopPlugin{}.Capabilities()
if caps.DistributedTables || caps.ReferenceTables || caps.Distributed2PC ||
caps.OnlineRebalance || caps.ColumnarStorage || caps.NativeQueryPlanner {
t.Errorf("noop plugin 이 capability 광고를 함: %+v", caps)
}
})

t.Run("UnsupportedSentinel", func(t *testing.T) {
// 미지원 capability 호출 시 ErrUnsupported 가 반환되어야 webhook 에서
// 의미 있는 거절 메시지 생성 가능 (RFC 0001 §3.1).
err := noopPlugin{}.CreateReferenceTable(context.TODO(), nil, "public.t")
var sentinel *ErrUnsupported
if !errors.As(err, &sentinel) {
t.Fatalf("ErrUnsupported sentinel 미반환: %v", err)
}
if sentinel.Backend != testBackendNoop {
t.Errorf("Backend 필드 불일치: %q", sentinel.Backend)
}
})
}