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
126 changes: 126 additions & 0 deletions api/v1alpha1/conversion_parity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
Copyright 2026 Keiailab.

Licensed under the MIT License. See the LICENSE file for details.
*/

// 필드 단위 수동 단언은 **빠뜨린 필드를 잡지 못한다** — 실제로 그렇게 놓쳤다:
// conversion_test.go 가 Valkey/ValkeyCluster 를 "테스트하고 있었는데도"
// spec.slowLog 가 Hub(v1alpha2)에 없어 조용히 유실되고 있었고,
// ValkeyBackupTarget 의 gcs/azure 도 마찬가지였다.
//
// 그래서 이 파일은 필드를 **열거하지 않는다**. v1alpha1 객체를 JSON 으로 채운 뒤
// v1alpha2 로 변환하고 되돌려서, 원본 JSON 과 **완전히 같은지**만 본다. 변환이
// JSON byte-copy 이므로 Hub 에 대응 태그가 없는 필드는 왕복에서 사라지고 즉시 실패한다.
// 앞으로 v1alpha1 에 필드가 추가되면 Hub 에 함께 넣지 않는 한 이 테스트가 막는다.
package v1alpha1_test

import (
"encoding/json"
"testing"

"sigs.k8s.io/controller-runtime/pkg/conversion"

"github.com/keiailab/valkey-operator/api/v1alpha1"
"github.com/keiailab/valkey-operator/api/v1alpha2"
)

// spoke — v1alpha1 쪽 타입이 만족해야 하는 최소 인터페이스.
type spoke interface {
ConvertTo(conversion.Hub) error
ConvertFrom(conversion.Hub) error
}

// assertLosslessRoundTrip — src(JSON 으로 채워진 v1alpha1) → hub → back 왕복 후
// JSON 이 원본과 동일한지 검증한다.
func assertLosslessRoundTrip(t *testing.T, name string, srcJSON string, src, back spoke, hub conversion.Hub) {
t.Helper()

if err := json.Unmarshal([]byte(srcJSON), src); err != nil {
t.Fatalf("%s: 픽스처 unmarshal: %v", name, err)
}
if err := src.ConvertTo(hub); err != nil {
t.Fatalf("%s: ConvertTo: %v", name, err)
}
if err := back.ConvertFrom(hub); err != nil {
t.Fatalf("%s: ConvertFrom: %v", name, err)
}

want, err := json.Marshal(src)
if err != nil {
t.Fatalf("%s: marshal src: %v", name, err)
}
got, err := json.Marshal(back)
if err != nil {
t.Fatalf("%s: marshal back: %v", name, err)
}
if string(want) != string(got) {
t.Errorf("%s: 왕복에서 내용이 바뀌었다 — Hub(v1alpha2)에 대응 필드가 없을 가능성이 높다.\n원본: %s\n왕복: %s",
name, want, got)
}
}

func TestConversion_전_타입_왕복이_무손실이다(t *testing.T) {
cases := []struct {
name string
srcJSON string
src spoke
back spoke
hub conversion.Hub
}{
{
name: "Valkey",
srcJSON: `{"metadata":{"name":"vk","namespace":"ns"},"spec":{
"mode":"Replication","replicas":3,
"version":{"version":"8.1.7","image":"docker.io/valkey/valkey"},
"slowLog":{"thresholdMicros":5000,"maxEntries":256},
"autoFailover":true}}`,
src: &v1alpha1.Valkey{}, back: &v1alpha1.Valkey{}, hub: &v1alpha2.Valkey{},
},
{
name: "ValkeyCluster",
srcJSON: `{"metadata":{"name":"vkc","namespace":"ns"},"spec":{
"shards":3,"replicasPerShard":0,
"version":{"version":"8.1.7"},
"slowLog":{"thresholdMicros":7500,"maxEntries":64}}}`,
src: &v1alpha1.ValkeyCluster{}, back: &v1alpha1.ValkeyCluster{}, hub: &v1alpha2.ValkeyCluster{},
},
{
name: "ValkeyBackup",
srcJSON: `{"metadata":{"name":"b","namespace":"ns"},"spec":{
"clusterRef":{"kind":"ValkeyCluster","name":"vkc"},
"type":"Full","storageSize":"10Gi","retainPVC":true,"ttl":"168h",
"volumeSnapshotClassName":"csi-snapclass"}}`,
src: &v1alpha1.ValkeyBackup{}, back: &v1alpha1.ValkeyBackup{}, hub: &v1alpha2.ValkeyBackup{},
},
{
name: "ValkeyBackupTarget/GCS",
srcJSON: `{"metadata":{"name":"t-gcs","namespace":"ns"},"spec":{
"type":"GCS",
"gcs":{"bucket":"bk","prefix":"c/","credentialsSecretRef":{"name":"s","serviceAccountJSONKey":"key.json"}}}}`,
src: &v1alpha1.ValkeyBackupTarget{}, back: &v1alpha1.ValkeyBackupTarget{}, hub: &v1alpha2.ValkeyBackupTarget{},
},
{
name: "ValkeyBackupTarget/Azure",
srcJSON: `{"metadata":{"name":"t-az","namespace":"ns"},"spec":{
"type":"Azure",
"azure":{"accountName":"acct","container":"c","prefix":"p/","serviceURL":"https://x",
"credentialsSecretRef":{"name":"s","accountKeyKey":"AZURE_STORAGE_ACCOUNT_KEY"}}}}`,
src: &v1alpha1.ValkeyBackupTarget{}, back: &v1alpha1.ValkeyBackupTarget{}, hub: &v1alpha2.ValkeyBackupTarget{},
},
{
name: "ValkeyRestore",
srcJSON: `{"metadata":{"name":"r","namespace":"ns"},"spec":{
"clusterRef":{"kind":"ValkeyCluster","name":"vkc"},
"restoreType":"AOF","pointInTime":"2026-07-31T10:00:00Z",
"source":{"pvc":{"name":"snap"}}}}`,
src: &v1alpha1.ValkeyRestore{}, back: &v1alpha1.ValkeyRestore{}, hub: &v1alpha2.ValkeyRestore{},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assertLosslessRoundTrip(t, tc.name, tc.srcJSON, tc.src, tc.back, tc.hub)
})
}
}
19 changes: 19 additions & 0 deletions api/v1alpha2/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,3 +485,22 @@ type PersistencePolicy struct {
// +kubebuilder:default="everysec"
AOFAppendFsync string `json:"aofAppendFsync,omitempty"`
}

// SlowLogSpec — Valkey SLOWLOG 임계값 + 보존 entry 수.
//
// Threshold 보다 오래 걸린 명령은 SLOWLOG 에 기록 — `valkey-cli SLOWLOG GET` 로 조회.
// redis_exporter sidecar 가 자동으로 redis_slowlog_length metric 으로 노출.
//
// v1alpha1 에 있으나 Hub 에 없어 변환 시 조용히 유실되던 타입이다
// (conversion 은 JSON byte-copy 라 대응 태그가 없으면 그냥 사라진다).
type SlowLogSpec struct {
// 단위: microseconds. 0 = SLOWLOG 비활성. -1 = 모든 명령 기록 (debug 만).
// +kubebuilder:default=10000
// +optional
ThresholdMicros int64 `json:"thresholdMicros,omitempty"`

// 보존 entry 수 — FIFO. 초과 시 가장 오래된 entry 폐기.
// +kubebuilder:default=128
// +optional
MaxEntries int32 `json:"maxEntries,omitempty"`
}
5 changes: 5 additions & 0 deletions api/v1alpha2/valkey_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ type ValkeySpec struct {
// +optional
Autoscaling *AutoscalingSpec `json:"autoscaling,omitempty"`

// SlowLog — Valkey SLOWLOG 임계값 + 보존 entry 수 설정.
// nil 이면 valkey 기본값 (10ms / 128 entries) 사용.
// +optional
SlowLog *SlowLogSpec `json:"slowLog,omitempty"`

// +optional
ExternalReplica *ExternalReplicaSpec `json:"externalReplica,omitempty"`

Expand Down
70 changes: 70 additions & 0 deletions api/v1alpha2/valkeybackuptarget_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ type ValkeyBackupTargetSpec struct {
// +optional
S3 *S3Spec `json:"s3,omitempty"`

// Type=GCS 시 필수.
// +optional
GCS *GCSSpec `json:"gcs,omitempty"`

// Type=Azure 시 필수.
// +optional
Azure *AzureSpec `json:"azure,omitempty"`

// Retention — 이 target 을 참조하는 완료된 ValkeyBackup 의 자동 보존 정책
// (cross-region backup lifecycle, ROADMAP 2.x). v1alpha1 reconcile hub 미러.
// +optional
Expand Down Expand Up @@ -166,3 +174,65 @@ func init() {
}

func (t *ValkeyBackupTarget) GetConditions() *[]metav1.Condition { return &t.Status.Conditions }

// 아래 GCS/Azure 계열 타입은 v1alpha1 에 있으나 Hub 에 없어 변환 시 조용히
// 유실되던 것들이다 — 백업 target 이 GCS/Azure 면 대상 자체가 사라진다.
// GCSSpec — Google Cloud Storage 외부 저장 정의 (cloud.google.com/go/storage v1.62.1).
//
// 자격증명: service account JSON key 를 Secret 에 저장. backup Job 의 환경변수
// GOOGLE_APPLICATION_CREDENTIALS 가 가리키는 파일에 mount.
type GCSSpec struct {
// 버킷 이름. 사전 생성 필요.
Bucket string `json:"bucket"`

// object key prefix. 예: "cluster-A/".
// +optional
Prefix string `json:"prefix,omitempty"`

// 자격증명 Secret 참조 (service account JSON).
CredentialsSecretRef GCSCredentialsSecretRef `json:"credentialsSecretRef"`
}

// GCSCredentialsSecretRef — service account JSON 이 들어 있는 Secret.
type GCSCredentialsSecretRef struct {
// Secret 이름.
Name string `json:"name"`

// service account JSON 이 들어 있는 key 이름. 기본 "key.json".
// +kubebuilder:default="key.json"
// +optional
ServiceAccountJSONKey string `json:"serviceAccountJSONKey,omitempty"`
}

// AzureSpec — Azure Blob Storage 외부 저장 정의
// (github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.x).
type AzureSpec struct {
// storage account 이름. e.g. "mystorageacct".
AccountName string `json:"accountName"`

// container 이름 (S3 의 bucket 등가). 사전 생성 필요.
Container string `json:"container"`

// blob name prefix. 예: "cluster-A/".
// +optional
Prefix string `json:"prefix,omitempty"`

// service URL override. 기본: https://<accountName>.blob.core.windows.net.
// Azure China / Government cloud / Azurite (test) 용.
// +optional
ServiceURL string `json:"serviceURL,omitempty"`

// 자격증명 Secret 참조 (account key 또는 SAS token).
CredentialsSecretRef AzureCredentialsSecretRef `json:"credentialsSecretRef"`
}

// AzureCredentialsSecretRef — Azure storage account key 또는 SAS.
type AzureCredentialsSecretRef struct {
// Secret 이름.
Name string `json:"name"`

// account key 가 들어 있는 key 이름. 기본 "AZURE_STORAGE_ACCOUNT_KEY".
// +kubebuilder:default="AZURE_STORAGE_ACCOUNT_KEY"
// +optional
AccountKeyKey string `json:"accountKeyKey,omitempty"`
}
5 changes: 5 additions & 0 deletions api/v1alpha2/valkeycluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ type ValkeyClusterSpec struct {
Auth AuthSpec `json:"auth,omitempty"`
// +optional
Monitoring *MonitoringSpec `json:"monitoring,omitempty"`

// SlowLog — Valkey SLOWLOG 임계값 + 보존 entry 수 설정.
// nil 이면 valkey 기본값 (10ms / 128 entries) 사용.
// +optional
SlowLog *SlowLogSpec `json:"slowLog,omitempty"`
// +optional
Pod *PodSpec `json:"pod,omitempty"`
// +optional
Expand Down
97 changes: 97 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