Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a6b9d48
RS Controller refactor fixed merge conflicts
Julien-Ben Oct 1, 2025
4196529
Fix and improve certificates handling
Julien-Ben Oct 1, 2025
1eead94
Pass current agent auth mode
Julien-Ben Oct 1, 2025
e913144
currentAgentAuthMode in deploymentOptions
Julien-Ben Oct 1, 2025
74cc89a
PrepareScaleDown without need for sts
Julien-Ben Oct 1, 2025
dd4756f
Pass configmap to publishAutomationConfigFirstRS directly
Julien-Ben Oct 1, 2025
c3ef579
Lint
Julien-Ben Oct 1, 2025
e41d24a
Commit TODOs
Julien-Ben Oct 1, 2025
c89617b
Remove TODOs
Julien-Ben Oct 1, 2025
eb78918
Lint
Julien-Ben Oct 1, 2025
073032b
Edge case TLS disabled and rs scaled
Julien-Ben Oct 1, 2025
8c4f229
Remove unused lines
Julien-Ben Oct 1, 2025
691d804
Import order
Julien-Ben Oct 1, 2025
31daeae
Fix comment
Julien-Ben Oct 1, 2025
310030e
Run applySearchOverrides early
Julien-Ben Oct 1, 2025
82467d6
Revert "Edge case TLS disabled and rs scaled"
Julien-Ben Oct 3, 2025
c7268d1
Revert to controller gen 0.18
Julien-Ben Oct 3, 2025
e302990
Fix edge case
Julien-Ben Oct 3, 2025
02920f8
TestPublishAutomationConfigFirstRS
Julien-Ben Oct 7, 2025
580927f
Remove old comment
Julien-Ben Oct 7, 2025
ea14853
TestCreateMongodProcessesFromMongoDB
Julien-Ben Oct 7, 2025
1376b5d
TestBuildFromMongoDBWithReplicas
Julien-Ben Oct 7, 2025
3c30758
Revert unrelated changed
Julien-Ben Oct 7, 2025
e2d2e33
Lint`
Julien-Ben Oct 7, 2025
1c1446a
Merge branch 'master' into jben/rs-controller-refactor-clean-rebase
Julien-Ben Oct 7, 2025
96e6dc0
Update doc
Julien-Ben Oct 7, 2025
e1beb93
Revert renaming shouldMirrorKeyfile
Julien-Ben Oct 16, 2025
be16ca1
descriptive prometheus certificate generation error
Julien-Ben Oct 16, 2025
2cfc70c
Add test for GetDNSNames
Julien-Ben Oct 16, 2025
39810dc
Simplify TestCreateMongodProcessesFromMongoDB
Julien-Ben Oct 16, 2025
a4c0942
Merge branch 'master' into jben/rs-controller-refactor-clean-rebase
Julien-Ben Oct 16, 2025
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 api/v1/mdb/mongodb_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,7 @@ func (a *Authentication) IsOIDCEnabled() bool {
return stringutil.Contains(a.GetModes(), util.OIDC)
}

// GetModes returns the modes of the Authentication instance of an empty
// GetModes returns the modes of the Authentication instance, or an empty
// list if it is nil
func (a *Authentication) GetModes() []string {
if a == nil {
Expand Down
12 changes: 12 additions & 0 deletions controllers/om/process/om_process.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ func CreateMongodProcessesWithLimit(mongoDBImage string, forceEnterprise bool, s
return processes
}

// CreateMongodProcessesFromMongoDB creates mongod processes directly from MongoDB resource without StatefulSet
func CreateMongodProcessesFromMongoDB(mongoDBImage string, forceEnterprise bool, mdb *mdbv1.MongoDB, limit int, fcv string, tlsCertPath string) []om.Process {
hostnames, names := dns.GetDNSNames(mdb.Name, mdb.ServiceName(), mdb.Namespace, mdb.Spec.GetClusterDomain(), limit, mdb.Spec.DbCommonSpec.GetExternalDomain())
processes := make([]om.Process, len(hostnames))

for idx, hostname := range hostnames {
processes[idx] = om.NewMongodProcess(names[idx], hostname, mongoDBImage, forceEnterprise, mdb.Spec.GetAdditionalMongodConfig(), &mdb.Spec, tlsCertPath, mdb.Annotations, fcv)
}

return processes
}

// CreateMongodProcessesWithLimitMulti creates the process array for automationConfig based on MultiCluster CR spec
func CreateMongodProcessesWithLimitMulti(mongoDBImage string, forceEnterprise bool, mrs mdbmultiv1.MongoDBMultiCluster, certFileName string) ([]om.Process, error) {
hostnames := make([]string, 0)
Expand Down
146 changes: 146 additions & 0 deletions controllers/om/process/om_process_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package process

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"

mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb"
"github.com/mongodb/mongodb-kubernetes/pkg/util/maputil"
)

const (
defaultMongoDBImage = "mongodb/mongodb-enterprise-server:7.0.0"
defaultFCV = "7.0"
defaultNamespace = "test-namespace"
)

func TestCreateMongodProcessesFromMongoDB(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test and TestCreateMongodProcessesFromMongoDB_AdditionalConfig seem to be testing om.NewMongodProcess (already has own unit tests) and dns.GetDNSNames (doesn't have own unit tests).

I think it is better to unit-test om.NewMongodProcess and dns.GetDNSNames separately. Once you have unit tests for these building blocks you wouldn't need to test the integration so thoroughly.

This also would (indirectly) cover WaitForRsAgentsToRegisterByResource and other places where these functions used.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point, I tested the underlying function instead

t.Run("Happy path - creates processes with correct integration", func(t *testing.T) {
mdb := baseReplicaSet("test-rs", 3)
processes := CreateMongodProcessesFromMongoDB(
defaultMongoDBImage,
false,
mdb,
3,
defaultFCV,
"",
)

assert.Len(t, processes, 3, "Should create 3 processes")

// Verify basic integration - processes are created with correct names and FCV
for i, process := range processes {
expectedName := fmt.Sprintf("test-rs-%d", i)
assert.Equal(t, expectedName, process.Name(), "Process name should be generated correctly")
assert.Equal(t, defaultFCV, process.FeatureCompatibilityVersion(), "FCV should be set correctly")
assert.NotEmpty(t, process.HostName(), "Hostname should be generated")
}
})

t.Run("Limit parameter controls process count", func(t *testing.T) {
mdb := baseReplicaSet("scale-rs", 5)

// Test limit less than members (scale up in progress)
processesScaleUp := CreateMongodProcessesFromMongoDB(
defaultMongoDBImage,
false,
mdb,
3, // limit
defaultFCV,
"",
)
assert.Len(t, processesScaleUp, 3, "Limit should control process count during scale up")

// Test limit greater than members (scale down in progress)
processesScaleDown := CreateMongodProcessesFromMongoDB(
defaultMongoDBImage,
false,
mdb,
7, // limit
defaultFCV,
"",
)
assert.Len(t, processesScaleDown, 7, "Limit should control process count during scale down")

// Test limit zero
processesZero := CreateMongodProcessesFromMongoDB(
defaultMongoDBImage,
false,
mdb,
0, // limit
defaultFCV,
"",
)
assert.Empty(t, processesZero, "Zero limit should create empty process slice")
})

t.Run("TLS cert path flows through to processes", func(t *testing.T) {
mdb := baseReplicaSet("tls-rs", 2)
mdb.Spec.Security = &mdbv1.Security{
TLSConfig: &mdbv1.TLSConfig{Enabled: true},
}

tlsCertPath := "/custom/path/to/cert.pem"
processes := CreateMongodProcessesFromMongoDB(
defaultMongoDBImage,
false,
mdb,
2,
defaultFCV,
tlsCertPath,
)

assert.Len(t, processes, 2)

// Verify TLS configuration is properly integrated
for i, process := range processes {
tlsConfig := process.TLSConfig()
assert.NotNil(t, tlsConfig, "TLS config should be set when cert path provided")
assert.Equal(t, tlsCertPath, tlsConfig["certificateKeyFile"], "TLS cert path should match at index %d", i)
}
})
}

func TestCreateMongodProcessesFromMongoDB_AdditionalConfig(t *testing.T) {
config := mdbv1.NewAdditionalMongodConfig("storage.engine", "inMemory").
AddOption("replication.oplogSizeMB", 2048)

mdb := mdbv1.NewReplicaSetBuilder().
SetName("config-rs").
SetNamespace(defaultNamespace).
SetMembers(2).
SetVersion("7.0.0").
SetFCVersion(defaultFCV).
SetAdditionalConfig(config).
Build()

processes := CreateMongodProcessesFromMongoDB(
defaultMongoDBImage,
false,
mdb,
2,
defaultFCV,
"",
)

assert.Len(t, processes, 2)

for i, process := range processes {
assert.Equal(t, "inMemory", maputil.ReadMapValueAsInterface(process.Args(), "storage", "engine"),
"Storage engine mismatch at index %d", i)
assert.Equal(t, 2048, maputil.ReadMapValueAsInterface(process.Args(), "replication", "oplogSizeMB"),
"OplogSizeMB mismatch at index %d", i)
}
}

func baseReplicaSet(name string, members int) *mdbv1.MongoDB {
return mdbv1.NewReplicaSetBuilder().
SetName(name).
SetNamespace(defaultNamespace).
SetMembers(members).
SetVersion("7.0.0").
SetFCVersion(defaultFCV).
Build()
}
31 changes: 12 additions & 19 deletions controllers/om/replicaset/om_replicaset.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ func BuildFromStatefulSetWithReplicas(mongoDBImage string, forceEnterprise bool,
return rsWithProcesses
}

// BuildFromMongoDBWithReplicas returns a replica set that can be set in the Automation Config
// based on the given MongoDB resource directly without requiring a StatefulSet.
func BuildFromMongoDBWithReplicas(mongoDBImage string, forceEnterprise bool, mdb *mdbv1.MongoDB, replicas int, fcv string, tlsCertPath string) om.ReplicaSetWithProcesses {
members := process.CreateMongodProcessesFromMongoDB(mongoDBImage, forceEnterprise, mdb, replicas, fcv, tlsCertPath)
replicaSet := om.NewReplicaSet(mdb.Name, mdb.Spec.GetMongoDBVersion())
rsWithProcesses := om.NewReplicaSetWithProcesses(replicaSet, members, mdb.Spec.GetMemberOptions())
rsWithProcesses.SetHorizons(mdb.Spec.GetHorizonConfig())
return rsWithProcesses
}

// PrepareScaleDownFromMap performs additional steps necessary to make sure removed members are not primary (so no
// election happens and replica set is available) (see
// https://jira.mongodb.org/browse/HELP-3818?focusedCommentId=1548348 for more details)
Expand Down Expand Up @@ -65,30 +75,13 @@ func PrepareScaleDownFromMap(omClient om.Connection, rsMembers map[string][]stri
log.Debugw("Marked replica set members as non-voting", "replica set with members", rsMembers)
}

// TODO practice shows that automation agents can get stuck on setting db to "disabled" also it seems that this process
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was old (<2022)

// works correctly without explicit disabling - feel free to remove this code after some time when it is clear
// that everything works correctly without disabling

// Stage 2. Set disabled to true
//err = omClient.ReadUpdateDeployment(
// func(d om.Deployment) error {
// d.DisableProcesses(allProcesses)
// return nil
// },
//)
//
//if err != nil {
// return errors.New(fmt.Sprintf("Unable to set disabled to true, hosts: %v, err: %w", allProcesses, err))
//}
//log.Debugw("Disabled processes", "processes", allProcesses)

log.Infow("Performed some preliminary steps to support scale down", "hosts", processes)

return nil
}

func PrepareScaleDownFromStatefulSet(omClient om.Connection, statefulSet appsv1.StatefulSet, rs *mdbv1.MongoDB, log *zap.SugaredLogger) error {
_, podNames := dns.GetDnsForStatefulSetReplicasSpecified(statefulSet, rs.Spec.GetClusterDomain(), rs.Status.Members, nil)
func PrepareScaleDownFromMongoDB(omClient om.Connection, rs *mdbv1.MongoDB, log *zap.SugaredLogger) error {
_, podNames := dns.GetDNSNames(rs.Name, rs.ServiceName(), rs.Namespace, rs.Spec.GetClusterDomain(), rs.Status.Members, rs.Spec.DbCommonSpec.GetExternalDomain())
podNames = podNames[scale.ReplicasThisReconciliation(rs):rs.Status.Members]

if len(podNames) != 1 {
Expand Down
95 changes: 95 additions & 0 deletions controllers/om/replicaset/om_replicaset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package replicaset

import (
"testing"

"github.com/stretchr/testify/assert"
"k8s.io/utils/ptr"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

mdbv1 "github.com/mongodb/mongodb-kubernetes/api/v1/mdb"
"github.com/mongodb/mongodb-kubernetes/controllers/om"
"github.com/mongodb/mongodb-kubernetes/mongodb-community-operator/pkg/automationconfig"
)

// This test focuses on the integration/glue logic, not re-testing components.
func TestBuildFromMongoDBWithReplicas(t *testing.T) {
memberOptions := []automationconfig.MemberOptions{
{Votes: ptr.To(1), Priority: ptr.To("1.0")},
{Votes: ptr.To(1), Priority: ptr.To("0.5")},
{Votes: ptr.To(0), Priority: ptr.To("0")}, // Non-voting member
}

mdb := &mdbv1.MongoDB{
ObjectMeta: metav1.ObjectMeta{
Name: "test-rs",
Namespace: "test-namespace",
},
Spec: mdbv1.MongoDbSpec{
DbCommonSpec: mdbv1.DbCommonSpec{
Version: "7.0.5",
Security: &mdbv1.Security{
TLSConfig: &mdbv1.TLSConfig{},
Authentication: &mdbv1.Authentication{},
},
Connectivity: &mdbv1.MongoDBConnectivity{
ReplicaSetHorizons: []mdbv1.MongoDBHorizonConfig{},
},
},
Members: 5, // Spec (target) is 5 members
MemberConfig: memberOptions,
},
}

// 3 replicas is less than spec.Members, scale up scenario
replicas := 3
rsWithProcesses := BuildFromMongoDBWithReplicas(
"mongodb/mongodb-enterprise-server:7.0.5",
false,
mdb,
replicas,
"7.0",
"",
)

// Assert: ReplicaSet structure
assert.Equal(t, "test-rs", rsWithProcesses.Rs.Name(), "ReplicaSet ID should match MongoDB name")
assert.Equal(t, "1", rsWithProcesses.Rs["protocolVersion"], "Protocol version should be set to 1 for this MongoDB version")

// Assert: Member count is controlled by replicas parameter, NOT mdb.Spec.Members
members := rsWithProcesses.Rs["members"].([]om.ReplicaSetMember)
assert.Len(t, members, replicas, "Member count should match replicas parameter (3), not mdb.Spec.Members (5)")
assert.Equal(t, 3, len(members), "Should have exactly 3 members")

// Assert: Processes are created correctly
assert.Len(t, rsWithProcesses.Processes, replicas, "Process count should match replicas parameter")
expectedProcessNames := []string{"test-rs-0", "test-rs-1", "test-rs-2"}
expectedHostnames := []string{
"test-rs-0.test-rs-svc.test-namespace.svc.cluster.local",
"test-rs-1.test-rs-svc.test-namespace.svc.cluster.local",
"test-rs-2.test-rs-svc.test-namespace.svc.cluster.local",
}

for i := 0; i < replicas; i++ {
assert.Equal(t, expectedProcessNames[i], rsWithProcesses.Processes[i].Name(),
"Process name mismatch at index %d", i)
assert.Equal(t, expectedHostnames[i], rsWithProcesses.Processes[i].HostName(),
"Process hostname mismatch at index %d", i)
}

// Assert: Member options are propagated
assert.Equal(t, 1, members[0].Votes(), "Member 0 should have 1 vote")
assert.Equal(t, float32(1.0), members[0].Priority(), "Member 0 should have priority 1.0")
assert.Equal(t, 1, members[1].Votes(), "Member 1 should have 1 vote")
assert.Equal(t, float32(0.5), members[1].Priority(), "Member 1 should have priority 0.5")
assert.Equal(t, 0, members[2].Votes(), "Member 2 should have 0 votes (non-voting)")
assert.Equal(t, float32(0), members[2].Priority(), "Member 2 should have priority 0")

// Assert: Member host field contains process name (not full hostname)
// Note: ReplicaSetMember["host"] is the process name, not the full hostname
for i := 0; i < replicas; i++ {
assert.Equal(t, expectedProcessNames[i], members[i].Name(),
"Member host should match process name at index %d", i)
}
}
13 changes: 13 additions & 0 deletions controllers/operator/agents/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ func WaitForRsAgentsToRegister(set appsv1.StatefulSet, members int, clusterName
return nil
}

// WaitForRsAgentsToRegisterByResource waits for RS agents to register using MongoDB resource directly without StatefulSet
func WaitForRsAgentsToRegisterByResource(rs *mdbv1.MongoDB, members int, omConnection om.Connection, log *zap.SugaredLogger) error {
hostnames, _ := dns.GetDNSNames(rs.Name, rs.ServiceName(), rs.Namespace, rs.Spec.GetClusterDomain(), members, rs.Spec.DbCommonSpec.GetExternalDomain())

log = log.With("mongodb", rs.Name)

ok, msg := waitUntilRegistered(omConnection, log, retryParams{retrials: 5, waitSeconds: 3}, hostnames...)
if !ok {
return getAgentRegisterError(msg)
}
return nil
}

// WaitForRsAgentsToRegisterSpecifiedHostnames waits for the specified agents to registry with Ops Manager.
func WaitForRsAgentsToRegisterSpecifiedHostnames(omConnection om.Connection, hostnames []string, log *zap.SugaredLogger) error {
ok, msg := waitUntilRegistered(omConnection, log, retryParams{retrials: 10, waitSeconds: 9}, hostnames...)
Expand Down
10 changes: 5 additions & 5 deletions controllers/operator/mongodbmultireplicaset_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,6 @@ func (r *ReconcileMongoDbMultiReplicaSet) Reconcile(ctx context.Context, request

r.SetupCommonWatchers(&mrs, nil, nil, mrs.Name)

publishAutomationConfigFirst, err := r.publishAutomationConfigFirstMultiCluster(ctx, &mrs, log)
if err != nil {
return r.updateStatus(ctx, &mrs, workflow.Failed(err), log)
}

// If tls is enabled we need to configure the "processes" array in opsManager/Cloud Manager with the
// correct tlsCertPath, with the new tls design, this path has the certHash in it(so that cert can be rotated
// without pod restart).
Expand Down Expand Up @@ -210,6 +205,11 @@ func (r *ReconcileMongoDbMultiReplicaSet) Reconcile(ctx context.Context, request
}
}

publishAutomationConfigFirst, err := r.publishAutomationConfigFirstMultiCluster(ctx, &mrs, log)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brought it closer to where it's actually used

if err != nil {
return r.updateStatus(ctx, &mrs, workflow.Failed(err), log)
}

status := workflow.RunInGivenOrder(publishAutomationConfigFirst,
func() workflow.Status {
if err := r.updateOmDeploymentRs(ctx, conn, mrs, agentCertPath, tlsCertPath, internalClusterCertPath, false, log); err != nil {
Expand Down
Loading