Skip to content

Commit d65385d

Browse files
authored
NGINX Plus: dynamic upstream reloads support (#1469)
NGINX Plus: dynamic upstream reloads support Problem: One of the benefits of using NGINX Plus is the ability to dynamically update upstream servers using the API. We currently only perform nginx reloads to update upstream servers, which can be a disruptive process. Solution: If using NGINX Plus, we'll now use the N+ API to update upstream servers. This reduces the amount of reloads that we have to perform, specifically when endpoints change (scaled, for example) with no other changes.
1 parent 73f7918 commit d65385d

28 files changed

+1076
-262
lines changed

internal/mode/static/handler.go

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/go-logr/logr"
9+
ngxclient "github.com/nginxinc/nginx-plus-go-client/client"
910
apiv1 "k8s.io/api/core/v1"
1011
v1 "k8s.io/api/core/v1"
1112
"k8s.io/apimachinery/pkg/types"
@@ -98,21 +99,33 @@ func (h *eventHandlerImpl) HandleEventBatch(ctx context.Context, logger logr.Log
9899
h.handleEvent(ctx, logger, event)
99100
}
100101

101-
changed, graph := h.cfg.processor.Process()
102-
if !changed {
102+
changeType, graph := h.cfg.processor.Process()
103+
104+
var err error
105+
switch changeType {
106+
case state.NoChange:
103107
logger.Info("Handling events didn't result into NGINX configuration changes")
104108
if !h.cfg.healthChecker.ready && h.cfg.healthChecker.firstBatchError == nil {
105109
h.cfg.healthChecker.setAsReady()
106110
}
107111
return
112+
case state.EndpointsOnlyChange:
113+
h.cfg.version++
114+
err = h.updateUpstreamServers(
115+
ctx,
116+
logger,
117+
dataplane.BuildConfiguration(ctx, graph, h.cfg.serviceResolver, h.cfg.version),
118+
)
119+
case state.ClusterStateChange:
120+
h.cfg.version++
121+
err = h.updateNginxConf(
122+
ctx,
123+
dataplane.BuildConfiguration(ctx, graph, h.cfg.serviceResolver, h.cfg.version),
124+
)
108125
}
109126

110127
var nginxReloadRes nginxReloadResult
111-
h.cfg.version++
112-
if err := h.updateNginx(
113-
ctx,
114-
dataplane.BuildConfiguration(ctx, graph, h.cfg.serviceResolver, h.cfg.version),
115-
); err != nil {
128+
if err != nil {
116129
logger.Error(err, "Failed to update NGINX configuration")
117130
nginxReloadRes.error = err
118131
if !h.cfg.healthChecker.ready {
@@ -174,9 +187,9 @@ func (h *eventHandlerImpl) handleEvent(ctx context.Context, logger logr.Logger,
174187
}
175188
}
176189

177-
func (h *eventHandlerImpl) updateNginx(ctx context.Context, conf dataplane.Configuration) error {
190+
// updateNginxConf updates nginx conf files and reloads nginx
191+
func (h *eventHandlerImpl) updateNginxConf(ctx context.Context, conf dataplane.Configuration) error {
178192
files := h.cfg.generator.Generate(conf)
179-
180193
if err := h.cfg.nginxFileMgr.ReplaceFiles(files); err != nil {
181194
return fmt.Errorf("failed to replace NGINX configuration files: %w", err)
182195
}
@@ -188,6 +201,93 @@ func (h *eventHandlerImpl) updateNginx(ctx context.Context, conf dataplane.Confi
188201
return nil
189202
}
190203

204+
// updateUpstreamServers is called only when endpoints have changed. It updates nginx conf files and then:
205+
// - if using NGINX Plus, determines which servers have changed and uses the N+ API to update them;
206+
// - otherwise if not using NGINX Plus, or an error was returned from the API, reloads nginx
207+
func (h *eventHandlerImpl) updateUpstreamServers(
208+
ctx context.Context,
209+
logger logr.Logger,
210+
conf dataplane.Configuration,
211+
) error {
212+
isPlus := h.cfg.nginxRuntimeMgr.IsPlus()
213+
214+
files := h.cfg.generator.Generate(conf)
215+
if err := h.cfg.nginxFileMgr.ReplaceFiles(files); err != nil {
216+
return fmt.Errorf("failed to replace NGINX configuration files: %w", err)
217+
}
218+
219+
reload := func() error {
220+
if err := h.cfg.nginxRuntimeMgr.Reload(ctx, conf.Version); err != nil {
221+
return fmt.Errorf("failed to reload NGINX: %w", err)
222+
}
223+
224+
return nil
225+
}
226+
227+
if isPlus {
228+
type upstream struct {
229+
name string
230+
servers []ngxclient.UpstreamServer
231+
}
232+
var upstreams []upstream
233+
234+
prevUpstreams, err := h.cfg.nginxRuntimeMgr.GetUpstreams()
235+
if err != nil {
236+
logger.Error(err, "failed to get upstreams from API, reloading configuration instead")
237+
return reload()
238+
}
239+
240+
for _, u := range conf.Upstreams {
241+
upstream := upstream{
242+
name: u.Name,
243+
servers: ngxConfig.ConvertEndpoints(u.Endpoints),
244+
}
245+
246+
if u, ok := prevUpstreams[upstream.name]; ok {
247+
if !serversEqual(upstream.servers, u.Peers) {
248+
upstreams = append(upstreams, upstream)
249+
}
250+
}
251+
}
252+
253+
var reloadPlus bool
254+
for _, upstream := range upstreams {
255+
if err := h.cfg.nginxRuntimeMgr.UpdateHTTPServers(upstream.name, upstream.servers); err != nil {
256+
logger.Error(
257+
err, "couldn't update upstream via the API, reloading configuration instead",
258+
"upstreamName", upstream.name,
259+
)
260+
reloadPlus = true
261+
}
262+
}
263+
264+
if !reloadPlus {
265+
return nil
266+
}
267+
}
268+
269+
return reload()
270+
}
271+
272+
func serversEqual(newServers []ngxclient.UpstreamServer, oldServers []ngxclient.Peer) bool {
273+
if len(newServers) != len(oldServers) {
274+
return false
275+
}
276+
277+
diff := make(map[string]struct{}, len(newServers))
278+
for _, s := range newServers {
279+
diff[s.Server] = struct{}{}
280+
}
281+
282+
for _, s := range oldServers {
283+
if _, ok := diff[s.Server]; !ok {
284+
return false
285+
}
286+
}
287+
288+
return true
289+
}
290+
191291
// updateControlPlaneAndSetStatus updates the control plane configuration and then sets the status
192292
// based on the outcome
193293
func (h *eventHandlerImpl) updateControlPlaneAndSetStatus(

internal/mode/static/handler_test.go

Lines changed: 167 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"context"
55
"errors"
66

7+
ngxclient "github.com/nginxinc/nginx-plus-go-client/client"
78
. "github.com/onsi/ginkgo/v2"
89
. "github.com/onsi/gomega"
910
"go.uber.org/zap"
1011
v1 "k8s.io/api/core/v1"
12+
discoveryV1 "k8s.io/api/discovery/v1"
1113
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1214
"k8s.io/apimachinery/pkg/types"
1315
"k8s.io/client-go/tools/record"
@@ -27,6 +29,7 @@ import (
2729
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/file"
2830
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/file/filefakes"
2931
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/runtime/runtimefakes"
32+
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state"
3033
staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions"
3134
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
3235
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph"
@@ -64,6 +67,7 @@ var _ = Describe("eventHandler", func() {
6467

6568
BeforeEach(func() {
6669
fakeProcessor = &statefakes.FakeChangeProcessor{}
70+
fakeProcessor.ProcessReturns(state.NoChange, &graph.Graph{})
6771
fakeGenerator = &configfakes.FakeGenerator{}
6872
fakeNginxFileMgr = &filefakes.FakeManager{}
6973
fakeNginxRuntimeMgr = &runtimefakes.FakeManager{}
@@ -112,7 +116,7 @@ var _ = Describe("eventHandler", func() {
112116
}
113117

114118
BeforeEach(func() {
115-
fakeProcessor.ProcessReturns(true /* changed */, &graph.Graph{})
119+
fakeProcessor.ProcessReturns(state.ClusterStateChange /* changed */, &graph.Graph{})
116120

117121
fakeGenerator.GenerateReturns(fakeCfgFiles)
118122
})
@@ -280,11 +284,129 @@ var _ = Describe("eventHandler", func() {
280284
})
281285
})
282286

287+
When("receiving an EndpointsOnlyChange update", func() {
288+
e := &events.UpsertEvent{Resource: &discoveryV1.EndpointSlice{
289+
ObjectMeta: metav1.ObjectMeta{
290+
Name: "nginx-gateway",
291+
Namespace: "nginx-gateway",
292+
},
293+
}}
294+
batch := []interface{}{e}
295+
296+
BeforeEach(func() {
297+
fakeProcessor.ProcessReturns(state.EndpointsOnlyChange, &graph.Graph{})
298+
upstreams := ngxclient.Upstreams{
299+
"one": ngxclient.Upstream{
300+
Peers: []ngxclient.Peer{
301+
{Server: "server1"},
302+
},
303+
},
304+
}
305+
fakeNginxRuntimeMgr.GetUpstreamsReturns(upstreams, nil)
306+
})
307+
308+
When("running NGINX Plus", func() {
309+
It("should call the NGINX Plus API", func() {
310+
fakeNginxRuntimeMgr.IsPlusReturns(true)
311+
312+
handler.HandleEventBatch(context.Background(), ctlrZap.New(), batch)
313+
Expect(fakeGenerator.GenerateCallCount()).To(Equal(1))
314+
Expect(fakeNginxFileMgr.ReplaceFilesCallCount()).To(Equal(1))
315+
Expect(fakeNginxRuntimeMgr.GetUpstreamsCallCount()).To(Equal(1))
316+
})
317+
})
318+
319+
When("not running NGINX Plus", func() {
320+
It("should not call the NGINX Plus API", func() {
321+
handler.HandleEventBatch(context.Background(), ctlrZap.New(), batch)
322+
Expect(fakeGenerator.GenerateCallCount()).To(Equal(1))
323+
Expect(fakeNginxFileMgr.ReplaceFilesCallCount()).To(Equal(1))
324+
Expect(fakeNginxRuntimeMgr.GetUpstreamsCallCount()).To(Equal(0))
325+
Expect(fakeNginxRuntimeMgr.ReloadCallCount()).To(Equal(1))
326+
})
327+
})
328+
})
329+
330+
When("updating upstream servers", func() {
331+
conf := dataplane.Configuration{
332+
Upstreams: []dataplane.Upstream{
333+
{
334+
Name: "one",
335+
},
336+
},
337+
}
338+
339+
type callCounts struct {
340+
generate int
341+
update int
342+
reload int
343+
}
344+
345+
assertCallCounts := func(cc callCounts) {
346+
Expect(fakeGenerator.GenerateCallCount()).To(Equal(cc.generate))
347+
Expect(fakeNginxFileMgr.ReplaceFilesCallCount()).To(Equal(cc.generate))
348+
Expect(fakeNginxRuntimeMgr.UpdateHTTPServersCallCount()).To(Equal(cc.update))
349+
Expect(fakeNginxRuntimeMgr.ReloadCallCount()).To(Equal(cc.reload))
350+
}
351+
352+
BeforeEach(func() {
353+
upstreams := ngxclient.Upstreams{
354+
"one": ngxclient.Upstream{
355+
Peers: []ngxclient.Peer{
356+
{Server: "server1"},
357+
},
358+
},
359+
}
360+
fakeNginxRuntimeMgr.GetUpstreamsReturns(upstreams, nil)
361+
})
362+
363+
When("running NGINX Plus", func() {
364+
BeforeEach(func() {
365+
fakeNginxRuntimeMgr.IsPlusReturns(true)
366+
})
367+
368+
It("should update servers using the NGINX Plus API", func() {
369+
Expect(handler.updateUpstreamServers(context.Background(), ctlrZap.New(), conf)).To(Succeed())
370+
371+
assertCallCounts(callCounts{generate: 1, update: 1, reload: 0})
372+
})
373+
374+
It("should reload when GET API returns an error", func() {
375+
fakeNginxRuntimeMgr.GetUpstreamsReturns(nil, errors.New("error"))
376+
Expect(handler.updateUpstreamServers(context.Background(), ctlrZap.New(), conf)).To(Succeed())
377+
378+
assertCallCounts(callCounts{generate: 1, update: 0, reload: 1})
379+
})
380+
381+
It("should reload when POST API returns an error", func() {
382+
fakeNginxRuntimeMgr.UpdateHTTPServersReturns(errors.New("error"))
383+
Expect(handler.updateUpstreamServers(context.Background(), ctlrZap.New(), conf)).To(Succeed())
384+
385+
assertCallCounts(callCounts{generate: 1, update: 1, reload: 1})
386+
})
387+
})
388+
389+
When("not running NGINX Plus", func() {
390+
It("should update servers by reloading", func() {
391+
Expect(handler.updateUpstreamServers(context.Background(), ctlrZap.New(), conf)).To(Succeed())
392+
393+
assertCallCounts(callCounts{generate: 1, update: 0, reload: 1})
394+
})
395+
396+
It("should return an error when reloading fails", func() {
397+
fakeNginxRuntimeMgr.ReloadReturns(errors.New("error"))
398+
Expect(handler.updateUpstreamServers(context.Background(), ctlrZap.New(), conf)).ToNot(Succeed())
399+
400+
assertCallCounts(callCounts{generate: 1, update: 0, reload: 1})
401+
})
402+
})
403+
})
404+
283405
It("should set the health checker status properly when there are changes", func() {
284406
e := &events.UpsertEvent{Resource: &gatewayv1.HTTPRoute{}}
285407
batch := []interface{}{e}
286408

287-
fakeProcessor.ProcessReturns(true, &graph.Graph{})
409+
fakeProcessor.ProcessReturns(state.ClusterStateChange, &graph.Graph{})
288410

289411
Expect(handler.cfg.healthChecker.readyCheck(nil)).ToNot(Succeed())
290412
handler.HandleEventBatch(context.Background(), ctlrZap.New(), batch)
@@ -304,22 +426,22 @@ var _ = Describe("eventHandler", func() {
304426
e := &events.UpsertEvent{Resource: &gatewayv1.HTTPRoute{}}
305427
batch := []interface{}{e}
306428

307-
fakeProcessor.ProcessReturns(true, &graph.Graph{})
429+
fakeProcessor.ProcessReturns(state.ClusterStateChange, &graph.Graph{})
308430
fakeNginxRuntimeMgr.ReloadReturns(errors.New("reload error"))
309431

310432
handler.HandleEventBatch(context.Background(), ctlrZap.New(), batch)
311433

312434
Expect(handler.cfg.healthChecker.readyCheck(nil)).ToNot(Succeed())
313435

314436
// now send an update with no changes; should still return an error
315-
fakeProcessor.ProcessReturns(false, &graph.Graph{})
437+
fakeProcessor.ProcessReturns(state.NoChange, &graph.Graph{})
316438

317439
handler.HandleEventBatch(context.Background(), ctlrZap.New(), batch)
318440

319441
Expect(handler.cfg.healthChecker.readyCheck(nil)).ToNot(Succeed())
320442

321443
// error goes away
322-
fakeProcessor.ProcessReturns(true, &graph.Graph{})
444+
fakeProcessor.ProcessReturns(state.ClusterStateChange, &graph.Graph{})
323445
fakeNginxRuntimeMgr.ReloadReturns(nil)
324446

325447
handler.HandleEventBatch(context.Background(), ctlrZap.New(), batch)
@@ -339,6 +461,46 @@ var _ = Describe("eventHandler", func() {
339461
})
340462
})
341463

464+
var _ = Describe("serversEqual", func() {
465+
DescribeTable("determines if server lists are equal",
466+
func(newServers []ngxclient.UpstreamServer, oldServers []ngxclient.Peer, equal bool) {
467+
Expect(serversEqual(newServers, oldServers)).To(Equal(equal))
468+
},
469+
Entry("different length",
470+
[]ngxclient.UpstreamServer{
471+
{Server: "server1"},
472+
},
473+
[]ngxclient.Peer{
474+
{Server: "server1"},
475+
{Server: "server2"},
476+
},
477+
false,
478+
),
479+
Entry("differing elements",
480+
[]ngxclient.UpstreamServer{
481+
{Server: "server1"},
482+
{Server: "server2"},
483+
},
484+
[]ngxclient.Peer{
485+
{Server: "server1"},
486+
{Server: "server3"},
487+
},
488+
false,
489+
),
490+
Entry("same elements",
491+
[]ngxclient.UpstreamServer{
492+
{Server: "server1"},
493+
{Server: "server2"},
494+
},
495+
[]ngxclient.Peer{
496+
{Server: "server1"},
497+
{Server: "server2"},
498+
},
499+
true,
500+
),
501+
)
502+
})
503+
342504
var _ = Describe("getGatewayAddresses", func() {
343505
It("gets gateway addresses from a Service", func() {
344506
fakeClient := fake.NewFakeClient()

0 commit comments

Comments
 (0)