diff --git a/internal/resources/builder.go b/internal/resources/builder.go index e3ac87e..b8d95d9 100644 --- a/internal/resources/builder.go +++ b/internal/resources/builder.go @@ -14,21 +14,13 @@ import ( "time" appsv1 "k8s.io/api/apps/v1" - autoscalingv2 "k8s.io/api/autoscaling/v2" - batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" - networkingv1 "k8s.io/api/networking/v1" - policyv1 "k8s.io/api/policy/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "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" - 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" @@ -59,6 +51,10 @@ const ( scriptReadiness = "readiness-probe.sh" scriptBootstrap = "bootstrap-admin.sh" scriptStepDown = "prestop-stepdown.sh" + + // envMongoDBURI — backup/restore Job 의 MongoDB 연결 URI env 이름 (RS/sharded + // StatefulSet + backup/restore Job 공용 — goconst SSOT). + envMongoDBURI = "MONGODB_URI" ) // Helper functions @@ -735,7 +731,7 @@ func BuildReplicaSetStatefulSet(mdb *mongodbv1alpha1.MongoDB) *appsv1.StatefulSe }, Env: []corev1.EnvVar{ { - Name: "MONGODB_URI", + Name: envMongoDBURI, ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ @@ -1818,7 +1814,7 @@ func BuildMongosDeployment(mdbsh *mongodbv1alpha1.MongoDBSharded) *appsv1.Deploy Args: []string{"--collect-all", "--compatible-mode"}, Env: []corev1.EnvVar{ { - Name: "MONGODB_URI", + Name: envMongoDBURI, ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ @@ -1964,578 +1960,6 @@ func buildMongosVolumes(mdbsh *mongodbv1alpha1.MongoDBSharded) []corev1.Volume { return volumes } -// BuildBackupJob creates a Job for MongoDB backup -func BuildBackupJob(backup *mongodbv1alpha1.MongoDBBackup, authSecretName string) *batchv1.Job { - labels := buildLabels(backup.Name, "backup") - - backoff := int32(3) - ttl := int32(86400) // 24 hours - - var envVars []corev1.EnvVar - envVars = append(envVars, corev1.EnvVar{ - Name: "MONGODB_URI", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: authSecretName, - }, - Key: "connectionString", - }, - }, - }) - - // S3 storage configuration - if backup.Spec.Storage.Type == "s3" && backup.Spec.Storage.S3 != nil { - s3 := backup.Spec.Storage.S3 - envVars = append(envVars, - corev1.EnvVar{Name: "S3_BUCKET", Value: s3.Bucket}, - corev1.EnvVar{Name: "S3_ENDPOINT", Value: s3.Endpoint}, - corev1.EnvVar{Name: "S3_REGION", Value: s3.Region}, - corev1.EnvVar{Name: "S3_PREFIX", Value: s3.Prefix}, - corev1.EnvVar{ - Name: "AWS_ACCESS_KEY_ID", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: s3.CredentialsRef, - Key: "access-key", - }, - }, - }, - corev1.EnvVar{ - Name: "AWS_SECRET_ACCESS_KEY", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: s3.CredentialsRef, - Key: "secret-key", - }, - }, - }, - ) - } - - // Build backup script - script := buildBackupScript(backup) - - // 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. -func BuildBackupCronJob(clusterName, namespace, schedule, clusterKind string, backupSpec mongodbv1alpha1.BackupSpec) *batchv1.CronJob { - labels := buildLabels(clusterName, "backup-scheduler") - historyLimit := int32(3) - failedLimit := int32(1) - - backupName := fmt.Sprintf("%s-scheduled-$(date +%%Y%%m%%d-%%H%%M%%S)", clusterName) - storageType := "pvc" - if backupSpec.Storage.Type != "" { - storageType = backupSpec.Storage.Type - } - - script := fmt.Sprintf(`#!/bin/sh -set -e -BACKUP_NAME="%s-scheduled-$(date +%%Y%%m%%d-%%H%%M%%S)" -cat < --archive= [--oplogReplay] 실행 -// 3. PointInTime 이 설정되면 --oplogLimit 추가 (PITR) -// -// 본 cycle 의 acceptance: Job 객체 생성 + controller 가 spawn. 실제 oplog -// archive 의 S3 fetch + mongorestore 실행 정합은 cycle 16 운영 강화 시점. -func BuildRestoreJob(backup *mongodbv1alpha1.MongoDBBackup, authSecretName string) (*batchv1.Job, error) { - // 결함 #1: 일반 백업 (Spec.Restore=nil) verify 시 nil 역참조 panic 차단. - // 본 함수는 restore 작업 (Spec.Restore != nil) 에 대해서만 호출되어야 한다. - if backup.Spec.Restore == nil { - return nil, fmt.Errorf("backup %s: Spec.Restore is nil", backup.Name) - } - labels := buildLabels(backup.Name, "restore") - backoff := int32(3) - ttl := int32(86400) - - envVars := []corev1.EnvVar{ - {Name: "MONGODB_URI", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: authSecretName}, Key: "connectionString"}}}, - {Name: "SOURCE_BACKUP", Value: backup.Spec.Restore.SourceBackupName}, - } - if backup.Spec.Restore.PointInTime != nil { - envVars = append(envVars, corev1.EnvVar{ - Name: "POINT_IN_TIME", - Value: backup.Spec.Restore.PointInTime.Format("2006-01-02T15:04:05Z"), - }) - } - - // Restore script — mongorestore + --oplogReplay --oplogLimit - script := `set -eu -echo "[restore] source=${SOURCE_BACKUP} pit=${POINT_IN_TIME:-none}" -RESTORE_FLAGS="--archive=/data/source/dump.archive --gzip --drop" -if [ -n "${POINT_IN_TIME:-}" ]; then - EPOCH=$(date -u -d "${POINT_IN_TIME}" +%s) - RESTORE_FLAGS="${RESTORE_FLAGS} --oplogReplay --oplogLimit=${EPOCH}:0" -fi -mongorestore --uri "${MONGODB_URI}" ${RESTORE_FLAGS} -echo "[restore] completed" -` - // 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(), - }, - }, - Volumes: []corev1.Volume{ - { - Name: "source", - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: backup.Spec.Restore.SourceBackupName, - ReadOnly: true, - }, - }, - }, - }, - PodSecurityContext: buildDefaultSecurityContext(), - }), nil -} - -// BuildMongoDBPDB는 MongoDB ReplicaSet workload를 위한 PodDisruptionBudget을 생성한다. -// spec.podDisruptionBudget가 nil이거나 enabled=false면 nil을 반환해 controller가 -// 생성/업데이트를 skip하게 한다. -// -// 기본값: minAvailable = max(replicas-1, 0). 3 멤버 RS면 minAvailable=2가 -// 적용되어 한 번에 한 멤버만 maintenance 가능하도록 보장. -func BuildMongoDBPDB(mdb *mongodbv1alpha1.MongoDB) *policyv1.PodDisruptionBudget { - pdbSpec := mdb.Spec.PodDisruptionBudget - if pdbSpec == nil || !pdbSpec.Enabled { - return nil - } - 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 -// NetworkPolicy를 생성한다. spec.networkPolicy.enabled=false면 nil 반환. -// -// 기본 정책: 같은 RS의 pods간 27017 ingress만 허용. AdditionalIngressFrom으로 -// 운영 namespace, 모니터링 stack(exporter scrape) 등을 추가 ingress로 명시 가능. -// -// iteration 26 (2026-05-07): keiailab-commons/pkg/networkpolicy v0.3.0 위임. -// 인라인 builder → commons.New + WithSelfIngress + WithIngressFromPeers. -// valkey iteration 25 (97162b5) 패턴 차용. semantic equivalence 보존. -func BuildMongoDBNetworkPolicy(mdb *mongodbv1alpha1.MongoDB) *networkingv1.NetworkPolicy { - npSpec := mdb.Spec.NetworkPolicy - if npSpec == nil || !npSpec.Enabled { - return nil - } - selector := buildLabels(mdb.Name, "replicaset") - - opts := []commonsnp.Option{ - commonsnp.WithLabels(selector), - commonsnp.WithSelfIngress([]int32{int32(mongoDBPort)}), - } - if extra := convertAdditionalPeers(npSpec.AdditionalIngressFrom); len(extra) > 0 { - opts = append(opts, commonsnp.WithIngressFromPeers(extra, []int32{int32(mongoDBPort)})) - } - return commonsnp.New(mdb.Name+"-netpol", mdb.Namespace, selector, opts...) -} - -// convertAdditionalPeers — mongodbv1alpha1.NetworkPolicyPeer → commons Peer. -// PodSelector / NamespaceSelector 둘 다 nil 인 entry 는 skip (기존 동작 보존). -func convertAdditionalPeers(in []mongodbv1alpha1.NetworkPolicyPeer) []commonsnp.Peer { - out := make([]commonsnp.Peer, 0, len(in)) - for _, peer := range in { - if peer.PodSelector == nil && peer.NamespaceSelector == nil { - continue - } - p := commonsnp.Peer{} - if peer.PodSelector != nil { - p.PodSelector = *peer.PodSelector - } - if peer.NamespaceSelector != nil { - p.NamespaceSelector = *peer.NamespaceSelector - } - out = append(out, p) - } - return out -} - -// pdbBaseSpec은 PodDisruptionBudgetSpec를 받아 기본 minAvailable=replicas-1을 -// 적용한 K8s PDB spec을 만든다(MinAvailable/MaxUnavailable 미지정 시). -func pdbBaseSpec(spec *mongodbv1alpha1.PodDisruptionBudgetSpec, replicas int32, selector map[string]string) policyv1.PodDisruptionBudgetSpec { - // 빌드 골격(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 각 컴포넌트에 대한 -// PodDisruptionBudget 슬라이스를 반환한다. Spec.PodDisruptionBudget이 nil이거나 -// disabled면 nil 슬라이스 반환. -func BuildShardedPDBs(mdbsh *mongodbv1alpha1.MongoDBSharded) []*policyv1.PodDisruptionBudget { - pdbSpec := mdbsh.Spec.PodDisruptionBudget - if pdbSpec == nil || !pdbSpec.Enabled { - return nil - } - var out []*policyv1.PodDisruptionBudget - - // Config server - cfgSelector := buildLabels(mdbsh.Name, "configsvr") - out = append(out, &policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Name: mdbsh.Name + "-cfg-pdb", - Namespace: mdbsh.Namespace, - Labels: cfgSelector, - }, - Spec: pdbBaseSpec(pdbSpec, mdbsh.Spec.ConfigServer.Members, cfgSelector), - }) - - // Shards (각 shard별) - for i := int32(0); i < mdbsh.Spec.Shards.Count; i++ { - shSelector := buildLabels(mdbsh.Name, fmt.Sprintf("shard-%d", i)) - out = append(out, &policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-shard-%d-pdb", mdbsh.Name, i), - Namespace: mdbsh.Namespace, - Labels: shSelector, - }, - Spec: pdbBaseSpec(pdbSpec, mdbsh.Spec.Shards.MembersPerShard, shSelector), - }) - } - - // Mongos - mongosSelector := buildLabels(mdbsh.Name, "mongos") - out = append(out, &policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Name: mdbsh.Name + "-mongos-pdb", - Namespace: mdbsh.Namespace, - Labels: mongosSelector, - }, - Spec: pdbBaseSpec(pdbSpec, mdbsh.Spec.Mongos.Replicas, mongosSelector), - }) - - return out -} - -// buildShardedComponentNetworkPolicy은 cfg/shard/mongos 컴포넌트별 NetworkPolicy -// 빌더의 공용 helper. selector(컴포넌트 라벨)와 port(cfg=27019/shard=27018/ -// mongos=27017)를 매개변수화한다. -// -// iteration 26: commons.New 위임. self-peer 가 *cluster-wide* (instance + managed-by -// 만 매칭, component 무관) — cfg ↔ shard ↔ mongos 통신 허용 패턴 — WithIngressFromPeers -// 의 cluster-wide Peer 로 표현. -func buildShardedComponentNetworkPolicy(mdbsh *mongodbv1alpha1.MongoDBSharded, name string, port int, selector map[string]string) *networkingv1.NetworkPolicy { - npSpec := mdbsh.Spec.NetworkPolicy - if npSpec == nil || !npSpec.Enabled { - return nil - } - ports := []int32{int32(port)} - clusterPeer := commonsnp.Peer{ - PodSelector: map[string]string{ - "app.kubernetes.io/instance": mdbsh.Name, - "app.kubernetes.io/managed-by": "mongodb-operator", - }, - } - opts := []commonsnp.Option{ - commonsnp.WithLabels(selector), - commonsnp.WithIngressFromPeers([]commonsnp.Peer{clusterPeer}, ports), - } - if extra := convertAdditionalPeers(npSpec.AdditionalIngressFrom); len(extra) > 0 { - opts = append(opts, commonsnp.WithIngressFromPeers(extra, ports)) - } - return commonsnp.New(name, mdbsh.Namespace, selector, opts...) -} - -// BuildShardedNetworkPolicies는 cfg/shards/mongos 각 컴포넌트에 대한 NetworkPolicy -// 슬라이스를 반환한다. spec.networkPolicy.enabled=false면 nil 반환. -func BuildShardedNetworkPolicies(mdbsh *mongodbv1alpha1.MongoDBSharded) []*networkingv1.NetworkPolicy { - npSpec := mdbsh.Spec.NetworkPolicy - if npSpec == nil || !npSpec.Enabled { - return nil - } - var out []*networkingv1.NetworkPolicy - out = append(out, buildShardedComponentNetworkPolicy(mdbsh, mdbsh.Name+"-cfg-netpol", 27019, - buildLabels(mdbsh.Name, "configsvr"))) - for i := int32(0); i < mdbsh.Spec.Shards.Count; i++ { - out = append(out, buildShardedComponentNetworkPolicy(mdbsh, - fmt.Sprintf("%s-shard-%d-netpol", mdbsh.Name, i), 27018, - buildLabels(mdbsh.Name, fmt.Sprintf("shard-%d", i)))) - } - out = append(out, buildShardedComponentNetworkPolicy(mdbsh, mdbsh.Name+"-mongos-netpol", 27017, - buildLabels(mdbsh.Name, "mongos"))) - return out -} - -// BuildMongosHPA는 mongos Deployment에 대한 HorizontalPodAutoscaler를 만든다. -// `Spec.Mongos.AutoScaling.Enabled=false`이면 nil(호출자가 기존 HPA 삭제 처리). -// mongos는 stateless router라 deliberate 가드가 불필요(ADR-0007). -func BuildMongosHPA(mdbsh *mongodbv1alpha1.MongoDBSharded) *autoscalingv2.HorizontalPodAutoscaler { - if !IsMongosHPAActive(mdbsh) { - return nil - } - return buildHPAForTarget( - mdbsh.Name+"-mongos-hpa", mdbsh.Namespace, buildLabels(mdbsh.Name, "mongos"), - "Deployment", mdbsh.Name+"-mongos", - mdbsh.Spec.Mongos.AutoScaling, - ) -} - -// BuildReplicaSetHPA는 ReplicaSet StatefulSet에 대한 HPA를 만든다. -// `Spec.AutoScaling.Enabled=true` + `Spec.ScalePolicy.Deliberate=true` *둘 다* -// 일 때만 nil 아닌 HPA 반환(이중 가드, ADR-0008). -// -// 이중 가드 근거 — RS 멤버 수 변경은 RS reconfig + initial sync 부작용 동반. -// HPA controller가 metric 변동에 따라 Replicas를 자동 patch하면 운영자 모르는 -// 사이 RS reconfig가 발동될 수 있어, "운영자 의도된 자동화"임을 명시(deliberate) -// 해야만 활성화한다. -func BuildReplicaSetHPA(mdb *mongodbv1alpha1.MongoDB) *autoscalingv2.HorizontalPodAutoscaler { - if !IsRSHPAActive(mdb) { - return nil - } - return buildHPAForTarget( - mdb.Name+"-hpa", mdb.Namespace, buildLabels(mdb.Name, "replicaset"), - "StatefulSet", mdb.Name, - mdb.Spec.AutoScaling, - ) -} - -// BuildConfigServerHPA는 cfg StatefulSet에 대한 HPA를 만든다. cfg는 보통 작은 -// 멤버 수(3-7)이고 변동이 거의 없어 *deliberate 이중 가드*를 동일하게 적용한다 -// (ADR-0009). -func BuildConfigServerHPA(mdbsh *mongodbv1alpha1.MongoDBSharded) *autoscalingv2.HorizontalPodAutoscaler { - if !IsConfigServerHPAActive(mdbsh) { - return nil - } - return buildHPAForTarget( - mdbsh.Name+"-cfg-hpa", mdbsh.Namespace, buildLabels(mdbsh.Name, "configsvr"), - "StatefulSet", mdbsh.Name+"-cfg", - mdbsh.Spec.ConfigServer.AutoScaling, - ) -} - -// buildHPAForTarget는 HPA 객체 생성 공통 로직(가드 검사는 호출자 책임). -func buildHPAForTarget(name, namespace string, labels map[string]string, kind, refName string, - as *mongodbv1alpha1.AutoScalingSpec, -) *autoscalingv2.HorizontalPodAutoscaler { - // 빌드 골격(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에서 활성 상태인지 검사한다(이중 가드). -// applyStatefulSet의 HPA-aware preserve 분기에서 사용. -func IsRSHPAActive(mdb *mongodbv1alpha1.MongoDB) bool { - return mdb.Spec.AutoScaling != nil && mdb.Spec.AutoScaling.Enabled && - mdb.Spec.ScalePolicy != nil && mdb.Spec.ScalePolicy.Deliberate -} - -// IsConfigServerHPAActive는 cfg HPA의 이중 가드 통과 여부를 검사한다. -func IsConfigServerHPAActive(mdbsh *mongodbv1alpha1.MongoDBSharded) bool { - return mdbsh.Spec.ConfigServer.AutoScaling != nil && mdbsh.Spec.ConfigServer.AutoScaling.Enabled && - mdbsh.Spec.ConfigServer.ScalePolicy != nil && mdbsh.Spec.ConfigServer.ScalePolicy.Deliberate -} - -// IsMongosHPAActive는 mongos HPA의 활성 상태(단일 가드: enabled). -func IsMongosHPAActive(mdbsh *mongodbv1alpha1.MongoDBSharded) bool { - return mdbsh.Spec.Mongos.AutoScaling != nil && mdbsh.Spec.Mongos.AutoScaling.Enabled -} - -// IsRSScaleDeliberate는 ReplicaSet 멤버 수 변경이 의도된 자동화임을 검사한다. -// Spec.ScalePolicy=nil이면 default false(즉시 변경 금지). true이면 spec.Members -// 변경이 즉시 STS replicas로 반영된다(ADR-0008). -func IsRSScaleDeliberate(mdb *mongodbv1alpha1.MongoDB) bool { - return mdb.Spec.ScalePolicy != nil && mdb.Spec.ScalePolicy.Deliberate -} - -// IsConfigServerScaleDeliberate는 cfg 멤버 수 변경 가드(ADR-0008/0009). -func IsConfigServerScaleDeliberate(mdbsh *mongodbv1alpha1.MongoDBSharded) bool { - return mdbsh.Spec.ConfigServer.ScalePolicy != nil && mdbsh.Spec.ConfigServer.ScalePolicy.Deliberate -} - -// IsShardScaleDeliberate는 shard MembersPerShard 변경 가드(ADR-0008/0009). -func IsShardScaleDeliberate(mdbsh *mongodbv1alpha1.MongoDBSharded) bool { - return mdbsh.Spec.Shards.ScalePolicy != nil && mdbsh.Spec.Shards.ScalePolicy.Deliberate -} - -// buildHPAMetrics는 spec의 metric 목록을 autoscaling/v2 metric으로 변환한다. -// 알 수 없는 type은 silently skip하지 않고(운영자 진단성을 위해) caller가 spec -// validation에서 차단해야 한다 — 본 함수는 변환 책임만. -func buildHPAMetrics(metrics []mongodbv1alpha1.AutoScalingMetric) []autoscalingv2.MetricSpec { - if len(metrics) == 0 { - // 아무 metric도 없으면 cpu 80% 기본값 — upstream chart 등 표준 기본. - 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, commonshpa.CPUUtilization(target)) - case "memory": - out = append(out, commonshpa.MemoryUtilization(target)) - case "custom": - if m.CustomMetric == nil || m.CustomMetric.Name == "" { - continue - } - q := resource.NewQuantity(int64(target), resource.DecimalSI) - out = append(out, autoscalingv2.MetricSpec{ - Type: autoscalingv2.PodsMetricSourceType, - Pods: &autoscalingv2.PodsMetricSource{ - Metric: autoscalingv2.MetricIdentifier{Name: m.CustomMetric.Name}, - Target: autoscalingv2.MetricTarget{ - Type: autoscalingv2.AverageValueMetricType, - AverageValue: q, - }, - }, - }) - } - } - return out -} - // BuildQueryableStatefulSet creates a single-member read-only StatefulSet // for queryable backup access. func BuildQueryableStatefulSet(backup *mongodbv1alpha1.MongoDBBackup, spec *mongodbv1alpha1.QueryableBackupSpec) *appsv1.StatefulSet { diff --git a/internal/resources/builder_backup.go b/internal/resources/builder_backup.go new file mode 100644 index 0000000..fe1125f --- /dev/null +++ b/internal/resources/builder_backup.go @@ -0,0 +1,271 @@ +/* +Copyright 2024 Keiailab. + +Licensed under the MIT License. See the LICENSE file for details. +*/ + +package resources + +import ( + "fmt" + + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + commonsbatchjob "github.com/keiailab/keiailab-commons/pkg/batchjob" + + mongodbv1alpha1 "github.com/keiailab/mongodb-operator/api/v1alpha1" + "github.com/keiailab/mongodb-operator/internal/assets" +) + +// BuildBackupJob creates a Job for MongoDB backup +func BuildBackupJob(backup *mongodbv1alpha1.MongoDBBackup, authSecretName string) *batchv1.Job { + labels := buildLabels(backup.Name, "backup") + + backoff := int32(3) + ttl := int32(86400) // 24 hours + + var envVars []corev1.EnvVar + envVars = append(envVars, corev1.EnvVar{ + Name: envMongoDBURI, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: authSecretName, + }, + Key: "connectionString", + }, + }, + }) + + // S3 storage configuration + if backup.Spec.Storage.Type == "s3" && backup.Spec.Storage.S3 != nil { + s3 := backup.Spec.Storage.S3 + envVars = append(envVars, + corev1.EnvVar{Name: "S3_BUCKET", Value: s3.Bucket}, + corev1.EnvVar{Name: "S3_ENDPOINT", Value: s3.Endpoint}, + corev1.EnvVar{Name: "S3_REGION", Value: s3.Region}, + corev1.EnvVar{Name: "S3_PREFIX", Value: s3.Prefix}, + corev1.EnvVar{ + Name: "AWS_ACCESS_KEY_ID", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: s3.CredentialsRef, + Key: "access-key", + }, + }, + }, + corev1.EnvVar{ + Name: "AWS_SECRET_ACCESS_KEY", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: s3.CredentialsRef, + Key: "secret-key", + }, + }, + }, + ) + } + + // Build backup script + script := buildBackupScript(backup) + + // 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. +func BuildBackupCronJob(clusterName, namespace, schedule, clusterKind string, backupSpec mongodbv1alpha1.BackupSpec) *batchv1.CronJob { + labels := buildLabels(clusterName, "backup-scheduler") + historyLimit := int32(3) + failedLimit := int32(1) + + backupName := fmt.Sprintf("%s-scheduled-$(date +%%Y%%m%%d-%%H%%M%%S)", clusterName) + storageType := "pvc" + if backupSpec.Storage.Type != "" { + storageType = backupSpec.Storage.Type + } + + script := fmt.Sprintf(`#!/bin/sh +set -e +BACKUP_NAME="%s-scheduled-$(date +%%Y%%m%%d-%%H%%M%%S)" +cat < --archive= [--oplogReplay] 실행 +// 3. PointInTime 이 설정되면 --oplogLimit 추가 (PITR) +// +// 본 cycle 의 acceptance: Job 객체 생성 + controller 가 spawn. 실제 oplog +// archive 의 S3 fetch + mongorestore 실행 정합은 cycle 16 운영 강화 시점. +func BuildRestoreJob(backup *mongodbv1alpha1.MongoDBBackup, authSecretName string) (*batchv1.Job, error) { + // 결함 #1: 일반 백업 (Spec.Restore=nil) verify 시 nil 역참조 panic 차단. + // 본 함수는 restore 작업 (Spec.Restore != nil) 에 대해서만 호출되어야 한다. + if backup.Spec.Restore == nil { + return nil, fmt.Errorf("backup %s: Spec.Restore is nil", backup.Name) + } + labels := buildLabels(backup.Name, "restore") + backoff := int32(3) + ttl := int32(86400) + + envVars := []corev1.EnvVar{ + {Name: envMongoDBURI, ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: authSecretName}, Key: "connectionString"}}}, + {Name: "SOURCE_BACKUP", Value: backup.Spec.Restore.SourceBackupName}, + } + if backup.Spec.Restore.PointInTime != nil { + envVars = append(envVars, corev1.EnvVar{ + Name: "POINT_IN_TIME", + Value: backup.Spec.Restore.PointInTime.Format("2006-01-02T15:04:05Z"), + }) + } + + // Restore script — mongorestore + --oplogReplay --oplogLimit + script := `set -eu +echo "[restore] source=${SOURCE_BACKUP} pit=${POINT_IN_TIME:-none}" +RESTORE_FLAGS="--archive=/data/source/dump.archive --gzip --drop" +if [ -n "${POINT_IN_TIME:-}" ]; then + EPOCH=$(date -u -d "${POINT_IN_TIME}" +%s) + RESTORE_FLAGS="${RESTORE_FLAGS} --oplogReplay --oplogLimit=${EPOCH}:0" +fi +mongorestore --uri "${MONGODB_URI}" ${RESTORE_FLAGS} +echo "[restore] completed" +` + // 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(), + }, + }, + Volumes: []corev1.Volume{ + { + Name: "source", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: backup.Spec.Restore.SourceBackupName, + ReadOnly: true, + }, + }, + }, + }, + PodSecurityContext: buildDefaultSecurityContext(), + }), nil +} diff --git a/internal/resources/builder_hpa.go b/internal/resources/builder_hpa.go new file mode 100644 index 0000000..5a01c76 --- /dev/null +++ b/internal/resources/builder_hpa.go @@ -0,0 +1,154 @@ +/* +Copyright 2024 Keiailab. + +Licensed under the MIT License. See the LICENSE file for details. +*/ + +package resources + +import ( + autoscalingv2 "k8s.io/api/autoscaling/v2" + "k8s.io/apimachinery/pkg/api/resource" + + commonshpa "github.com/keiailab/keiailab-commons/pkg/hpa" + + mongodbv1alpha1 "github.com/keiailab/mongodb-operator/api/v1alpha1" +) + +// BuildMongosHPA는 mongos Deployment에 대한 HorizontalPodAutoscaler를 만든다. +// `Spec.Mongos.AutoScaling.Enabled=false`이면 nil(호출자가 기존 HPA 삭제 처리). +// mongos는 stateless router라 deliberate 가드가 불필요(ADR-0007). +func BuildMongosHPA(mdbsh *mongodbv1alpha1.MongoDBSharded) *autoscalingv2.HorizontalPodAutoscaler { + if !IsMongosHPAActive(mdbsh) { + return nil + } + return buildHPAForTarget( + mdbsh.Name+"-mongos-hpa", mdbsh.Namespace, buildLabels(mdbsh.Name, "mongos"), + "Deployment", mdbsh.Name+"-mongos", + mdbsh.Spec.Mongos.AutoScaling, + ) +} + +// BuildReplicaSetHPA는 ReplicaSet StatefulSet에 대한 HPA를 만든다. +// `Spec.AutoScaling.Enabled=true` + `Spec.ScalePolicy.Deliberate=true` *둘 다* +// 일 때만 nil 아닌 HPA 반환(이중 가드, ADR-0008). +// +// 이중 가드 근거 — RS 멤버 수 변경은 RS reconfig + initial sync 부작용 동반. +// HPA controller가 metric 변동에 따라 Replicas를 자동 patch하면 운영자 모르는 +// 사이 RS reconfig가 발동될 수 있어, "운영자 의도된 자동화"임을 명시(deliberate) +// 해야만 활성화한다. +func BuildReplicaSetHPA(mdb *mongodbv1alpha1.MongoDB) *autoscalingv2.HorizontalPodAutoscaler { + if !IsRSHPAActive(mdb) { + return nil + } + return buildHPAForTarget( + mdb.Name+"-hpa", mdb.Namespace, buildLabels(mdb.Name, "replicaset"), + "StatefulSet", mdb.Name, + mdb.Spec.AutoScaling, + ) +} + +// BuildConfigServerHPA는 cfg StatefulSet에 대한 HPA를 만든다. cfg는 보통 작은 +// 멤버 수(3-7)이고 변동이 거의 없어 *deliberate 이중 가드*를 동일하게 적용한다 +// (ADR-0009). +func BuildConfigServerHPA(mdbsh *mongodbv1alpha1.MongoDBSharded) *autoscalingv2.HorizontalPodAutoscaler { + if !IsConfigServerHPAActive(mdbsh) { + return nil + } + return buildHPAForTarget( + mdbsh.Name+"-cfg-hpa", mdbsh.Namespace, buildLabels(mdbsh.Name, "configsvr"), + "StatefulSet", mdbsh.Name+"-cfg", + mdbsh.Spec.ConfigServer.AutoScaling, + ) +} + +// buildHPAForTarget는 HPA 객체 생성 공통 로직(가드 검사는 호출자 책임). +func buildHPAForTarget(name, namespace string, labels map[string]string, kind, refName string, + as *mongodbv1alpha1.AutoScalingSpec, +) *autoscalingv2.HorizontalPodAutoscaler { + // 빌드 골격(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에서 활성 상태인지 검사한다(이중 가드). +// applyStatefulSet의 HPA-aware preserve 분기에서 사용. +func IsRSHPAActive(mdb *mongodbv1alpha1.MongoDB) bool { + return mdb.Spec.AutoScaling != nil && mdb.Spec.AutoScaling.Enabled && + mdb.Spec.ScalePolicy != nil && mdb.Spec.ScalePolicy.Deliberate +} + +// IsConfigServerHPAActive는 cfg HPA의 이중 가드 통과 여부를 검사한다. +func IsConfigServerHPAActive(mdbsh *mongodbv1alpha1.MongoDBSharded) bool { + return mdbsh.Spec.ConfigServer.AutoScaling != nil && mdbsh.Spec.ConfigServer.AutoScaling.Enabled && + mdbsh.Spec.ConfigServer.ScalePolicy != nil && mdbsh.Spec.ConfigServer.ScalePolicy.Deliberate +} + +// IsMongosHPAActive는 mongos HPA의 활성 상태(단일 가드: enabled). +func IsMongosHPAActive(mdbsh *mongodbv1alpha1.MongoDBSharded) bool { + return mdbsh.Spec.Mongos.AutoScaling != nil && mdbsh.Spec.Mongos.AutoScaling.Enabled +} + +// IsRSScaleDeliberate는 ReplicaSet 멤버 수 변경이 의도된 자동화임을 검사한다. +// Spec.ScalePolicy=nil이면 default false(즉시 변경 금지). true이면 spec.Members +// 변경이 즉시 STS replicas로 반영된다(ADR-0008). +func IsRSScaleDeliberate(mdb *mongodbv1alpha1.MongoDB) bool { + return mdb.Spec.ScalePolicy != nil && mdb.Spec.ScalePolicy.Deliberate +} + +// IsConfigServerScaleDeliberate는 cfg 멤버 수 변경 가드(ADR-0008/0009). +func IsConfigServerScaleDeliberate(mdbsh *mongodbv1alpha1.MongoDBSharded) bool { + return mdbsh.Spec.ConfigServer.ScalePolicy != nil && mdbsh.Spec.ConfigServer.ScalePolicy.Deliberate +} + +// IsShardScaleDeliberate는 shard MembersPerShard 변경 가드(ADR-0008/0009). +func IsShardScaleDeliberate(mdbsh *mongodbv1alpha1.MongoDBSharded) bool { + return mdbsh.Spec.Shards.ScalePolicy != nil && mdbsh.Spec.Shards.ScalePolicy.Deliberate +} + +// buildHPAMetrics는 spec의 metric 목록을 autoscaling/v2 metric으로 변환한다. +// 알 수 없는 type은 silently skip하지 않고(운영자 진단성을 위해) caller가 spec +// validation에서 차단해야 한다 — 본 함수는 변환 책임만. +func buildHPAMetrics(metrics []mongodbv1alpha1.AutoScalingMetric) []autoscalingv2.MetricSpec { + if len(metrics) == 0 { + // 아무 metric도 없으면 cpu 80% 기본값 — upstream chart 등 표준 기본. + 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, commonshpa.CPUUtilization(target)) + case "memory": + out = append(out, commonshpa.MemoryUtilization(target)) + case "custom": + if m.CustomMetric == nil || m.CustomMetric.Name == "" { + continue + } + q := resource.NewQuantity(int64(target), resource.DecimalSI) + out = append(out, autoscalingv2.MetricSpec{ + Type: autoscalingv2.PodsMetricSourceType, + Pods: &autoscalingv2.PodsMetricSource{ + Metric: autoscalingv2.MetricIdentifier{Name: m.CustomMetric.Name}, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.AverageValueMetricType, + AverageValue: q, + }, + }, + }) + } + } + return out +} diff --git a/internal/resources/builder_policy.go b/internal/resources/builder_policy.go new file mode 100644 index 0000000..a2958fa --- /dev/null +++ b/internal/resources/builder_policy.go @@ -0,0 +1,204 @@ +/* +Copyright 2024 Keiailab. + +Licensed under the MIT License. See the LICENSE file for details. +*/ + +package resources + +import ( + "fmt" + + networkingv1 "k8s.io/api/networking/v1" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + commonsnp "github.com/keiailab/keiailab-commons/pkg/networkpolicy" + commonspdb "github.com/keiailab/keiailab-commons/pkg/pdb" + + mongodbv1alpha1 "github.com/keiailab/mongodb-operator/api/v1alpha1" +) + +// BuildMongoDBPDB는 MongoDB ReplicaSet workload를 위한 PodDisruptionBudget을 생성한다. +// spec.podDisruptionBudget가 nil이거나 enabled=false면 nil을 반환해 controller가 +// 생성/업데이트를 skip하게 한다. +// +// 기본값: minAvailable = max(replicas-1, 0). 3 멤버 RS면 minAvailable=2가 +// 적용되어 한 번에 한 멤버만 maintenance 가능하도록 보장. +func BuildMongoDBPDB(mdb *mongodbv1alpha1.MongoDB) *policyv1.PodDisruptionBudget { + pdbSpec := mdb.Spec.PodDisruptionBudget + if pdbSpec == nil || !pdbSpec.Enabled { + return nil + } + 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 +// NetworkPolicy를 생성한다. spec.networkPolicy.enabled=false면 nil 반환. +// +// 기본 정책: 같은 RS의 pods간 27017 ingress만 허용. AdditionalIngressFrom으로 +// 운영 namespace, 모니터링 stack(exporter scrape) 등을 추가 ingress로 명시 가능. +// +// iteration 26 (2026-05-07): keiailab-commons/pkg/networkpolicy v0.3.0 위임. +// 인라인 builder → commons.New + WithSelfIngress + WithIngressFromPeers. +// valkey iteration 25 (97162b5) 패턴 차용. semantic equivalence 보존. +func BuildMongoDBNetworkPolicy(mdb *mongodbv1alpha1.MongoDB) *networkingv1.NetworkPolicy { + npSpec := mdb.Spec.NetworkPolicy + if npSpec == nil || !npSpec.Enabled { + return nil + } + selector := buildLabels(mdb.Name, "replicaset") + + opts := []commonsnp.Option{ + commonsnp.WithLabels(selector), + commonsnp.WithSelfIngress([]int32{int32(mongoDBPort)}), + } + if extra := convertAdditionalPeers(npSpec.AdditionalIngressFrom); len(extra) > 0 { + opts = append(opts, commonsnp.WithIngressFromPeers(extra, []int32{int32(mongoDBPort)})) + } + return commonsnp.New(mdb.Name+"-netpol", mdb.Namespace, selector, opts...) +} + +// convertAdditionalPeers — mongodbv1alpha1.NetworkPolicyPeer → commons Peer. +// PodSelector / NamespaceSelector 둘 다 nil 인 entry 는 skip (기존 동작 보존). +func convertAdditionalPeers(in []mongodbv1alpha1.NetworkPolicyPeer) []commonsnp.Peer { + out := make([]commonsnp.Peer, 0, len(in)) + for _, peer := range in { + if peer.PodSelector == nil && peer.NamespaceSelector == nil { + continue + } + p := commonsnp.Peer{} + if peer.PodSelector != nil { + p.PodSelector = *peer.PodSelector + } + if peer.NamespaceSelector != nil { + p.NamespaceSelector = *peer.NamespaceSelector + } + out = append(out, p) + } + return out +} + +// pdbBaseSpec은 PodDisruptionBudgetSpec를 받아 기본 minAvailable=replicas-1을 +// 적용한 K8s PDB spec을 만든다(MinAvailable/MaxUnavailable 미지정 시). +func pdbBaseSpec(spec *mongodbv1alpha1.PodDisruptionBudgetSpec, replicas int32, selector map[string]string) policyv1.PodDisruptionBudgetSpec { + // 빌드 골격(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 각 컴포넌트에 대한 +// PodDisruptionBudget 슬라이스를 반환한다. Spec.PodDisruptionBudget이 nil이거나 +// disabled면 nil 슬라이스 반환. +func BuildShardedPDBs(mdbsh *mongodbv1alpha1.MongoDBSharded) []*policyv1.PodDisruptionBudget { + pdbSpec := mdbsh.Spec.PodDisruptionBudget + if pdbSpec == nil || !pdbSpec.Enabled { + return nil + } + var out []*policyv1.PodDisruptionBudget + + // Config server + cfgSelector := buildLabels(mdbsh.Name, "configsvr") + out = append(out, &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: mdbsh.Name + "-cfg-pdb", + Namespace: mdbsh.Namespace, + Labels: cfgSelector, + }, + Spec: pdbBaseSpec(pdbSpec, mdbsh.Spec.ConfigServer.Members, cfgSelector), + }) + + // Shards (각 shard별) + for i := int32(0); i < mdbsh.Spec.Shards.Count; i++ { + shSelector := buildLabels(mdbsh.Name, fmt.Sprintf("shard-%d", i)) + out = append(out, &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-shard-%d-pdb", mdbsh.Name, i), + Namespace: mdbsh.Namespace, + Labels: shSelector, + }, + Spec: pdbBaseSpec(pdbSpec, mdbsh.Spec.Shards.MembersPerShard, shSelector), + }) + } + + // Mongos + mongosSelector := buildLabels(mdbsh.Name, "mongos") + out = append(out, &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: mdbsh.Name + "-mongos-pdb", + Namespace: mdbsh.Namespace, + Labels: mongosSelector, + }, + Spec: pdbBaseSpec(pdbSpec, mdbsh.Spec.Mongos.Replicas, mongosSelector), + }) + + return out +} + +// buildShardedComponentNetworkPolicy은 cfg/shard/mongos 컴포넌트별 NetworkPolicy +// 빌더의 공용 helper. selector(컴포넌트 라벨)와 port(cfg=27019/shard=27018/ +// mongos=27017)를 매개변수화한다. +// +// iteration 26: commons.New 위임. self-peer 가 *cluster-wide* (instance + managed-by +// 만 매칭, component 무관) — cfg ↔ shard ↔ mongos 통신 허용 패턴 — WithIngressFromPeers +// 의 cluster-wide Peer 로 표현. +func buildShardedComponentNetworkPolicy(mdbsh *mongodbv1alpha1.MongoDBSharded, name string, port int, selector map[string]string) *networkingv1.NetworkPolicy { + npSpec := mdbsh.Spec.NetworkPolicy + if npSpec == nil || !npSpec.Enabled { + return nil + } + ports := []int32{int32(port)} + clusterPeer := commonsnp.Peer{ + PodSelector: map[string]string{ + "app.kubernetes.io/instance": mdbsh.Name, + "app.kubernetes.io/managed-by": "mongodb-operator", + }, + } + opts := []commonsnp.Option{ + commonsnp.WithLabels(selector), + commonsnp.WithIngressFromPeers([]commonsnp.Peer{clusterPeer}, ports), + } + if extra := convertAdditionalPeers(npSpec.AdditionalIngressFrom); len(extra) > 0 { + opts = append(opts, commonsnp.WithIngressFromPeers(extra, ports)) + } + return commonsnp.New(name, mdbsh.Namespace, selector, opts...) +} + +// BuildShardedNetworkPolicies는 cfg/shards/mongos 각 컴포넌트에 대한 NetworkPolicy +// 슬라이스를 반환한다. spec.networkPolicy.enabled=false면 nil 반환. +func BuildShardedNetworkPolicies(mdbsh *mongodbv1alpha1.MongoDBSharded) []*networkingv1.NetworkPolicy { + npSpec := mdbsh.Spec.NetworkPolicy + if npSpec == nil || !npSpec.Enabled { + return nil + } + var out []*networkingv1.NetworkPolicy + out = append(out, buildShardedComponentNetworkPolicy(mdbsh, mdbsh.Name+"-cfg-netpol", 27019, + buildLabels(mdbsh.Name, "configsvr"))) + for i := int32(0); i < mdbsh.Spec.Shards.Count; i++ { + out = append(out, buildShardedComponentNetworkPolicy(mdbsh, + fmt.Sprintf("%s-shard-%d-netpol", mdbsh.Name, i), 27018, + buildLabels(mdbsh.Name, fmt.Sprintf("shard-%d", i)))) + } + out = append(out, buildShardedComponentNetworkPolicy(mdbsh, mdbsh.Name+"-mongos-netpol", 27017, + buildLabels(mdbsh.Name, "mongos"))) + return out +}