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
48 changes: 45 additions & 3 deletions api/v1alpha1/conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"

"github.com/keiailab/valkey-operator/api/v1alpha1"
"github.com/keiailab/valkey-operator/api/v1alpha2"
Expand Down Expand Up @@ -104,7 +105,9 @@ func TestValkeyCluster_ConvertTo_v1alpha2(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Name: "cluster-test"},
Spec: v1alpha1.ValkeyClusterSpec{
Shards: 3,
ReplicasPerShard: 1,
ReplicasPerShard: ptr.To[int32](0), // defect ④: masters-only 명시 0 round-trip (nil 과 구별).
// defect ⑥: cluster modules round-trip (v1alpha1 ↔ v1alpha2).
Modules: []v1alpha1.ModuleSpec{{Name: "valkey-search"}},
},
}
dst := &v1alpha2.ValkeyCluster{}
Expand All @@ -115,7 +118,46 @@ func TestValkeyCluster_ConvertTo_v1alpha2(t *testing.T) {
if dst.Spec.Shards != 3 {
t.Errorf("Shards = %d, want 3", dst.Spec.Shards)
}
if dst.Spec.ReplicasPerShard != 1 {
t.Errorf("ReplicasPerShard = %d, want 1", dst.Spec.ReplicasPerShard)
if dst.Spec.ReplicasPerShard == nil || *dst.Spec.ReplicasPerShard != 0 {
t.Errorf("ReplicasPerShard = %v, want explicit 0 (masters-only must round-trip, not nil)", dst.Spec.ReplicasPerShard)
}
if len(dst.Spec.Modules) != 1 || dst.Spec.Modules[0].Name != "valkey-search" {
t.Errorf("Modules = %+v, want [valkey-search]", dst.Spec.Modules)
}

// 역방향 round-trip — v1alpha2 → v1alpha1 도 보존.
back := &v1alpha1.ValkeyCluster{}
if err := back.ConvertFrom(dst); err != nil {
t.Fatalf("ConvertFrom failed: %v", err)
}
if back.Spec.ReplicasPerShard == nil || *back.Spec.ReplicasPerShard != 0 {
t.Errorf("round-trip ReplicasPerShard = %v, want explicit 0", back.Spec.ReplicasPerShard)
}
if len(back.Spec.Modules) != 1 || back.Spec.Modules[0].Name != "valkey-search" {
t.Errorf("round-trip Modules = %+v, want [valkey-search]", back.Spec.Modules)
}
}

// TestValkeyCluster_Convert_NilReplicasPerShard — defect ④: nil(미지정) 은
// round-trip 내내 nil 로 유지되어야 한다 (controller/webhook 가 1 로 default).
// nil 이 도중에 0 으로 바뀌면 omit→masters-only 로 의미가 뒤집힌다.
func TestValkeyCluster_Convert_NilReplicasPerShard(t *testing.T) {
src := &v1alpha1.ValkeyCluster{
ObjectMeta: metav1.ObjectMeta{Name: "cluster-nil-rps"},
Spec: v1alpha1.ValkeyClusterSpec{Shards: 3, ReplicasPerShard: nil},
}
dst := &v1alpha2.ValkeyCluster{}
if err := src.ConvertTo(dst); err != nil {
t.Fatalf("ConvertTo failed: %v", err)
}
if dst.Spec.ReplicasPerShard != nil {
t.Errorf("nil ReplicasPerShard converted to %v, want nil (omitted must stay omitted)", *dst.Spec.ReplicasPerShard)
}
back := &v1alpha1.ValkeyCluster{}
if err := back.ConvertFrom(dst); err != nil {
t.Fatalf("ConvertFrom failed: %v", err)
}
if back.Spec.ReplicasPerShard != nil {
t.Errorf("round-trip nil ReplicasPerShard = %v, want nil", *back.Spec.ReplicasPerShard)
}
}
42 changes: 36 additions & 6 deletions api/v1alpha1/types_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ Licensed under the MIT License. See the LICENSE file for details.

package v1alpha1

import "testing"
import (
"testing"

"k8s.io/utils/ptr"
)

func TestValkeySpecIsAutoFailoverEnabled(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -97,13 +101,15 @@ func TestValkeyClusterSpecTotalNodes(t *testing.T) {
cases := []struct {
name string
shards int32
replicasPerShard int32
replicasPerShard *int32
want int32
}{
{"3 shards, 1 replica each → 6", 3, 1, 6},
{"6 shards, 2 replicas each → 18", 6, 2, 18},
{"1 shard, 0 replicas → 1", 1, 0, 1},
{"0 shards → 0", 0, 1, 0},
{"3 shards, 1 replica each → 6", 3, ptr.To[int32](1), 6},
{"6 shards, 2 replicas each → 18", 6, ptr.To[int32](2), 18},
{"1 shard, explicit 0 replicas → 1 (masters-only)", 1, ptr.To[int32](0), 1},
{"3 shards, explicit 0 replicas → 3 (masters-only)", 3, ptr.To[int32](0), 3},
{"3 shards, nil replicas → 6 (nil defaults to 1)", 3, nil, 6},
{"0 shards → 0", 0, ptr.To[int32](1), 0},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand All @@ -115,3 +121,27 @@ func TestValkeyClusterSpecTotalNodes(t *testing.T) {
})
}
}

// TestValkeyClusterSpecGetReplicasPerShard — defect ④: nil(미지정)→1,
// 명시 0(masters-only)→0, 명시 2→2. 명시 0 이 default 1 로 덮이지 않음을 증명.
func TestValkeyClusterSpecGetReplicasPerShard(t *testing.T) {
t.Parallel()
cases := []struct {
name string
rps *int32
want int32
}{
{"nil → 1 (omitted defaults to 1)", nil, 1},
{"explicit 0 → 0 (masters-only preserved)", ptr.To[int32](0), 0},
{"explicit 2 → 2", ptr.To[int32](2), 2},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Parallel()
s := &ValkeyClusterSpec{ReplicasPerShard: c.rps}
if got := s.GetReplicasPerShard(); got != c.want {
t.Fatalf("GetReplicasPerShard() = %d, want %d", got, c.want)
}
})
}
}
32 changes: 28 additions & 4 deletions api/v1alpha1/valkeycluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,17 @@ type ValkeyClusterSpec struct {
// +kubebuilder:default=3
Shards int32 `json:"shards"`

// shard 당 replica 수. 기본 1 → 총 노드 = shards*(1+replicasPerShard).
// shard 당 replica 수. 총 노드 = shards*(1+replicasPerShard).
//
// *pointer* 인 이유 (defect ④): non-pointer int32 + CRD `+kubebuilder:default=1` 은
// apiserver/admission 이 "필드 미지정" 과 "명시 0" 을 구별하지 못해 명시 0
// (masters-only 토폴로지) 을 default 1 로 덮어쓴다. pointer 로 두면 nil(미지정)
// 과 명시 0 이 구별된다. CRD default 는 제거하고 nil→1 defaulting 은 코드
// (GetReplicasPerShard 접근자 + mutating webhook) 에서 처리한다.
// +kubebuilder:validation:Minimum=0
// +kubebuilder:validation:Maximum=5
// +kubebuilder:default=1
ReplicasPerShard int32 `json:"replicasPerShard"`
// +optional
ReplicasPerShard *int32 `json:"replicasPerShard,omitempty"`

// +kubebuilder:default=true
AutoFailover bool `json:"autoFailover,omitempty"`
Expand Down Expand Up @@ -83,6 +89,13 @@ type ValkeyClusterSpec struct {
// +optional
AdditionalConfig map[string]string `json:"additionalConfig,omitempty"`

// Modules — Valkey 공식 BSD module(valkey-search/json/bloom) 또는 BYO module 로딩.
// 외부 Redis Stack(RediSearch/RedisJSON, RSALv2/SSPL)은 라이선스 비호환으로 미지원
// (ADR-0032). Name 만 지정 시 공식 preset 자동 resolve, Image 지정 시 BYO. Valkey CR
// (ValkeySpec.Modules) 와 동일 타입/검증 — cluster 샤드 pod 전체에 동일 적용.
// +optional
Modules []ModuleSpec `json:"modules,omitempty"`

// +optional
SlowLog *SlowLogSpec `json:"slowLog,omitempty"`

Expand Down Expand Up @@ -163,4 +176,15 @@ func init() {
func (v *ValkeyCluster) GetConditions() *[]metav1.Condition { return &v.Status.Conditions }
func (v *ValkeyCluster) SetPhase(phase string) { v.Status.Phase = ClusterPhase(phase) }

func (s *ValkeyClusterSpec) TotalNodes() int32 { return s.Shards * (1 + s.ReplicasPerShard) }
// GetReplicasPerShard — nil(미지정) → 1, 명시값(0 포함) → 그대로.
//
// defect ④: 명시 0 은 masters-only 토폴로지이므로 보존하고, 미지정만 1 로
// defaulting 한다. ReplicasPerShard 를 읽는 모든 곳은 이 접근자를 거친다.
func (s *ValkeyClusterSpec) GetReplicasPerShard() int32 {
if s.ReplicasPerShard == nil {
return 1
}
return *s.ReplicasPerShard
}

func (s *ValkeyClusterSpec) TotalNodes() int32 { return s.Shards * (1 + s.GetReplicasPerShard()) }
12 changes: 12 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 50 additions & 6 deletions api/v1alpha2/types_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ Licensed under the MIT License. See the LICENSE file for details.

package v1alpha2

import "testing"
import (
"testing"

"k8s.io/utils/ptr"
)

func TestValkeySpecIsAutoFailoverEnabled(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -97,13 +101,15 @@ func TestValkeyClusterSpecTotalNodes(t *testing.T) {
cases := []struct {
name string
shards int32
replicasPerShard int32
replicasPerShard *int32
want int32
}{
{"3 shards, 1 replica each → 6", 3, 1, 6},
{"6 shards, 2 replicas each → 18", 6, 2, 18},
{"1 shard, 0 replicas → 1", 1, 0, 1},
{"0 shards → 0", 0, 1, 0},
{"3 shards, 1 replica each → 6", 3, ptr.To[int32](1), 6},
{"6 shards, 2 replicas each → 18", 6, ptr.To[int32](2), 18},
{"1 shard, explicit 0 replicas → 1 (masters-only)", 1, ptr.To[int32](0), 1},
{"3 shards, explicit 0 replicas → 3 (masters-only)", 3, ptr.To[int32](0), 3},
{"3 shards, nil replicas → 12 (nil defaults to 1)", 3, nil, 6},
{"0 shards → 0", 0, ptr.To[int32](1), 0},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand All @@ -115,3 +121,41 @@ func TestValkeyClusterSpecTotalNodes(t *testing.T) {
})
}
}

// TestValkeyClusterSpecGetReplicasPerShard — defect ④: nil(미지정)→1,
// 명시 0(masters-only)→0, 명시 2→2. 명시 0 이 default 1 로 덮이지 않음을 증명.
func TestValkeyClusterSpecGetReplicasPerShard(t *testing.T) {
t.Parallel()
cases := []struct {
name string
rps *int32
want int32
}{
{"nil → 1 (omitted defaults to 1)", nil, 1},
{"explicit 0 → 0 (masters-only preserved)", ptr.To[int32](0), 0},
{"explicit 2 → 2", ptr.To[int32](2), 2},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Parallel()
s := &ValkeyClusterSpec{ReplicasPerShard: c.rps}
if got := s.GetReplicasPerShard(); got != c.want {
t.Fatalf("GetReplicasPerShard() = %d, want %d", got, c.want)
}
})
}
}

// TestValkeyClusterSpecTotalNodesMastersOnly — 명시 0 의 TotalNodes 가 shards
// 와 같음(masters-only)을 명시 검증. nil 은 shards*2.
func TestValkeyClusterSpecTotalNodesMastersOnly(t *testing.T) {
t.Parallel()
mastersOnly := &ValkeyClusterSpec{Shards: 5, ReplicasPerShard: ptr.To[int32](0)}
if got := mastersOnly.TotalNodes(); got != 5 {
t.Fatalf("masters-only TotalNodes() = %d, want 5 (== shards)", got)
}
omitted := &ValkeyClusterSpec{Shards: 5, ReplicasPerShard: nil}
if got := omitted.TotalNodes(); got != 10 {
t.Fatalf("omitted TotalNodes() = %d, want 10 (shards*2, nil→1)", got)
}
}
32 changes: 28 additions & 4 deletions api/v1alpha2/valkeycluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,17 @@ type ValkeyClusterSpec struct {
// +kubebuilder:default=3
Shards int32 `json:"shards"`

// shard 당 replica 수. 기본 1 → 총 노드 = shards*(1+replicasPerShard).
// shard 당 replica 수. 총 노드 = shards*(1+replicasPerShard).
//
// *pointer* 인 이유 (defect ④): non-pointer int32 + CRD `+kubebuilder:default=1` 은
// apiserver/admission 이 "필드 미지정" 과 "명시 0" 을 구별하지 못해 명시 0
// (masters-only 토폴로지) 을 default 1 로 덮어쓴다. pointer 로 두면 nil(미지정)
// 과 명시 0 이 구별된다. CRD default 는 제거하고 nil→1 defaulting 은 코드
// (GetReplicasPerShard 접근자 + mutating webhook) 에서 처리한다.
// +kubebuilder:validation:Minimum=0
// +kubebuilder:validation:Maximum=5
// +kubebuilder:default=1
ReplicasPerShard int32 `json:"replicasPerShard"`
// +optional
ReplicasPerShard *int32 `json:"replicasPerShard,omitempty"`

// +kubebuilder:default=true
AutoFailover bool `json:"autoFailover,omitempty"`
Expand Down Expand Up @@ -83,6 +89,13 @@ type ValkeyClusterSpec struct {
// +optional
AdditionalConfig map[string]string `json:"additionalConfig,omitempty"`

// Modules — Valkey 공식 BSD module(valkey-search/json/bloom) 또는 BYO module 로딩.
// 외부 Redis Stack(RediSearch/RedisJSON, RSALv2/SSPL)은 라이선스 비호환으로 미지원
// (ADR-0032). Name 만 지정 시 공식 preset 자동 resolve, Image 지정 시 BYO. Valkey CR
// (ValkeySpec.Modules) 와 동일 타입/검증 — cluster 샤드 pod 전체에 동일 적용.
// +optional
Modules []ModuleSpec `json:"modules,omitempty"`

// RevisionHistoryLimit — StatefulSet rollout history 보존 개수.
// +kubebuilder:validation:Minimum=0
// +optional
Expand Down Expand Up @@ -153,4 +166,15 @@ func init() {
func (v *ValkeyCluster) GetConditions() *[]metav1.Condition { return &v.Status.Conditions }
func (v *ValkeyCluster) SetPhase(phase string) { v.Status.Phase = ClusterPhase(phase) }

func (s *ValkeyClusterSpec) TotalNodes() int32 { return s.Shards * (1 + s.ReplicasPerShard) }
// GetReplicasPerShard — nil(미지정) → 1, 명시값(0 포함) → 그대로.
//
// defect ④: 명시 0 은 masters-only 토폴로지이므로 보존하고, 미지정만 1 로
// defaulting 한다. ReplicasPerShard 를 읽는 모든 곳은 이 접근자를 거친다.
func (s *ValkeyClusterSpec) GetReplicasPerShard() int32 {
if s.ReplicasPerShard == nil {
return 1
}
return *s.ReplicasPerShard
}

func (s *ValkeyClusterSpec) TotalNodes() int32 { return s.Shards * (1 + s.GetReplicasPerShard()) }
12 changes: 12 additions & 0 deletions api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading