From fb1476b55b905478607cc9e7dc34cec968ad1a9b Mon Sep 17 00:00:00 2001 From: phil Date: Tue, 30 Jun 2026 09:55:51 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20hashSecretData=20=EB=B9=84=EA=B2=B0?= =?UTF-8?q?=EC=A0=95=EC=84=B1=20=EA=B5=90=EC=A0=95=20=E2=80=94=20commons?= =?UTF-8?q?=20secrethash=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mongo 의 hashSecretData 가 `for k, v := range secret.Data` 로 map 을 비결정적 순회해 동일 Secret 이 reconcile 마다 다른 해시를 낼 수 있던 correctness 버그 (불필요한 password-rotation pending 신호 유발)를 keiailab-commons/pkg/secrethash.Hash (정렬 누적)로 위임해 구조적 교정. 16자 truncation 은 annotation 형식 보존. - controller/password_rotation.go: hashSecretData → commonssecrethash.Hash(...)[:16] - controller/password_rotation_hash_test.go: determinism + value-change 테스트 신설 (기존 0건 → 2건). 200회 반복 동일 해시 검증. commons 의존 v0.11.0 → v0.12.1-0.20260630004504-13e0976fc111 (keiailab-commons#86). ⚠️ adopt 시 1회 해시 변화로 rotation pending 신호가 한 번 set 될 수 있음 (determinism 교정의 불가피한 1회성, 이후 안정). 검증: go build ./... → OK go vet ./internal/controller/... → clean go test -run TestHashSecretData ./internal/controller/... → ok (determinism PASS) KUBEBUILDER_ASSETS=… go test ./internal/controller/... → ok (20.1s, envtest 전체 suite) Co-Authored-By: Claude Opus 4.8 Signed-off-by: phil --- go.mod | 2 +- go.sum | 2 + internal/controller/password_rotation.go | 15 ++++---- .../controller/password_rotation_hash_test.go | 37 +++++++++++++++++++ 4 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 internal/controller/password_rotation_hash_test.go diff --git a/go.mod b/go.mod index 4976ab2..c91de7c 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.26.4 require ( // TODO(#236): keiailab-commons v1.0.0 안정 릴리스 시 업그레이드 예정. // 현재 v0.10.0 — API 변경 가능성 잔존. - github.com/keiailab/keiailab-commons v0.11.0 + github.com/keiailab/keiailab-commons v0.12.1-0.20260630004504-13e0976fc111 github.com/onsi/ginkgo/v2 v2.29.0 github.com/onsi/gomega v1.41.0 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index 32401da..6e37996 100644 --- a/go.sum +++ b/go.sum @@ -105,6 +105,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/keiailab/keiailab-commons v0.11.0 h1:aydaheRHz3OjqgWU6C6dpssYA87ba9wZsd3G2kTAxCY= github.com/keiailab/keiailab-commons v0.11.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/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/internal/controller/password_rotation.go b/internal/controller/password_rotation.go index c57623f..a2c64af 100644 --- a/internal/controller/password_rotation.go +++ b/internal/controller/password_rotation.go @@ -2,9 +2,8 @@ package controller import ( "context" - "crypto/sha256" - "fmt" + commonssecrethash "github.com/keiailab/keiailab-commons/pkg/secrethash" commonsstatus "github.com/keiailab/keiailab-commons/pkg/status" corev1 "k8s.io/api/core/v1" @@ -62,12 +61,12 @@ func (r *MongoDBReconciler) reconcilePasswordRotation(ctx context.Context, mdb * } func hashSecretData(secret *corev1.Secret) string { - h := sha256.New() - for k, v := range secret.Data { - h.Write([]byte(k)) - h.Write(v) - } - return fmt.Sprintf("%x", h.Sum(nil))[:16] + // 결정적 정렬-누적은 keiailab-commons/pkg/secrethash.Hash 에 위임 — 기존 + // `for k, v := range secret.Data` map 순회의 *비결정성 버그*(동일 Secret 이 + // reconcile 마다 다른 해시 → 불필요한 rotation 신호) 를 구조적 교정. 16자 + // truncation 은 annotation 형식 보존을 위해 유지 (determinism 만 교정 → + // adopt 시 1회 해시 변화 후 안정). + return commonssecrethash.Hash(secret.Data)[:16] } func getAnnotation(mdb *mongodbv1alpha1.MongoDB, key string) string { diff --git a/internal/controller/password_rotation_hash_test.go b/internal/controller/password_rotation_hash_test.go new file mode 100644 index 0000000..4035cfc --- /dev/null +++ b/internal/controller/password_rotation_hash_test.go @@ -0,0 +1,37 @@ +package controller + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" +) + +// TestHashSecretData_Deterministic 은 commons secrethash 위임으로 기존 map-순회 +// 비결정성 버그가 교정됐음을 보증한다 — 동일 Secret 은 항상 동일 해시. +func TestHashSecretData_Deterministic(t *testing.T) { + t.Parallel() + s := &corev1.Secret{Data: map[string][]byte{ + "username": []byte("admin"), + "password": []byte("p@ss"), + "keyfile": []byte("xyzkeyfilecontents"), + }} + first := hashSecretData(s) + for range 200 { + if got := hashSecretData(s); got != first { + t.Fatalf("hashSecretData 비결정적: %s != %s", got, first) + } + } + if len(first) != 16 { + t.Fatalf("해시 길이 %d, want 16 (annotation 형식 truncation 보존)", len(first)) + } +} + +// 값 변경이 해시에 반영돼야 rotation 신호가 동작한다. +func TestHashSecretData_ValueChangeChangesHash(t *testing.T) { + t.Parallel() + a := hashSecretData(&corev1.Secret{Data: map[string][]byte{"password": []byte("OLD")}}) + b := hashSecretData(&corev1.Secret{Data: map[string][]byte{"password": []byte("NEW")}}) + if a == b { + t.Fatal("값 변경이 해시에 반영되지 않음") + } +} From 101df1272abc36be1e89343401cdb641320aba91 Mon Sep 17 00:00:00 2001 From: phil Date: Tue, 30 Jun 2026 10:03:51 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20PDB/HPA=20=EB=B9=8C=EB=8D=94?= =?UTF-8?q?=EB=A5=BC=20keiailab-commons=20=EB=A1=9C=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mongo 의 PDB/HPA 빌더 본문을 commons 공유 패키지로 위임 (builder.go 중복 -53 LOC). exported 시그니처 유지 → 호출부 변경 0. - BuildMongoDBPDB + pdbBaseSpec → commons pdb.Build (DefaultFloor=0). mongo 의 MinAvailable-우선 = commons 기본과 동일 → 거동 보존. - buildHPAForTarget → commons hpa.Build (MinFloor=1). buildHPAMetrics 의 cpu/memory/ 기본 케이스 → commons CPUUtilization/MemoryUtilization 헬퍼. custom Pods metric 은 mongo 도메인 잔류. 검증: go build ./... → OK go vet ./internal/resources/... → clean golangci-lint run ./internal/resources/... → 0 issues go test ./internal/resources/... → ok Co-Authored-By: Claude Opus 4.8 Signed-off-by: phil --- internal/resources/builder.go | 137 +++++++++++----------------------- 1 file changed, 42 insertions(+), 95 deletions(-) diff --git a/internal/resources/builder.go b/internal/resources/builder.go index 5102a85..7a6f211 100644 --- a/internal/resources/builder.go +++ b/internal/resources/builder.go @@ -24,8 +24,10 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" + commonshpa "github.com/keiailab/keiailab-commons/pkg/hpa" commonslabels "github.com/keiailab/keiailab-commons/pkg/labels" commonsnp "github.com/keiailab/keiailab-commons/pkg/networkpolicy" + commonspdb "github.com/keiailab/keiailab-commons/pkg/pdb" "github.com/keiailab/keiailab-commons/pkg/probes" commonstopology "github.com/keiailab/keiailab-commons/pkg/topology" @@ -2282,32 +2284,19 @@ func BuildMongoDBPDB(mdb *mongodbv1alpha1.MongoDB) *policyv1.PodDisruptionBudget if pdbSpec == nil || !pdbSpec.Enabled { return nil } - pdb := &policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Name: mdb.Name + "-pdb", - Namespace: mdb.Namespace, - Labels: buildLabels(mdb.Name, "replicaset"), - }, - Spec: policyv1.PodDisruptionBudgetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: buildLabels(mdb.Name, "replicaset"), - }, - }, - } - switch { - case pdbSpec.MinAvailable != nil: - pdb.Spec.MinAvailable = pdbSpec.MinAvailable - case pdbSpec.MaxUnavailable != nil: - pdb.Spec.MaxUnavailable = pdbSpec.MaxUnavailable - default: - minAvail := mdb.Spec.Members - 1 - if minAvail < 0 { - minAvail = 0 - } - v := intstr.FromInt(int(minAvail)) - pdb.Spec.MinAvailable = &v - } - return pdb + labels := buildLabels(mdb.Name, "replicaset") + // 빌드 골격은 keiailab-commons/pkg/pdb.Build 에 위임 (DefaultFloor=0 = mongo). + // mongo 는 MinAvailable-우선 = commons 기본과 동일 → 거동 보존. + return commonspdb.Build(commonspdb.Params{ + Name: mdb.Name + "-pdb", + Namespace: mdb.Namespace, + Labels: labels, + Selector: labels, + Replicas: mdb.Spec.Members, + DefaultFloor: 0, + MinAvailable: pdbSpec.MinAvailable, + MaxUnavailable: pdbSpec.MaxUnavailable, + }) } // BuildMongoDBNetworkPolicy는 MongoDB ReplicaSet pods에 대한 deny-by-default @@ -2359,23 +2348,16 @@ func convertAdditionalPeers(in []mongodbv1alpha1.NetworkPolicyPeer) []commonsnp. // pdbBaseSpec은 PodDisruptionBudgetSpec를 받아 기본 minAvailable=replicas-1을 // 적용한 K8s PDB spec을 만든다(MinAvailable/MaxUnavailable 미지정 시). func pdbBaseSpec(spec *mongodbv1alpha1.PodDisruptionBudgetSpec, replicas int32, selector map[string]string) policyv1.PodDisruptionBudgetSpec { - out := policyv1.PodDisruptionBudgetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: selector}, - } - switch { - case spec.MinAvailable != nil: - out.MinAvailable = spec.MinAvailable - case spec.MaxUnavailable != nil: - out.MaxUnavailable = spec.MaxUnavailable - default: - minAvail := replicas - 1 - if minAvail < 0 { - minAvail = 0 - } - v := intstr.FromInt(int(minAvail)) - out.MinAvailable = &v - } - return out + // 빌드 골격(min/max 우선순위 + default-floor)은 keiailab-commons/pkg/pdb.Build + // 에 위임 (DefaultFloor=0 = mongo). BuildShardedPDBs 가 ObjectMeta 를 별도 + // 설정하므로 여기서는 Spec 만 반환한다. + return commonspdb.Build(commonspdb.Params{ + Selector: selector, + Replicas: replicas, + DefaultFloor: 0, + MinAvailable: spec.MinAvailable, + MaxUnavailable: spec.MaxUnavailable, + }).Spec } // BuildShardedPDBs는 sharded cluster의 cfg/shards/mongos 각 컴포넌트에 대한 @@ -2526,27 +2508,20 @@ func BuildConfigServerHPA(mdbsh *mongodbv1alpha1.MongoDBSharded) *autoscalingv2. func buildHPAForTarget(name, namespace string, labels map[string]string, kind, refName string, as *mongodbv1alpha1.AutoScalingSpec, ) *autoscalingv2.HorizontalPodAutoscaler { - min := as.MinReplicas - if min < 1 { - min = 1 - } - return &autoscalingv2.HorizontalPodAutoscaler{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - }, - Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ - ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ - APIVersion: "apps/v1", - Kind: kind, - Name: refName, - }, - MinReplicas: &min, - MaxReplicas: as.MaxReplicas, - Metrics: buildHPAMetrics(as.Metrics), - }, - } + // 빌드 골격(ScaleTargetRef apps/v1 + MinReplicas clamp)은 + // keiailab-commons/pkg/hpa.Build 에 위임 (MinFloor=1 = mongo). metric 조립은 + // buildHPAMetrics(cpu/memory/custom) 도메인 잔류. + return commonshpa.Build(commonshpa.Params{ + Name: name, + Namespace: namespace, + Labels: labels, + TargetKind: kind, + TargetName: refName, + MinReplicas: as.MinReplicas, + MaxReplicas: as.MaxReplicas, + MinFloor: 1, + Metrics: buildHPAMetrics(as.Metrics), + }) } // IsRSHPAActive는 RS HPA가 reconcile에서 활성 상태인지 검사한다(이중 가드). @@ -2590,44 +2565,16 @@ func IsShardScaleDeliberate(mdbsh *mongodbv1alpha1.MongoDBSharded) bool { func buildHPAMetrics(metrics []mongodbv1alpha1.AutoScalingMetric) []autoscalingv2.MetricSpec { if len(metrics) == 0 { // 아무 metric도 없으면 cpu 80% 기본값 — upstream chart 등 표준 기본. - v := int32(80) - return []autoscalingv2.MetricSpec{{ - Type: autoscalingv2.ResourceMetricSourceType, - Resource: &autoscalingv2.ResourceMetricSource{ - Name: corev1.ResourceCPU, - Target: autoscalingv2.MetricTarget{ - Type: autoscalingv2.UtilizationMetricType, - AverageUtilization: &v, - }, - }, - }} + return []autoscalingv2.MetricSpec{commonshpa.CPUUtilization(80)} } out := make([]autoscalingv2.MetricSpec, 0, len(metrics)) for _, m := range metrics { target := m.Target switch m.Type { case "cpu": - out = append(out, autoscalingv2.MetricSpec{ - Type: autoscalingv2.ResourceMetricSourceType, - Resource: &autoscalingv2.ResourceMetricSource{ - Name: corev1.ResourceCPU, - Target: autoscalingv2.MetricTarget{ - Type: autoscalingv2.UtilizationMetricType, - AverageUtilization: &target, - }, - }, - }) + out = append(out, commonshpa.CPUUtilization(target)) case "memory": - out = append(out, autoscalingv2.MetricSpec{ - Type: autoscalingv2.ResourceMetricSourceType, - Resource: &autoscalingv2.ResourceMetricSource{ - Name: corev1.ResourceMemory, - Target: autoscalingv2.MetricTarget{ - Type: autoscalingv2.UtilizationMetricType, - AverageUtilization: &target, - }, - }, - }) + out = append(out, commonshpa.MemoryUtilization(target)) case "custom": if m.CustomMetric == nil || m.CustomMetric.Name == "" { continue From 8d41aeb4bf6efc5eacd4e7f91192a09db50d8fe9 Mon Sep 17 00:00:00 2001 From: phil Date: Tue, 30 Jun 2026 10:25:55 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20Service/TLS-volume=20=EB=B9=8C?= =?UTF-8?q?=EB=8D=94=EB=A5=BC=20commons=20=EB=A1=9C=20=EC=9C=84=EC=9E=84?= =?UTF-8?q?=20(2=EC=B0=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - resources/builder.go BuildHeadlessService/BuildClientService → commons service.Build (Headless → ClusterIP None + PublishNotReadyAddresses). - buildTLSServerVolume/buildTLSServerMount → commons volume.TLSSecretMount (0o400 + ReadOnly cert 불변식 SSOT). commons 의존 → v0.12.1-0.20260630011743-a54c4a826028 (keiailab-commons#86 2차). 검증: go build ./... OK / go vet clean / golangci-lint 0 issues / go test resources ok. Co-Authored-By: Claude Opus 4.8 Signed-off-by: phil --- go.mod | 2 +- go.sum | 2 + internal/resources/builder.go | 76 ++++++++++++++--------------------- 3 files changed, 34 insertions(+), 46 deletions(-) diff --git a/go.mod b/go.mod index c91de7c..2793b16 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.26.4 require ( // TODO(#236): keiailab-commons v1.0.0 안정 릴리스 시 업그레이드 예정. // 현재 v0.10.0 — API 변경 가능성 잔존. - github.com/keiailab/keiailab-commons v0.12.1-0.20260630004504-13e0976fc111 + github.com/keiailab/keiailab-commons v0.12.1-0.20260630011743-a54c4a826028 github.com/onsi/ginkgo/v2 v2.29.0 github.com/onsi/gomega v1.41.0 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index 6e37996..700228a 100644 --- a/go.sum +++ b/go.sum @@ -107,6 +107,8 @@ github.com/keiailab/keiailab-commons v0.11.0 h1:aydaheRHz3OjqgWU6C6dpssYA87ba9wZ github.com/keiailab/keiailab-commons v0.11.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/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/internal/resources/builder.go b/internal/resources/builder.go index 7a6f211..a2af3a0 100644 --- a/internal/resources/builder.go +++ b/internal/resources/builder.go @@ -29,7 +29,9 @@ import ( commonsnp "github.com/keiailab/keiailab-commons/pkg/networkpolicy" commonspdb "github.com/keiailab/keiailab-commons/pkg/pdb" "github.com/keiailab/keiailab-commons/pkg/probes" + commonsservice "github.com/keiailab/keiailab-commons/pkg/service" commonstopology "github.com/keiailab/keiailab-commons/pkg/topology" + commonsvolume "github.com/keiailab/keiailab-commons/pkg/volume" mongodbv1alpha1 "github.com/keiailab/mongodb-operator/api/v1alpha1" "github.com/keiailab/mongodb-operator/internal/assets" @@ -212,24 +214,15 @@ func tlsArgs(tls *mongodbv1alpha1.TLSSpec) []string { // Phase 3a 는 volume mount 만 추가 — mongod args 의 --tlsCertificateKeyFile 통합은 // Phase 3b 에서 init container PEM merge (cat tls.crt tls.key > server.pem) 후. func buildTLSServerVolume(secretName string) corev1.Volume { - return corev1.Volume{ - Name: "tls-server", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: secretName, - DefaultMode: ptr.To[int32](0o400), - }, - }, - } + // 0o400/readonly cert 불변식은 keiailab-commons/pkg/volume.TLSSecretMount 에 위임. + vol, _ := commonsvolume.TLSSecretMount("tls-server", secretName, MongoTLSMountPath) + return vol } // buildTLSServerMount 는 tls-server Volume 의 mountPath 를 반환한다. func buildTLSServerMount() corev1.VolumeMount { - return corev1.VolumeMount{ - Name: "tls-server", - MountPath: MongoTLSMountPath, - ReadOnly: true, - } + _, mount := commonsvolume.TLSSecretMount("tls-server", "", MongoTLSMountPath) + return mount } // buildLabels — keiailab-commons/pkg/labels 위임 (iteration 27). @@ -530,42 +523,35 @@ func BuildShardScriptsConfigMap(mdbsh *mongodbv1alpha1.MongoDBSharded, shardInde } } -// BuildHeadlessService creates a headless service for StatefulSet +// BuildHeadlessService creates a headless service for StatefulSet. +// 조립은 keiailab-commons/pkg/service.Build 에 위임 (Headless → ClusterIP None + +// PublishNotReadyAddresses). 포트/라벨은 mongo 도메인 잔류. func BuildHeadlessService(mdb *mongodbv1alpha1.MongoDB) *corev1.Service { - return &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: mdb.Name + "-headless", - Namespace: mdb.Namespace, - Labels: buildLabels(mdb.Name, "headless"), - }, - Spec: corev1.ServiceSpec{ - ClusterIP: "None", - Selector: buildLabels(mdb.Name, "replicaset"), - Ports: []corev1.ServicePort{ - {Name: "mongodb", Port: mongoDBPort, TargetPort: intstr.FromInt(mongoDBPort)}, - }, - PublishNotReadyAddresses: true, - }, - } + return commonsservice.Build(commonsservice.Params{ + Name: mdb.Name + "-headless", + Namespace: mdb.Namespace, + Labels: buildLabels(mdb.Name, "headless"), + Selector: buildLabels(mdb.Name, "replicaset"), + Ports: []corev1.ServicePort{ + {Name: "mongodb", Port: mongoDBPort, TargetPort: intstr.FromInt(mongoDBPort)}, + }, + Headless: true, + }) } -// BuildClientService creates a client service for MongoDB access +// BuildClientService creates a client service for MongoDB access. func BuildClientService(mdb *mongodbv1alpha1.MongoDB) *corev1.Service { - return &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: mdb.Name, - Namespace: mdb.Namespace, - Labels: buildLabels(mdb.Name, "client"), + return commonsservice.Build(commonsservice.Params{ + Name: mdb.Name, + Namespace: mdb.Namespace, + Labels: buildLabels(mdb.Name, "client"), + Selector: buildLabels(mdb.Name, "replicaset"), + Type: corev1.ServiceTypeClusterIP, + Ports: []corev1.ServicePort{ + {Name: "mongodb", Port: mongoDBPort, TargetPort: intstr.FromInt(mongoDBPort)}, + {Name: "metrics", Port: metricsPort, TargetPort: intstr.FromInt(metricsPort)}, }, - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeClusterIP, - Selector: buildLabels(mdb.Name, "replicaset"), - Ports: []corev1.ServicePort{ - {Name: "mongodb", Port: mongoDBPort, TargetPort: intstr.FromInt(mongoDBPort)}, - {Name: "metrics", Port: metricsPort, TargetPort: intstr.FromInt(metricsPort)}, - }, - }, - } + }) } // BuildReplicaSetStatefulSet creates a StatefulSet for MongoDB ReplicaSet. From 75ce128303478274bd4d80a96911e76aeeb97d05 Mon Sep 17 00:00:00 2001 From: phil Date: Tue, 30 Jun 2026 10:33:28 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20=EB=B0=B1=EC=97=85/=EB=B3=B5?= =?UTF-8?q?=EC=9B=90=20Job=20=EC=97=94=EB=B2=A8=EB=A1=9C=ED=94=84=EB=A5=BC?= =?UTF-8?q?=20commons=20batchjob=20=EB=A1=9C=20=EC=9C=84=EC=9E=84=20(3?= =?UTF-8?q?=EC=B0=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mongo 의 배치 Job 빌더 2종을 keiailab-commons/pkg/batchjob.Build 로 위임. - BuildBackupJob (mongodump 스크립트) - BuildRestoreJob (mongorestore + oplogReplay) Job/PodTemplate 엔벨로프(BackoffLimit/TTL=86400/RestartPolicyOnFailure/라벨전파)는 commons SSOT. 컨테이너(스크립트)·볼륨·SecurityContext 조립은 mongo 잔류. BuildBackupCronJob 은 CronJob(JobTemplate) 타입이라 대상 외 (유지). 검증: go build OK / vet clean / golangci-lint 신규 0 / resources test ok / KUBEBUILDER_ASSETS=… controller envtest 전체 suite ok (19.3s). Co-Authored-By: Claude Opus 4.8 Signed-off-by: phil --- internal/resources/builder.go | 122 +++++++++++++++------------------- 1 file changed, 52 insertions(+), 70 deletions(-) diff --git a/internal/resources/builder.go b/internal/resources/builder.go index a2af3a0..5f7cc22 100644 --- a/internal/resources/builder.go +++ b/internal/resources/builder.go @@ -24,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" + commonsbatchjob "github.com/keiailab/keiailab-commons/pkg/batchjob" commonshpa "github.com/keiailab/keiailab-commons/pkg/hpa" commonslabels "github.com/keiailab/keiailab-commons/pkg/labels" commonsnp "github.com/keiailab/keiailab-commons/pkg/networkpolicy" @@ -2042,44 +2043,34 @@ func BuildBackupJob(backup *mongodbv1alpha1.MongoDBBackup, authSecretName string // Build backup script script := buildBackupScript(backup) - return &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: backup.Name, - Namespace: backup.Namespace, - Labels: labels, - }, - Spec: batchv1.JobSpec{ - BackoffLimit: &backoff, - TTLSecondsAfterFinished: &ttl, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - RestartPolicy: corev1.RestartPolicyOnFailure, - Containers: []corev1.Container{ - { - Name: "backup", - Image: defaultImage, - Command: []string{"/bin/bash", "-c"}, - Args: []string{script}, - Env: envVars, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("100m"), - corev1.ResourceMemory: resource.MustParse("256Mi"), - }, - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("500m"), - corev1.ResourceMemory: resource.MustParse("1Gi"), - }, - }, - }, + // Job 엔벨로프는 keiailab-commons/pkg/batchjob.Build 에 위임. 컨테이너(mongodump + // 스크립트) 조립은 mongo 도메인 잔류. + return commonsbatchjob.Build(commonsbatchjob.Params{ + Name: backup.Name, + Namespace: backup.Namespace, + Labels: labels, + BackoffLimit: &backoff, + TTLSecondsAfterFinished: &ttl, + Containers: []corev1.Container{ + { + Name: "backup", + Image: defaultImage, + Command: []string{"/bin/bash", "-c"}, + Args: []string{script}, + Env: envVars, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("1Gi"), }, }, }, }, - } + }) } // BuildBackupCronJob creates a CronJob that periodically creates MongoDBBackup CRs. @@ -2216,47 +2207,38 @@ fi mongorestore --uri "${MONGODB_URI}" ${RESTORE_FLAGS} echo "[restore] completed" ` - return &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: backup.Name + "-restore", - Namespace: backup.Namespace, - Labels: labels, + // Job 엔벨로프는 keiailab-commons/pkg/batchjob.Build 에 위임. 컨테이너(mongorestore)/볼륨 잔류. + return commonsbatchjob.Build(commonsbatchjob.Params{ + Name: backup.Name + "-restore", + Namespace: backup.Namespace, + Labels: labels, + BackoffLimit: &backoff, + TTLSecondsAfterFinished: &ttl, + Containers: []corev1.Container{ + { + Name: "mongorestore", + Image: getMongoDBImage(mongodbv1alpha1.MongoDBVersion{Version: "8.2"}), + Command: []string{"sh", "-c", script}, + Env: envVars, + VolumeMounts: []corev1.VolumeMount{ + {Name: "source", MountPath: "/data/source", ReadOnly: true}, + }, + SecurityContext: buildDefaultContainerSecurityContext(), + }, }, - Spec: batchv1.JobSpec{ - BackoffLimit: &backoff, - TTLSecondsAfterFinished: &ttl, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{Labels: labels}, - Spec: corev1.PodSpec{ - RestartPolicy: corev1.RestartPolicyOnFailure, - Containers: []corev1.Container{ - { - Name: "mongorestore", - Image: getMongoDBImage(mongodbv1alpha1.MongoDBVersion{Version: "8.2"}), - Command: []string{"sh", "-c", script}, - Env: envVars, - VolumeMounts: []corev1.VolumeMount{ - {Name: "source", MountPath: "/data/source", ReadOnly: true}, - }, - SecurityContext: buildDefaultContainerSecurityContext(), - }, - }, - Volumes: []corev1.Volume{ - { - Name: "source", - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: backup.Spec.Restore.SourceBackupName, - ReadOnly: true, - }, - }, - }, + Volumes: []corev1.Volume{ + { + Name: "source", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: backup.Spec.Restore.SourceBackupName, + ReadOnly: true, }, - SecurityContext: buildDefaultSecurityContext(), }, }, }, - }, nil + PodSecurityContext: buildDefaultSecurityContext(), + }), nil } // BuildMongoDBPDB는 MongoDB ReplicaSet workload를 위한 PodDisruptionBudget을 생성한다. From 7e2d35ba03b11467116862271f5a1f31fcd7ce73 Mon Sep 17 00:00:00 2001 From: phil Date: Tue, 30 Jun 2026 10:51:43 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore(deps):=20keiailab-commons=20v0.13.0?= =?UTF-8?q?=20(pseudo=20=E2=86=92=20release=20tag)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commons#86 머지 + v0.13.0 릴리스 후 clean tag 로 고정. Co-Authored-By: Claude Opus 4.8 Signed-off-by: phil --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2793b16..122314c 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.26.4 require ( // TODO(#236): keiailab-commons v1.0.0 안정 릴리스 시 업그레이드 예정. // 현재 v0.10.0 — API 변경 가능성 잔존. - github.com/keiailab/keiailab-commons v0.12.1-0.20260630011743-a54c4a826028 + github.com/keiailab/keiailab-commons v0.13.0 github.com/onsi/ginkgo/v2 v2.29.0 github.com/onsi/gomega v1.41.0 github.com/prometheus/client_golang v1.23.2 diff --git a/go.sum b/go.sum index 700228a..6a9b60e 100644 --- a/go.sum +++ b/go.sum @@ -109,6 +109,8 @@ github.com/keiailab/keiailab-commons v0.12.1-0.20260630004504-13e0976fc111 h1:9e 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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=