Skip to content

Commit a8147b6

Browse files
authored
CLOUDP-389836 & CLOUDP-386688: Load Balancer Improvements (#912)
# Summary Two problems with the managed Envoy LB: 1. The Envoy image had three sources: `spec.lb.envoy.image` CRD field, `MDB_ENVOY_IMAGE` env var, and a hardcoded fallback in the controller. This didn't match our configuration patterns for the other resources. 2. The main search controller reported Running even if Envoy failed to deploy, we had no way of easily observe the load balancer status. Changes: **Image resolution**: Remove `spec.lb.envoy.image` from the CRD and the hardcoded fallback from the controller. The Envoy image is now sourced exclusively from `MDB_ENVOY_IMAGE`. If the env var isn't set, the envoy controller fails at reconcile time with a clear error. **LB status**: Add a `status.loadBalancer` substatus (phase + message) to the MongoDBSearch CR. The envoy controller reports Pending/Running/Failed into it independently from the main status. The main controller checks `IsLoadBalancerReady()` before going Running, so the search resource won't claim Running until Envoy is healthy. A new `LoadBalancer` print column shows the phase in `kubectl get`. The Envoy image bump from v1.31 to v1.37 is just for local development defaults. ## Proof of Work - Unit tests for `LoadBalancerStatus`, `SearchPartOption`, `IsLoadBalancerReady`, and the LB status update path. - `assert_lb_status()` added to all managed and unmanaged LB E2E tests (RS and sharded) to verify the substatus is present+Running for managed, absent for unmanaged. ## Checklist - [x] Have you linked a jira ticket and/or is the ticket in the title? - [x] Have you checked whether your jira ticket required DOCSP changes? - [x] Have you added changelog file? - use `skip-changelog` label if not needed - refer to [Changelog files and Release Notes](https://github.com/mongodb/mongodb-kubernetes/blob/master/CONTRIBUTING.md#changelog-files-and-release-notes) section in CONTRIBUTING.md for more details
1 parent 3799f83 commit a8147b6

32 files changed

+440
-73
lines changed

api/v1/search/mongodbsearch_types.go

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const (
2626
MongotDefaultGrpcPort int32 = 27028
2727
MongotDefaultPrometheusPort int32 = 9946
2828
MongotDefautHealthCheckPort int32 = 8080
29+
EnvoyDefaultProxyPort int32 = 27028
2930
MongotDefaultSyncSourceUsername = "search-sync-source"
3031

3132
ForceWireprotoAnnotation = "mongodb.com/v1.force-search-wireproto"
@@ -130,10 +131,6 @@ type LoadBalancerConfig struct {
130131

131132
// EnvoyConfig contains configuration for the operator-managed Envoy load balancer.
132133
type EnvoyConfig struct {
133-
// Image is the container image for the Envoy proxy.
134-
// Overrides the operator-level default (MDB_ENVOY_IMAGE env var).
135-
// +optional
136-
Image string `json:"image,omitempty"`
137134
// ResourceRequirements for the Envoy container.
138135
// When not set, defaults to requests: {cpu: 100m, memory: 128Mi}, limits: {cpu: 500m, memory: 512Mi}.
139136
// When set, replaces the defaults entirely.
@@ -254,10 +251,20 @@ type TLS struct {
254251
CertsSecretPrefix string `json:"certsSecretPrefix,omitempty"`
255252
}
256253

254+
// LoadBalancerStatus reports the state of the operator-managed load balancer (Envoy).
255+
type LoadBalancerStatus struct {
256+
Phase status.Phase `json:"phase"`
257+
Message string `json:"message,omitempty"`
258+
}
259+
257260
type MongoDBSearchStatus struct {
258261
status.Common `json:",inline"`
259262
Version string `json:"version,omitempty"`
260263
Warnings []status.Warning `json:"warnings,omitempty"`
264+
// LoadBalancer reports the state of the operator-managed load balancer.
265+
// Only populated when spec.lb.mode == Managed.
266+
// +optional
267+
LoadBalancer *LoadBalancerStatus `json:"loadBalancer,omitempty"`
261268
}
262269

263270
// +k8s:deepcopy-gen=true
@@ -266,6 +273,7 @@ type MongoDBSearchStatus struct {
266273
// +kubebuilder:subresource:status
267274
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Current state of the MongoDB deployment."
268275
// +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".status.version",description="MongoDB Search version reconciled by the operator."
276+
// +kubebuilder:printcolumn:name="LoadBalancer",type="string",JSONPath=".status.loadBalancer.phase",description="Current state of the managed load balancer."
269277
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The time since the MongoDB resource was created."
270278
// +kubebuilder:resource:path=mongodbsearch,scope=Namespaced,shortName=mdbs
271279
type MongoDBSearch struct {
@@ -289,11 +297,21 @@ func (s *MongoDBSearch) GetCommonStatus(options ...status.Option) *status.Common
289297
return &s.Status.Common
290298
}
291299

292-
func (s *MongoDBSearch) GetStatus(...status.Option) interface{} {
300+
func (s *MongoDBSearch) GetStatus(options ...status.Option) interface{} {
301+
if partOpt, exists := status.GetOption(options, SearchPartOption{}); exists {
302+
if partOpt.(SearchPartOption).Part == SearchPartLoadBalancer {
303+
return s.Status.LoadBalancer
304+
}
305+
}
293306
return s.Status
294307
}
295308

296-
func (s *MongoDBSearch) GetStatusPath(...status.Option) string {
309+
func (s *MongoDBSearch) GetStatusPath(options ...status.Option) string {
310+
if partOpt, exists := status.GetOption(options, SearchPartOption{}); exists {
311+
if partOpt.(SearchPartOption).Part == SearchPartLoadBalancer {
312+
return "/status/loadBalancer"
313+
}
314+
}
297315
return "/status"
298316
}
299317

@@ -302,6 +320,13 @@ func (s *MongoDBSearch) SetWarnings(warnings []status.Warning, _ ...status.Optio
302320
}
303321

304322
func (s *MongoDBSearch) UpdateStatus(phase status.Phase, statusOptions ...status.Option) {
323+
if partOpt, exists := status.GetOption(statusOptions, SearchPartOption{}); exists {
324+
if partOpt.(SearchPartOption).Part == SearchPartLoadBalancer {
325+
s.updateLoadBalancerStatus(phase, statusOptions...)
326+
return
327+
}
328+
}
329+
305330
s.Status.UpdateCommonFields(phase, s.GetGeneration(), statusOptions...)
306331
if option, exists := status.GetOption(statusOptions, status.WarningsOption{}); exists {
307332
s.Status.Warnings = append(s.Status.Warnings, option.(status.WarningsOption).Warnings...)
@@ -311,6 +336,17 @@ func (s *MongoDBSearch) UpdateStatus(phase status.Phase, statusOptions ...status
311336
}
312337
}
313338

339+
func (s *MongoDBSearch) updateLoadBalancerStatus(phase status.Phase, statusOptions ...status.Option) {
340+
if s.Status.LoadBalancer == nil {
341+
s.Status.LoadBalancer = &LoadBalancerStatus{}
342+
}
343+
s.Status.LoadBalancer.Phase = phase
344+
s.Status.LoadBalancer.Message = ""
345+
if option, exists := status.GetOption(statusOptions, status.MessageOption{}); exists {
346+
s.Status.LoadBalancer.Message = option.(status.MessageOption).Message
347+
}
348+
}
349+
314350
func (s *MongoDBSearch) NamespacedName() types.NamespacedName {
315351
return types.NamespacedName{Name: s.Name, Namespace: s.Namespace}
316352
}
@@ -606,6 +642,15 @@ func stripPort(endpoint string) string {
606642
return host
607643
}
608644

645+
// IsLoadBalancerReady returns true if managed LB is not configured,
646+
// or if it is configured and its status phase is Running.
647+
func (s *MongoDBSearch) IsLoadBalancerReady() bool {
648+
if !s.IsLBModeManaged() {
649+
return true
650+
}
651+
return s.Status.LoadBalancer != nil && s.Status.LoadBalancer.Phase == status.PhaseRunning
652+
}
653+
609654
// LoadBalancerDeploymentName returns the name of the managed Envoy Deployment for this resource.
610655
func (s *MongoDBSearch) LoadBalancerDeploymentName() string {
611656
return s.Name + "-search-lb"
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package search
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
8+
"github.com/mongodb/mongodb-kubernetes/api/v1/status"
9+
)
10+
11+
func TestUpdateStatus_MainPath(t *testing.T) {
12+
s := &MongoDBSearch{}
13+
s.UpdateStatus(status.PhaseRunning, NewMongoDBSearchVersionOption("1.0"))
14+
15+
assert.Equal(t, status.PhaseRunning, s.Status.Phase)
16+
assert.Equal(t, "1.0", s.Status.Version)
17+
assert.Nil(t, s.Status.LoadBalancer)
18+
}
19+
20+
func TestUpdateStatus_LoadBalancerPath(t *testing.T) {
21+
s := &MongoDBSearch{}
22+
partOpt := NewSearchPartOption(SearchPartLoadBalancer)
23+
s.UpdateStatus(status.PhaseRunning, partOpt, status.NewMessageOption("Envoy ready"))
24+
25+
assert.NotNil(t, s.Status.LoadBalancer)
26+
assert.Equal(t, status.PhaseRunning, s.Status.LoadBalancer.Phase)
27+
assert.Equal(t, "Envoy ready", s.Status.LoadBalancer.Message)
28+
// Main status untouched
29+
assert.Equal(t, status.Phase(""), s.Status.Phase)
30+
}
31+
32+
func TestGetStatusPath_Default(t *testing.T) {
33+
s := &MongoDBSearch{}
34+
assert.Equal(t, "/status", s.GetStatusPath())
35+
}
36+
37+
func TestGetStatusPath_LoadBalancer(t *testing.T) {
38+
s := &MongoDBSearch{}
39+
partOpt := NewSearchPartOption(SearchPartLoadBalancer)
40+
assert.Equal(t, "/status/loadBalancer", s.GetStatusPath(partOpt))
41+
}
42+
43+
func TestGetStatus_Default(t *testing.T) {
44+
s := &MongoDBSearch{}
45+
s.Status.Phase = status.PhaseRunning
46+
got := s.GetStatus()
47+
assert.Equal(t, s.Status, got)
48+
}
49+
50+
func TestGetStatus_LoadBalancer(t *testing.T) {
51+
s := &MongoDBSearch{}
52+
s.Status.LoadBalancer = &LoadBalancerStatus{Phase: status.PhaseRunning, Message: "ok"}
53+
partOpt := NewSearchPartOption(SearchPartLoadBalancer)
54+
got := s.GetStatus(partOpt)
55+
assert.Equal(t, s.Status.LoadBalancer, got)
56+
}
57+
58+
// nil LB → GetStatus returns nil, used by clearLBStatus to patch null
59+
func TestGetStatus_LoadBalancerNil(t *testing.T) {
60+
s := &MongoDBSearch{}
61+
partOpt := NewSearchPartOption(SearchPartLoadBalancer)
62+
got := s.GetStatus(partOpt)
63+
assert.Nil(t, got)
64+
}
65+
66+
// Failed→Running transition must not carry over the old error message
67+
func TestUpdateStatus_LoadBalancerClearsStaleMessage(t *testing.T) {
68+
s := &MongoDBSearch{}
69+
partOpt := NewSearchPartOption(SearchPartLoadBalancer)
70+
71+
// Simulate a failure with a message
72+
s.UpdateStatus(status.PhaseFailed, partOpt, status.NewMessageOption("missing image"))
73+
assert.Equal(t, "missing image", s.Status.LoadBalancer.Message)
74+
75+
// Transition to Running without a message — old message must be cleared
76+
s.UpdateStatus(status.PhaseRunning, partOpt)
77+
assert.Equal(t, status.PhaseRunning, s.Status.LoadBalancer.Phase)
78+
assert.Equal(t, "", s.Status.LoadBalancer.Message)
79+
}
80+
81+
func TestIsLoadBalancerReady(t *testing.T) {
82+
tests := []struct {
83+
name string
84+
search MongoDBSearch
85+
expected bool
86+
}{
87+
{
88+
name: "no LB configured",
89+
search: MongoDBSearch{},
90+
expected: true,
91+
},
92+
{
93+
name: "managed LB, status nil",
94+
search: MongoDBSearch{
95+
Spec: MongoDBSearchSpec{
96+
LoadBalancer: &LoadBalancerConfig{Mode: LBModeManaged},
97+
},
98+
},
99+
expected: false,
100+
},
101+
{
102+
name: "managed LB, status Running",
103+
search: MongoDBSearch{
104+
Spec: MongoDBSearchSpec{
105+
LoadBalancer: &LoadBalancerConfig{Mode: LBModeManaged},
106+
},
107+
Status: MongoDBSearchStatus{
108+
LoadBalancer: &LoadBalancerStatus{Phase: status.PhaseRunning},
109+
},
110+
},
111+
expected: true,
112+
},
113+
{
114+
name: "managed LB, status Pending",
115+
search: MongoDBSearch{
116+
Spec: MongoDBSearchSpec{
117+
LoadBalancer: &LoadBalancerConfig{Mode: LBModeManaged},
118+
},
119+
Status: MongoDBSearchStatus{
120+
LoadBalancer: &LoadBalancerStatus{Phase: status.PhasePending},
121+
},
122+
},
123+
expected: false,
124+
},
125+
}
126+
127+
for _, tt := range tests {
128+
t.Run(tt.name, func(t *testing.T) {
129+
assert.Equal(t, tt.expected, tt.search.IsLoadBalancerReady())
130+
})
131+
}
132+
}

api/v1/search/status_options.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,28 @@ func NewMongoDBSearchVersionOption(version string) MongoDBSearchVersionOption {
1515
func (o MongoDBSearchVersionOption) Value() interface{} {
1616
return o.Version
1717
}
18+
19+
// SearchPart identifies which sub-status of MongoDBSearch to update.
20+
// Search-scoped to avoid polluting the shared status.Part enum.
21+
type SearchPart int
22+
23+
const (
24+
// SearchPartLoadBalancer targets status.loadBalancer.
25+
SearchPartLoadBalancer SearchPart = iota
26+
)
27+
28+
// SearchPartOption tells UpdateStatus/GetStatus/GetStatusPath which
29+
// sub-status to operate on. Analogous to status.OMPartOption.
30+
type SearchPartOption struct {
31+
Part SearchPart
32+
}
33+
34+
var _ status.Option = SearchPartOption{}
35+
36+
func NewSearchPartOption(part SearchPart) SearchPartOption {
37+
return SearchPartOption{Part: part}
38+
}
39+
40+
func (o SearchPartOption) Value() interface{} {
41+
return o.Part
42+
}

api/v1/search/zz_generated.deepcopy.go

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/mongodb.com_mongodbsearch.yaml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ spec:
2525
jsonPath: .status.version
2626
name: Version
2727
type: string
28+
- description: Current state of the managed load balancer.
29+
jsonPath: .status.loadBalancer.phase
30+
name: LoadBalancer
31+
type: string
2832
- description: The time since the MongoDB resource was created.
2933
jsonPath: .metadata.creationTimestamp
3034
name: Age
@@ -127,11 +131,6 @@ spec:
127131
required:
128132
- spec
129133
type: object
130-
image:
131-
description: |-
132-
Image is the container image for the Envoy proxy.
133-
Overrides the operator-level default (MDB_ENVOY_IMAGE env var).
134-
type: string
135134
resourceRequirements:
136135
description: |-
137136
ResourceRequirements for the Envoy container.
@@ -579,6 +578,18 @@ spec:
579578
properties:
580579
lastTransition:
581580
type: string
581+
loadBalancer:
582+
description: |-
583+
LoadBalancer reports the state of the operator-managed load balancer.
584+
Only populated when spec.lb.mode == Managed.
585+
properties:
586+
message:
587+
type: string
588+
phase:
589+
type: string
590+
required:
591+
- phase
592+
type: object
582593
message:
583594
type: string
584595
observedGeneration:

config/manager/manager.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,15 +342,15 @@ spec:
342342
- name: RELATED_IMAGE_MDB_SEARCH_IMAGE_f85ffd3e5a
343343
value: "quay.io/mongodb/mongodb-search:f85ffd3e5a"
344344
- name: RELATED_IMAGE_MDB_ENVOY_IMAGE
345-
value: "envoyproxy/envoy:v1.31-latest"
345+
value: "envoyproxy/envoy:v1.37-latest"
346346
- name: MDB_SEARCH_REPO_URL
347347
value: "quay.io/mongodb"
348348
- name: MDB_SEARCH_NAME
349349
value: "mongodb-search"
350350
- name: MDB_SEARCH_VERSION
351351
value: "f85ffd3e5a"
352352
- name: MDB_ENVOY_IMAGE
353-
value: "envoyproxy/envoy:v1.31-latest"
353+
value: "envoyproxy/envoy:v1.37-latest"
354354
volumes:
355355
- name: webhook-server-dir
356356
emptyDir: {}

0 commit comments

Comments
 (0)