From 8bc1eeeb891b011dafcfde3bd287afee3fef3f6f Mon Sep 17 00:00:00 2001 From: Andrii Dema Date: Wed, 8 Jul 2026 14:27:51 +0300 Subject: [PATCH 1/2] K8SPSMDB-1537: connection string secret fixes https://perconadev.atlassian.net/browse/K8SPSMDB-1537 --- .../perconaservermongodb/secrets.go | 3 +- .../perconaservermongodb/secrets_test.go | 10 +-- pkg/controller/perconaservermongodb/users.go | 1 - pkg/psmdb/client_test.go | 69 ++++++++++++++++++- pkg/psmdb/service.go | 30 +++----- pkg/psmdb/service_test.go | 2 +- 6 files changed, 85 insertions(+), 30 deletions(-) diff --git a/pkg/controller/perconaservermongodb/secrets.go b/pkg/controller/perconaservermongodb/secrets.go index 49b5bbbd52..4455239e45 100644 --- a/pkg/controller/perconaservermongodb/secrets.go +++ b/pkg/controller/perconaservermongodb/secrets.go @@ -57,7 +57,6 @@ func ensureConnectionStringSecret( cr *api.PerconaServerMongoDB, secretName, keyPrefix string, cred psmdb.Credentials, - owner metav1.Object, includeReplsets bool, ) error { connStrSecret := &corev1.Secret{ @@ -73,7 +72,7 @@ func ensureConnectionStringSecret( if err := fillUserConnectionString(ctx, cl, connStrSecret.Data, cr, keyPrefix, cred, includeReplsets); err != nil { return errors.Wrap(err, "fill user connection string") } - if err := controllerutil.SetOwnerReference(owner, connStrSecret, cl.Scheme()); err != nil { + if err := controllerutil.SetOwnerReference(cr, connStrSecret, cl.Scheme()); err != nil { return errors.Wrap(err, "set owner reference") } return nil diff --git a/pkg/controller/perconaservermongodb/secrets_test.go b/pkg/controller/perconaservermongodb/secrets_test.go index 147f483f94..689d8c8402 100644 --- a/pkg/controller/perconaservermongodb/secrets_test.go +++ b/pkg/controller/perconaservermongodb/secrets_test.go @@ -143,7 +143,7 @@ func TestEnsureConnectionStringSecret(t *testing.T) { } }, expected: map[string][]byte{ - "app_user_mongos_connectionString": []byte("mongodb://app-user:p%40ss%2Fword@10.0.0.20:27017/?authSource=application"), + "app_user_mongos_connectionString": []byte("mongodb://app-user:p%40ss%2Fword@cluster-mongos-0.database.svc.cluster.local:27017/?authSource=application"), "app_user_mongos_connectionStringExposed": []byte("mongodb://app-user:p%40ss%2Fword@mongos.example.com:27017/?authSource=application"), }, }, @@ -173,7 +173,6 @@ func TestEnsureConnectionStringSecret(t *testing.T) { Password: "p@ss/word", AuthSource: "application", }, - owner, tt.includeReplsets, ) require.NoError(t, err) @@ -188,8 +187,8 @@ func TestEnsureConnectionStringSecret(t *testing.T) { assert.Empty(t, validation.IsConfigMapKey(key)) } require.Len(t, actual.OwnerReferences, 1) - assert.Equal(t, owner.UID, actual.OwnerReferences[0].UID) - assert.Equal(t, "Secret", actual.OwnerReferences[0].Kind) + assert.Equal(t, cr.UID, actual.OwnerReferences[0].UID) + assert.Equal(t, "PerconaServerMongoDB", actual.OwnerReferences[0].Kind) }) } } @@ -231,7 +230,8 @@ func TestReconcileUsersCreatesConnectionStringSecretWhenCredentialsUnchanged(t * Namespace: cr.Namespace, } require.NoError(t, r.client.Get(t.Context(), key, actual)) - assert.Equal(t, + assert.Equal( + t, []byte("mongodb://databaseAdmin:password@cluster-rs0-0.cluster-rs0.database.svc.cluster.local:27017/?authSource=admin&replicaSet=rs0"), actual.Data["databaseAdmin_rs0_connectionString"], ) diff --git a/pkg/controller/perconaservermongodb/users.go b/pkg/controller/perconaservermongodb/users.go index 042e395af0..496c6ee607 100644 --- a/pkg/controller/perconaservermongodb/users.go +++ b/pkg/controller/perconaservermongodb/users.go @@ -101,7 +101,6 @@ func (r *ReconcilePerconaServerMongoDB) reconcileUsers(ctx context.Context, cr * naming.SecretDatabaseAdminConnStrName(cr), string(api.RoleDatabaseAdmin), cred, - &sysUsersSecretObj, true, ); err != nil { return errors.Wrap(err, "ensure connection string secret") diff --git a/pkg/psmdb/client_test.go b/pkg/psmdb/client_test.go index 845f54d16e..d842c94533 100644 --- a/pkg/psmdb/client_test.go +++ b/pkg/psmdb/client_test.go @@ -210,6 +210,54 @@ func TestMongoConfigReturnsServiceImportLookupError(t *testing.T) { } } +func TestMongoConfigSkipsTerminatingPods(t *testing.T) { + cr := clientTestCluster() + rs := cr.Spec.Replsets[0] + rs.Size = 2 + + stsLabels := naming.RSLabels(cr, rs) + stsLabels[naming.LabelKubernetesComponent] = naming.ComponentMongod + + pod0 := clientTestPod(cr, "cluster-rs0-0", naming.RSLabels(cr, rs)) + pod0.Labels[naming.LabelKubernetesComponent] = naming.ComponentMongod + + pod1 := clientTestPod(cr, "cluster-rs0-1", naming.RSLabels(cr, rs)) + pod1.Labels[naming.LabelKubernetesComponent] = naming.ComponentMongod + now := metav1.Now() + pod1.DeletionTimestamp = &now + pod1.Finalizers = []string{"test/finalizer"} + + cl := fake.NewClientBuilder(). + WithScheme(clientTestScheme(t)). + WithObjects( + &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster-rs0", + Namespace: cr.Namespace, + Labels: stsLabels, + }, + }, + pod0, + pod1, + ). + Build() + + cfg, err := MongoConfig( + t.Context(), + cl, + cr, + api.DNSModeInternal, + rs, + clientTestCredentials(), + false, + ) + require.NoError(t, err) + assert.Equal(t, + "mongodb://app-user:p%40ss%2Fword@cluster-rs0-0.cluster-rs0.database.svc.cluster.local:27017/?authSource=application&replicaSet=rs0", + cfg.URI(), + ) +} + func TestMongosConfigURIFromMongosAddrs(t *testing.T) { tests := map[string]struct { servicePerPod bool @@ -239,6 +287,25 @@ func TestMongosConfigURIFromMongosAddrs(t *testing.T) { }, expectedURI: "mongodb://app-user:p%40ss%2Fword@cluster-mongos-0.database.svc.cluster.local:27017,cluster-mongos-1.database.svc.cluster.local:27017/?authSource=application", }, + "service per pod skips terminating pods": { + servicePerPod: true, + useInternalAddr: true, + objects: func(cr *api.PerconaServerMongoDB) []client.Object { + labels := naming.MongosLabels(cr) + pod0 := clientTestPod(cr, "cluster-mongos-0", labels) + pod1 := clientTestPod(cr, "cluster-mongos-1", labels) + now := metav1.Now() + pod1.DeletionTimestamp = &now + pod1.Finalizers = []string{"test/finalizer"} + return []client.Object{ + pod0, + pod1, + clientTestMongosService(cr, "cluster-mongos-0"), + clientTestMongosService(cr, "cluster-mongos-1"), + } + }, + expectedURI: "mongodb://app-user:p%40ss%2Fword@cluster-mongos-0.database.svc.cluster.local:27017/?authSource=application", + }, "load balancer internal address": { exposeType: corev1.ServiceTypeLoadBalancer, useInternalAddr: true, @@ -248,7 +315,7 @@ func TestMongosConfigURIFromMongosAddrs(t *testing.T) { svc.Spec.ClusterIP = "10.0.0.20" return []client.Object{svc} }, - expectedURI: "mongodb://app-user:p%40ss%2Fword@10.0.0.20:27017/?authSource=application", + expectedURI: "mongodb://app-user:p%40ss%2Fword@cluster-mongos.database.svc.cluster.local:27017/?authSource=application", }, "load balancer external address": { exposeType: corev1.ServiceTypeLoadBalancer, diff --git a/pkg/psmdb/service.go b/pkg/psmdb/service.go index ee52cc4f2e..b2dd4fe9a9 100644 --- a/pkg/psmdb/service.go +++ b/pkg/psmdb/service.go @@ -232,21 +232,15 @@ func getServiceIngressAddr(ctx context.Context, cl client.Client, serviceNN type return "", ErrNoIngressPoints } -func getServiceClusterIP(ctx context.Context, cl client.Client, serviceNN types.NamespacedName) (string, error) { - svc := &corev1.Service{} - err := cl.Get(ctx, serviceNN, svc) - if err != nil { - return "", errors.Wrap(err, "failed to fetch service") - } - - return svc.Spec.ClusterIP, nil -} - // GetReplsetAddrs returns a slice of replset host:port addresses func GetReplsetAddrs(ctx context.Context, cl client.Client, cr *api.PerconaServerMongoDB, dnsMode api.DNSMode, rs *api.ReplsetSpec, rsExposed bool, pods []corev1.Pod) ([]string, error) { addrs := make([]string, 0) for _, pod := range pods { + if pod.DeletionTimestamp != nil { + continue + } + host, err := MongoHost(ctx, cl, cr, dnsMode, rs, rsExposed, pod) if err != nil { return nil, errors.Wrapf(err, "failed to get external hostname for pod %s", pod.Name) @@ -274,6 +268,10 @@ func GetMongosAddrs(ctx context.Context, cl client.Client, cr *api.PerconaServer addrs := make([]string, 0, len(list.Items)) for _, pod := range list.Items { + if pod.DeletionTimestamp != nil { + continue + } + host, err := MongosHost(ctx, cl, cr, &pod, useInternalAddr) if err != nil { return nil, errors.Wrap(err, "failed to get mongos host") @@ -374,16 +372,8 @@ func MongosHost(ctx context.Context, cl client.Client, cr *api.PerconaServerMong } mongos := cr.Spec.Sharding.Mongos - if mongos.Expose.ExposeType == corev1.ServiceTypeLoadBalancer && svc.Spec.Type == corev1.ServiceTypeLoadBalancer { - var host string - var err error - - if useInternalAddr { - host, err = getServiceClusterIP(ctx, cl, nn) - } else { - host, err = getServiceIngressAddr(ctx, cl, nn) - } - + if !useInternalAddr && mongos.Expose.ExposeType == corev1.ServiceTypeLoadBalancer && svc.Spec.Type == corev1.ServiceTypeLoadBalancer { + host, err := getServiceIngressAddr(ctx, cl, nn) if err != nil { return "", errors.Wrap(err, "get ingress endpoint") } diff --git a/pkg/psmdb/service_test.go b/pkg/psmdb/service_test.go index 1307b0a6bd..5a9f8e8dea 100644 --- a/pkg/psmdb/service_test.go +++ b/pkg/psmdb/service_test.go @@ -115,7 +115,7 @@ func TestMongosHost(t *testing.T) { }, useInternal: true, exposeType: corev1.ServiceTypeLoadBalancer, - expectedHost: "10.0.0.20:27018", + expectedHost: "test-cluster-mongos.default.svc.cluster.local:27018", }, "err: clusterip service type and port not found": { init: func(cl client.Client) { From d46785637a4c2909095fc69ac1ce765278596860 Mon Sep 17 00:00:00 2001 From: Andrii Dema Date: Thu, 9 Jul 2026 13:36:32 +0300 Subject: [PATCH 2/2] address copilot comments --- .../perconaservermongodb/secrets.go | 28 ++++++++----- .../perconaservermongodb/secrets_test.go | 41 +++++++++++++++++++ pkg/psmdb/mongo/mongo.go | 4 ++ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/pkg/controller/perconaservermongodb/secrets.go b/pkg/controller/perconaservermongodb/secrets.go index 4455239e45..6fae3d6d54 100644 --- a/pkg/controller/perconaservermongodb/secrets.go +++ b/pkg/controller/perconaservermongodb/secrets.go @@ -157,21 +157,23 @@ func fillUserConnectionString(ctx context.Context, cl client.Client, data map[st return errors.Wrap(err, "mongo config") } - connStr := cfg.URI() key := keyPrefix + "_" + rs.Name - data[key+"_connectionString"] = []byte(connStr) - data[key+"_connectionStringSrv"] = []byte(cfg.SRVURI(strings.Join([]string{ - naming.ServiceName(cr, rs), - cr.Namespace, - cr.Spec.ClusterServiceDNSSuffix, - }, "."))) + connStr := cfg.URI() + if connStr != "" { + data[key+"_connectionString"] = []byte(connStr) + data[key+"_connectionStringSrv"] = []byte(cfg.SRVURI(strings.Join([]string{ + naming.ServiceName(cr, rs), + cr.Namespace, + cr.Spec.ClusterServiceDNSSuffix, + }, "."))) + } if rs.Expose.Enabled { cfg, err := psmdb.MongoConfig(ctx, cl, cr, api.DNSModeExternal, rs, cred, true) if err != nil { return errors.Wrap(err, "mongo config") } - if exposedConnStr := cfg.URI(); exposedConnStr != connStr { + if exposedConnStr := cfg.URI(); exposedConnStr != "" && exposedConnStr != connStr { data[key+"_connectionStringExposed"] = []byte(exposedConnStr) } } @@ -184,14 +186,20 @@ func fillUserConnectionString(ctx context.Context, cl client.Client, data map[st if err != nil { return errors.Wrap(err, "mongos config") } - data[keyPrefix+"_mongos_connectionString"] = []byte(mongosCfg.URI()) + connStr := mongosCfg.URI() + if connStr != "" { + data[keyPrefix+"_mongos_connectionString"] = []byte(mongosCfg.URI()) + } if servicePerPod { mongosCfg, err := psmdb.MongosConfig(ctx, cl, cr, cred, false, true) if err != nil { return errors.Wrap(err, "mongos config") } - data[keyPrefix+"_mongos_connectionStringExposed"] = []byte(mongosCfg.URI()) + connStr := mongosCfg.URI() + if connStr != "" { + data[keyPrefix+"_mongos_connectionStringExposed"] = []byte(mongosCfg.URI()) + } } } return nil diff --git a/pkg/controller/perconaservermongodb/secrets_test.go b/pkg/controller/perconaservermongodb/secrets_test.go index 689d8c8402..f981b7e469 100644 --- a/pkg/controller/perconaservermongodb/secrets_test.go +++ b/pkg/controller/perconaservermongodb/secrets_test.go @@ -43,6 +43,24 @@ func TestEnsureConnectionStringSecret(t *testing.T) { "app_user_rs0_connectionStringSrv": []byte("mongodb+srv://app-user:p%40ss%2Fword@cluster-rs0.database.svc.cluster.local/?authSource=application&replicaSet=rs0"), }, }, + "replset without pods": { + includeReplsets: true, + setup: func(cr *api.PerconaServerMongoDB) []client.Object { + return []client.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-user-conn-str", + Namespace: cr.Namespace, + }, + Data: map[string][]byte{ + "app_user_rs0_connectionString": []byte("stale"), + "app_user_rs0_connectionStringSrv": []byte("stale"), + }, + }, + } + }, + expected: nil, + }, "exposed replset": { includeReplsets: true, setup: func(cr *api.PerconaServerMongoDB) []client.Object { @@ -147,6 +165,29 @@ func TestEnsureConnectionStringSecret(t *testing.T) { "app_user_mongos_connectionStringExposed": []byte("mongodb://app-user:p%40ss%2Fword@mongos.example.com:27017/?authSource=application"), }, }, + "service per pod mongos without pods": { + setup: func(cr *api.PerconaServerMongoDB) []client.Object { + cr.Spec.Sharding = api.Sharding{ + Enabled: true, + Mongos: &api.MongosSpec{ + Expose: api.MongosExpose{ServicePerPod: true}, + }, + } + return []client.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-user-conn-str", + Namespace: cr.Namespace, + }, + Data: map[string][]byte{ + "app_user_mongos_connectionString": []byte("stale"), + "app_user_mongos_connectionStringExposed": []byte("stale"), + }, + }, + } + }, + expected: nil, + }, } for name, tt := range tests { diff --git a/pkg/psmdb/mongo/mongo.go b/pkg/psmdb/mongo/mongo.go index 86f1e537cb..3c85341e32 100644 --- a/pkg/psmdb/mongo/mongo.go +++ b/pkg/psmdb/mongo/mongo.go @@ -42,6 +42,9 @@ func (conf *Config) SRVURI(hostname string) string { } func (conf *Config) uri(scheme, hostname string) string { + if len(conf.Hosts) == 0 { + return "" + } u := url.URL{ Scheme: scheme, Host: strings.Join(conf.Hosts, ","), @@ -141,6 +144,7 @@ type Client interface { type ClientDatabase interface { RunCommand(ctx context.Context, runCommand any, opts ...options.Lister[options.RunCmdOptions]) *mongo.SingleResult } + type mongoClient struct { *mongo.Client }