Skip to content

Commit d44ebda

Browse files
committed
add session persistence support for NGINX plus users
1 parent 948404e commit d44ebda

22 files changed

+2466
-185
lines changed

internal/controller/nginx/config/http/config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const (
120120

121121
// Upstream holds all configuration for an HTTP upstream.
122122
type Upstream struct {
123+
SessionPersistence UpstreamSessionPersistence
123124
Name string
124125
ZoneSize string // format: 512k, 1m
125126
StateFile string
@@ -129,6 +130,14 @@ type Upstream struct {
129130
Servers []UpstreamServer
130131
}
131132

133+
// UpstreamSessionPersistence holds the session persistence configuration for an upstream.
134+
type UpstreamSessionPersistence struct {
135+
Name string
136+
Expiry string
137+
Path string
138+
SessionType string
139+
}
140+
132141
// UpstreamKeepAlive holds the keepalive configuration for an HTTP upstream.
133142
type UpstreamKeepAlive struct {
134143
Time string

internal/controller/nginx/config/upstreams.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ func (g GeneratorImpl) createUpstream(
147147
processor upstreamsettings.Processor,
148148
) http.Upstream {
149149
var stateFile string
150+
var sp http.UpstreamSessionPersistence
150151
upstreamPolicySettings := processor.Process(up.Policies)
151152

152153
zoneSize := ossZoneSize
@@ -157,6 +158,8 @@ func (g GeneratorImpl) createUpstream(
157158
if !upstreamHasResolveServers(up) {
158159
stateFile = fmt.Sprintf("%s/%s.conf", stateDir, up.Name)
159160
}
161+
162+
sp = getSessionPersistenceConfiguration(up.SessionPersistence)
160163
}
161164

162165
if upstreamPolicySettings.ZoneSize != "" {
@@ -212,6 +215,7 @@ func (g GeneratorImpl) createUpstream(
212215
Servers: upstreamServers,
213216
KeepAlive: upstreamPolicySettings.KeepAlive,
214217
LoadBalancingMethod: chosenLBMethod,
218+
SessionPersistence: sp,
215219
}
216220
}
217221

@@ -236,3 +240,17 @@ func upstreamHasResolveServers(upstream dataplane.Upstream) bool {
236240
}
237241
return false
238242
}
243+
244+
// getSessionPersistenceConfiguration gets the session persistence configuration for an upstream.
245+
// Supported only for NGINX Plus and cookie-based type.
246+
func getSessionPersistenceConfiguration(sp dataplane.SessionPersistenceConfig) http.UpstreamSessionPersistence {
247+
if sp.Name == "" {
248+
return http.UpstreamSessionPersistence{}
249+
}
250+
return http.UpstreamSessionPersistence{
251+
Name: sp.Name,
252+
Expiry: sp.Expiry,
253+
Path: sp.Path,
254+
SessionType: string(sp.SessionType),
255+
}
256+
}

internal/controller/nginx/config/upstreams_template.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ upstream {{ $u.Name }} {
1717
zone {{ $u.Name }} {{ $u.ZoneSize }};
1818
{{ end -}}
1919
20+
{{ if $u.SessionPersistence.Name -}}
21+
sticky {{ $u.SessionPersistence.SessionType }} {{ $u.SessionPersistence.Name }}
22+
{{- if $u.SessionPersistence.Expiry }} expires={{ $u.SessionPersistence.Expiry }}{{- end }}
23+
{{- if $u.SessionPersistence.Path }} path={{ $u.SessionPersistence.Path }}{{- end }};
24+
{{ end -}}
25+
2026
{{- if $u.StateFile }}
2127
state {{ $u.StateFile }};
2228
{{- else }}

internal/controller/nginx/config/upstreams_test.go

Lines changed: 239 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ import (
1919
"github.com/nginx/nginx-gateway-fabric/v2/internal/framework/helpers"
2020
)
2121

22-
func TestExecuteUpstreams(t *testing.T) {
22+
func TestExecuteUpstreams_NginxOSS(t *testing.T) {
2323
t.Parallel()
24-
gen := GeneratorImpl{}
24+
gen := GeneratorImpl{
25+
plus: false,
26+
}
2527
stateUpstreams := []dataplane.Upstream{
2628
{
2729
Name: "up1",
@@ -102,9 +104,14 @@ func TestExecuteUpstreams(t *testing.T) {
102104
"keepalive_requests 1;": 1,
103105
"keepalive_time 5s;": 1,
104106
"keepalive_timeout 10s;": 1,
105-
"zone up5-usp 2m;": 1,
106107
"ip_hash;": 1,
107108

109+
"zone up1 512k;": 1,
110+
"zone up2 512k;": 1,
111+
"zone up3 512k;": 1,
112+
"zone up4-ipv6 512k;": 1,
113+
"zone up5-usp 2m;": 1,
114+
108115
"random two least_conn;": 4,
109116
}
110117

@@ -125,6 +132,200 @@ func TestExecuteUpstreams(t *testing.T) {
125132
}
126133
}
127134

135+
func TestExecuteUpstreams_NginxPlus(t *testing.T) {
136+
t.Parallel()
137+
gen := GeneratorImpl{
138+
plus: true,
139+
}
140+
stateUpstreams := []dataplane.Upstream{
141+
{
142+
Name: "up1",
143+
Endpoints: []resolver.Endpoint{
144+
{
145+
Address: "10.0.0.0",
146+
Port: 80,
147+
Resolve: true,
148+
},
149+
},
150+
},
151+
{
152+
Name: "up2",
153+
Endpoints: []resolver.Endpoint{
154+
{
155+
Address: "11.0.0.0",
156+
Port: 80,
157+
Resolve: true,
158+
},
159+
{
160+
Address: "11.0.0.1",
161+
Port: 80,
162+
Resolve: true,
163+
},
164+
{
165+
Address: "11.0.0.2",
166+
Port: 80,
167+
},
168+
},
169+
},
170+
{
171+
Name: "up3-ipv6",
172+
Endpoints: []resolver.Endpoint{
173+
{
174+
Address: "2001:db8::1",
175+
Port: 80,
176+
IPv6: true,
177+
Resolve: true,
178+
},
179+
},
180+
},
181+
{
182+
Name: "up4-ipv6",
183+
Endpoints: []resolver.Endpoint{
184+
{
185+
Address: "2001:db8::2",
186+
Port: 80,
187+
IPv6: true,
188+
Resolve: true,
189+
},
190+
{
191+
Address: "2001:db8::3",
192+
Port: 80,
193+
IPv6: true,
194+
},
195+
},
196+
},
197+
{
198+
Name: "up5",
199+
Endpoints: []resolver.Endpoint{},
200+
},
201+
{
202+
Name: "up6-usp-with-sp",
203+
Endpoints: []resolver.Endpoint{
204+
{
205+
Address: "12.0.0.1",
206+
Port: 80,
207+
Resolve: true,
208+
},
209+
},
210+
Policies: []policies.Policy{
211+
&ngfAPI.UpstreamSettingsPolicy{
212+
ObjectMeta: metav1.ObjectMeta{
213+
Name: "usp",
214+
Namespace: "test",
215+
},
216+
Spec: ngfAPI.UpstreamSettingsPolicySpec{
217+
ZoneSize: helpers.GetPointer[ngfAPI.Size]("2m"),
218+
KeepAlive: helpers.GetPointer(ngfAPI.UpstreamKeepAlive{
219+
Connections: helpers.GetPointer(int32(1)),
220+
Requests: helpers.GetPointer(int32(1)),
221+
Time: helpers.GetPointer[ngfAPI.Duration]("5s"),
222+
Timeout: helpers.GetPointer[ngfAPI.Duration]("10s"),
223+
}),
224+
LoadBalancingMethod: helpers.GetPointer(ngfAPI.LoadBalancingTypeIPHash),
225+
},
226+
},
227+
},
228+
SessionPersistence: dataplane.SessionPersistenceConfig{
229+
Name: "session-persistence",
230+
Expiry: "30m",
231+
Path: "/session",
232+
SessionType: dataplane.SessionPersistenceCookie,
233+
},
234+
},
235+
{
236+
Name: "up7-with-sp",
237+
Endpoints: []resolver.Endpoint{
238+
{
239+
Address: "12.0.0.2",
240+
Port: 80,
241+
Resolve: true,
242+
},
243+
},
244+
SessionPersistence: dataplane.SessionPersistenceConfig{
245+
Name: "session-persistence",
246+
Expiry: "100h",
247+
Path: "/v1/users",
248+
SessionType: dataplane.SessionPersistenceCookie,
249+
},
250+
},
251+
{
252+
Name: "up8-with-sp-expiry-and-path-empty",
253+
Endpoints: []resolver.Endpoint{
254+
{
255+
Address: "12.0.0.3",
256+
Port: 80,
257+
Resolve: true,
258+
},
259+
},
260+
SessionPersistence: dataplane.SessionPersistenceConfig{
261+
Name: "session-persistence",
262+
SessionType: dataplane.SessionPersistenceCookie,
263+
},
264+
},
265+
}
266+
267+
expectedSubStrings := map[string]int{
268+
"upstream up1": 1,
269+
"upstream up2": 1,
270+
"upstream up3-ipv6": 1,
271+
"upstream up4-ipv6": 1,
272+
"upstream up5": 1,
273+
"upstream up6-usp-with-sp": 1,
274+
"upstream up7-with-sp": 1,
275+
"upstream up8-with-sp-expiry-and-path-empty": 1,
276+
"upstream invalid-backend-ref": 1,
277+
278+
"random two least_conn;": 6,
279+
"ip_hash;": 1,
280+
281+
"zone up1 1m;": 1,
282+
"zone up2 1m;": 1,
283+
"zone up3-ipv6 1m;": 1,
284+
"zone up4-ipv6 1m;": 1,
285+
"zone up5 1m;": 1,
286+
"zone up6-usp-with-sp 2m;": 1,
287+
"zone up7-with-sp 1m;": 1,
288+
"zone up8-with-sp-expiry-and-path-empty 1m;": 1,
289+
290+
"sticky cookie session-persistence expires=30m path=/session;": 1,
291+
"sticky cookie session-persistence expires=100h path=/v1/users;": 1,
292+
"sticky cookie session-persistence;": 1,
293+
294+
"keepalive 1;": 1,
295+
"keepalive_requests 1;": 1,
296+
"keepalive_time 5s;": 1,
297+
"keepalive_timeout 10s;": 1,
298+
299+
"server 10.0.0.0:80 resolve;": 1,
300+
"server 11.0.0.0:80 resolve;": 1,
301+
"server 11.0.0.1:80 resolve;": 1,
302+
"server 11.0.0.2:80;": 1,
303+
"server [2001:db8::1]:80 resolve;": 1,
304+
"server [2001:db8::2]:80 resolve;": 1,
305+
"server [2001:db8::3]:80;": 1,
306+
"server 12.0.0.1:80 resolve;": 1,
307+
"server 12.0.0.2:80 resolve;": 1,
308+
"server 12.0.0.3:80 resolve;": 1,
309+
"server unix:/var/run/nginx/nginx-500-server.sock;": 1,
310+
}
311+
312+
upstreams := gen.createUpstreams(stateUpstreams, upstreamsettings.NewProcessor())
313+
314+
upstreamResults := executeUpstreams(upstreams)
315+
g := NewWithT(t)
316+
g.Expect(upstreamResults).To(HaveLen(1))
317+
g.Expect(upstreamResults[0].dest).To(Equal(httpConfigFile))
318+
319+
nginxUpstreams := string(upstreamResults[0].data)
320+
for expSubString, expectedCount := range expectedSubStrings {
321+
actualCount := strings.Count(nginxUpstreams, expSubString)
322+
g.Expect(actualCount).To(
323+
Equal(expectedCount),
324+
fmt.Sprintf("substring %q expected %d occurrence(s), got %d", expSubString, expectedCount, actualCount),
325+
)
326+
}
327+
}
328+
128329
func TestCreateUpstreams(t *testing.T) {
129330
t.Parallel()
130331
gen := GeneratorImpl{}
@@ -711,6 +912,41 @@ func TestCreateUpstreamPlus(t *testing.T) {
711912
LoadBalancingMethod: defaultLBMethod,
712913
},
713914
},
915+
{
916+
msg: "session persistence config with endpoints",
917+
stateUpstream: dataplane.Upstream{
918+
Name: "sp-with-endpoints",
919+
Endpoints: []resolver.Endpoint{
920+
{
921+
Address: "10.0.0.2",
922+
Port: 80,
923+
},
924+
},
925+
SessionPersistence: dataplane.SessionPersistenceConfig{
926+
Name: "session-persistence",
927+
Expiry: "45m",
928+
SessionType: dataplane.SessionPersistenceCookie,
929+
Path: "/app",
930+
},
931+
},
932+
expectedUpstream: http.Upstream{
933+
Name: "sp-with-endpoints",
934+
ZoneSize: plusZoneSize,
935+
StateFile: stateDir + "/sp-with-endpoints.conf",
936+
Servers: []http.UpstreamServer{
937+
{
938+
Address: "10.0.0.2:80",
939+
},
940+
},
941+
LoadBalancingMethod: defaultLBMethod,
942+
SessionPersistence: http.UpstreamSessionPersistence{
943+
Name: "session-persistence",
944+
Expiry: "45m",
945+
SessionType: string(dataplane.SessionPersistenceCookie),
946+
Path: "/app",
947+
},
948+
},
949+
},
714950
}
715951

716952
for _, test := range tests {

0 commit comments

Comments
 (0)