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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
cloud.google.com/go/storage v1.62.2
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0
github.com/keiailab/keiailab-commons v0.12.0
github.com/keiailab/keiailab-commons v0.13.0
github.com/minio/minio-go/v7 v7.2.0
github.com/onsi/ginkgo/v2 v2.29.0
github.com/onsi/gomega v1.41.0
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,12 @@ github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/keiailab/keiailab-commons v0.12.0 h1:mcFaekA1lTbtCi8LPsyALP+/ObNmYFGO9bKGzcI+/ZQ=
github.com/keiailab/keiailab-commons v0.12.0/go.mod h1:Lfj6BDoP6OaBxSDAtBuG0iHp9ADo5T8b/Bl54w8n74o=
github.com/keiailab/keiailab-commons v0.12.1-0.20260630004504-13e0976fc111 h1:9eZ33N5WdGza4aoJm13PVFC3UAsJ3EyCh9k8/W+5DJs=
github.com/keiailab/keiailab-commons v0.12.1-0.20260630004504-13e0976fc111/go.mod h1:Lfj6BDoP6OaBxSDAtBuG0iHp9ADo5T8b/Bl54w8n74o=
github.com/keiailab/keiailab-commons v0.12.1-0.20260630011743-a54c4a826028 h1:lFNY/8inscTA9bzp3MRjmZNKeA9LDGCChu6UE4vGAo0=
github.com/keiailab/keiailab-commons v0.12.1-0.20260630011743-a54c4a826028/go.mod h1:Lfj6BDoP6OaBxSDAtBuG0iHp9ADo5T8b/Bl54w8n74o=
github.com/keiailab/keiailab-commons v0.13.0 h1:GfJEIs/HeOu5CGMUJQkTHoeyA3BOX3tHMqqACldo6DI=
github.com/keiailab/keiailab-commons v0.13.0/go.mod h1:Lfj6BDoP6OaBxSDAtBuG0iHp9ADo5T8b/Bl54w8n74o=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
Expand Down
16 changes: 6 additions & 10 deletions internal/controller/tls_cert_hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ package controller

import (
"context"
"crypto/sha256"
"encoding/hex"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/keiailab/keiailab-commons/pkg/secrethash"
)

// hashTLSSecret — TLS Secret data (tls.crt + tls.key + ca.crt) 의 SHA256 (hex)
Expand Down Expand Up @@ -43,12 +43,8 @@ func hashTLSSecret(ctx context.Context, c client.Client, namespace, secretName s
}
return "", err
}
h := sha256.New()
// 결정적 순서로 누적 (map 순회 비결정성 회피).
for _, key := range []string{"tls.crt", "tls.key", "ca.crt"} {
// key 자체도 누적해 빈 값/누락을 구분 (예: ca.crt 만 변경되어도 hash 변경).
h.Write([]byte(key))
h.Write(s.Data[key])
}
return hex.EncodeToString(h.Sum(nil)), nil
// 결정적 키 순서 누적은 keiailab-commons/pkg/secrethash.Hash 에 위임. key 자체도
// 누적되어 빈 값/누락을 구분한다 (예: ca.crt 만 변경되어도 hash 변경). 기존 거동과
// byte-identical (동일 키 순서 + full hex).
return secrethash.Hash(s.Data, "tls.crt", "tls.key", "ca.crt"), nil
}
71 changes: 29 additions & 42 deletions internal/resources/backup_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

commonsbatchjob "github.com/keiailab/keiailab-commons/pkg/batchjob"
"github.com/keiailab/keiailab-commons/pkg/security"

cachev1alpha1 "github.com/keiailab/valkey-operator/api/v1alpha1"
Expand Down Expand Up @@ -112,50 +113,36 @@ func BuildBackupJob(p BackupJobParams) *batchv1.Job {
})
}

backoff := int32(2)
ttl := int32(86400) // 24h — Job 자체 는 24시간 후 자동 정리, PVC 는 보존.
return &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: BackupJobName(p.BackupName),
Namespace: p.Namespace,
Labels: BackupLabels(p.BackupName),
},
Spec: batchv1.JobSpec{
BackoffLimit: &backoff,
TTLSecondsAfterFinished: &ttl,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: BackupLabels(p.BackupName)},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyOnFailure,
Containers: []corev1.Container{{
Name: "rdb-copy",
Image: p.Image,
Command: cmd,
Env: []corev1.EnvVar{{
Name: "VALKEY_PASSWORD",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: p.PasswordSecret,
},
}},
VolumeMounts: volumeMounts,
// iteration 37 (cluster incident fix): PodSecurity restricted invariant.
// data ns 의 enforce=restricted 가 admission 단계에서 capabilities.drop /
// seccompProfile / allowPrivilegeEscalation 미설정 pod 거부 → backup
// job-controller 가 매 5-15s 재시도하며 ValkeyBackup Phase=Copying stuck.
// commons.RestrictedContainer 위임 — RunAsUser=999 (postgres-user 와 분리,
// valkey 표준).
SecurityContext: security.RestrictedContainer(security.WithRunAsUser(999)),
}},
Volumes: volumes,
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: new(true),
RunAsUser: new(int64(999)),
FSGroup: new(int64(999)),
},
// Job 엔벨로프(BackoffLimit/TTL/RestartPolicy/라벨전파)는 keiailab-commons/pkg/batchjob.Build
// 에 위임. 컨테이너(valkey-cli --rdb)/볼륨/SecurityContext 조립은 valkey 도메인 잔류.
return commonsbatchjob.Build(commonsbatchjob.Params{
Name: BackupJobName(p.BackupName),
Namespace: p.Namespace,
Labels: BackupLabels(p.BackupName),
BackoffLimit: new(int32(2)),
TTLSecondsAfterFinished: new(int32(86400)), // 24h — Job 자동 정리, PVC 보존.
Containers: []corev1.Container{{
Name: "rdb-copy",
Image: p.Image,
Command: cmd,
Env: []corev1.EnvVar{{
Name: "VALKEY_PASSWORD",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: p.PasswordSecret,
},
},
}},
VolumeMounts: volumeMounts,
// PodSecurity restricted invariant (data ns enforce=restricted admission 통과).
// commons.RestrictedContainer 위임 — RunAsUser=999 (valkey 표준).
SecurityContext: security.RestrictedContainer(security.WithRunAsUser(999)),
}},
Volumes: volumes,
PodSecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: new(true),
RunAsUser: new(int64(999)),
FSGroup: new(int64(999)),
},
}
})
}

func buildBackupShellCommand(p BackupJobParams) string {
Expand Down
71 changes: 31 additions & 40 deletions internal/resources/download_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

commonsbatchjob "github.com/keiailab/keiailab-commons/pkg/batchjob"
)

// RestoreSourcePVCName — ValkeyRestore 가 생성하는 임시 source PVC 이름.
Expand Down Expand Up @@ -114,48 +116,37 @@ func BuildDownloadJob(p DownloadJobParams) *batchv1.Job {
env := s3EnvForJob(p.Endpoint, p.Region, p.ForcePathStyle,
p.CredentialsSecretName, p.AccessKeyIDSecretKey, p.SecretAccessKeySecretKey)

backoff := int32(2)
ttl := int32(86400)
return &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: DownloadJobName(p.RestoreName),
Namespace: p.Namespace,
Labels: RestoreLabels(p.RestoreName),
},
Spec: batchv1.JobSpec{
BackoffLimit: &backoff,
TTLSecondsAfterFinished: &ttl,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: RestoreLabels(p.RestoreName)},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyOnFailure,
Containers: []corev1.Container{{
Name: "download",
Image: p.OperatorImage,
Args: args,
Env: env,
VolumeMounts: []corev1.VolumeMount{
// download 는 write 필요 — ReadOnly=false.
{Name: "backup", MountPath: BackupVolumeMountPath},
},
SecurityContext: buildRestrictedContainerSecurityContext(),
}},
Volumes: []corev1.Volume{
{Name: "backup", VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: p.PVCName,
},
}},
},
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: new(true),
RunAsUser: new(int64(65532)),
FSGroup: new(int64(65532)),
},
},
// Job 엔벨로프는 keiailab-commons/pkg/batchjob.Build 에 위임.
return commonsbatchjob.Build(commonsbatchjob.Params{
Name: DownloadJobName(p.RestoreName),
Namespace: p.Namespace,
Labels: RestoreLabels(p.RestoreName),
BackoffLimit: new(int32(2)),
TTLSecondsAfterFinished: new(int32(86400)),
Containers: []corev1.Container{{
Name: "download",
Image: p.OperatorImage,
Args: args,
Env: env,
VolumeMounts: []corev1.VolumeMount{
// download 는 write 필요 — ReadOnly=false.
{Name: "backup", MountPath: BackupVolumeMountPath},
},
SecurityContext: buildRestrictedContainerSecurityContext(),
}},
Volumes: []corev1.Volume{
{Name: "backup", VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: p.PVCName,
},
}},
},
}
PodSecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: new(true),
RunAsUser: new(int64(65532)),
FSGroup: new(int64(65532)),
},
})
}

// s3EnvForJob — Upload/Download Job 공통 env 빌더 (DRY).
Expand Down
64 changes: 19 additions & 45 deletions internal/resources/hpa.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ package resources

import (
autoscalingv2 "k8s.io/api/autoscaling/v2"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

commonshpa "github.com/keiailab/keiailab-commons/pkg/hpa"

cachev1alpha1 "github.com/keiailab/valkey-operator/api/v1alpha1"
)
Expand All @@ -23,60 +23,34 @@ func HPAName(crName string) string { return crName }
// - target: StatefulSet (CRName)
// - mode=Replication 만 사용 의도 — caller 가 사전 검증 (webhook + reconciler).
// - CPU + Memory metric source (HPA v2 표준).
//
// 빌드 골격(ScaleTargetRef / MinReplicas clamp / Max 보정)은 keiailab-commons/pkg/hpa.Build
// 에 위임. metric 조립은 valkey 도메인(CPU 기본 70 + opt-in Memory)으로 잔류 —
// commons CPUUtilization/MemoryUtilization 헬퍼 사용. MinFloor=2 (valkey 정책).
func BuildHorizontalPodAutoscaler(v *cachev1alpha1.Valkey) *autoscalingv2.HorizontalPodAutoscaler {
if v.Spec.Autoscaling == nil || !v.Spec.Autoscaling.Enabled {
return nil
}
a := v.Spec.Autoscaling

minR := max(a.MinReplicas, int32(2))
maxR := max(a.MaxReplicas, minR)

cpuTarget := a.TargetCPUUtilizationPercentage
if cpuTarget == 0 {
cpuTarget = 70
}

metrics := []autoscalingv2.MetricSpec{
{
Type: autoscalingv2.ResourceMetricSourceType,
Resource: &autoscalingv2.ResourceMetricSource{
Name: corev1.ResourceCPU,
Target: autoscalingv2.MetricTarget{
Type: autoscalingv2.UtilizationMetricType,
AverageUtilization: new(cpuTarget),
},
},
},
}
metrics := []autoscalingv2.MetricSpec{commonshpa.CPUUtilization(cpuTarget)}
if a.TargetMemoryUtilizationPercentage > 0 {
metrics = append(metrics, autoscalingv2.MetricSpec{
Type: autoscalingv2.ResourceMetricSourceType,
Resource: &autoscalingv2.ResourceMetricSource{
Name: corev1.ResourceMemory,
Target: autoscalingv2.MetricTarget{
Type: autoscalingv2.UtilizationMetricType,
AverageUtilization: new(a.TargetMemoryUtilizationPercentage),
},
},
})
metrics = append(metrics, commonshpa.MemoryUtilization(a.TargetMemoryUtilizationPercentage))
}

return &autoscalingv2.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: HPAName(v.Name),
Namespace: v.Namespace,
Labels: CommonLabels(v.Name, "valkey"),
},
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
APIVersion: "apps/v1",
Kind: "StatefulSet",
Name: StatefulSetName(v.Name),
},
MinReplicas: new(minR),
MaxReplicas: maxR,
Metrics: metrics,
},
}
return commonshpa.Build(commonshpa.Params{
Name: HPAName(v.Name),
Namespace: v.Namespace,
Labels: CommonLabels(v.Name, "valkey"),
TargetKind: "StatefulSet",
TargetName: StatefulSetName(v.Name),
MinReplicas: a.MinReplicas,
MaxReplicas: a.MaxReplicas,
MinFloor: 2,
Metrics: metrics,
})
}
Loading
Loading