Skip to content

Commit ded1099

Browse files
committed
add session persistence support for NGINX plus users
1 parent 2d689a5 commit ded1099

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
@@ -119,6 +119,7 @@ const (
119119

120120
// Upstream holds all configuration for an HTTP upstream.
121121
type Upstream struct {
122+
SessionPersistence UpstreamSessionPersistence
122123
Name string
123124
ZoneSize string // format: 512k, 1m
124125
StateFile string
@@ -127,6 +128,14 @@ type Upstream struct {
127128
Servers []UpstreamServer
128129
}
129130

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

internal/controller/nginx/config/upstreams.go

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

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

161164
if upstreamPolicySettings.ZoneSize != "" {
@@ -199,6 +202,7 @@ func (g GeneratorImpl) createUpstream(
199202
Servers: upstreamServers,
200203
KeepAlive: upstreamPolicySettings.KeepAlive,
201204
LoadBalancingMethod: chosenLBMethod,
205+
SessionPersistence: sp,
202206
}
203207
}
204208

@@ -223,3 +227,17 @@ func upstreamHasResolveServers(upstream dataplane.Upstream) bool {
223227
}
224228
return false
225229
}
230+
231+
// getSessionPersistenceConfiguration gets the session persistence configuration for an upstream.
232+
// Supported only for NGINX Plus and cookie-based type.
233+
func getSessionPersistenceConfiguration(sp dataplane.SessionPersistenceConfig) http.UpstreamSessionPersistence {
234+
if sp.Name == "" {
235+
return http.UpstreamSessionPersistence{}
236+
}
237+
return http.UpstreamSessionPersistence{
238+
Name: sp.Name,
239+
Expiry: sp.Expiry,
240+
Path: sp.Path,
241+
SessionType: string(sp.SessionType),
242+
}
243+
}

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;": 3,
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{}
@@ -707,6 +908,41 @@ func TestCreateUpstreamPlus(t *testing.T) {
707908
},
708909
},
709910
},
911+
{
912+
msg: "session persistence config with endpoints",
913+
stateUpstream: dataplane.Upstream{
914+
Name: "sp-with-endpoints",
915+
Endpoints: []resolver.Endpoint{
916+
{
917+
Address: "10.0.0.2",
918+
Port: 80,
919+
},
920+
},
921+
SessionPersistence: dataplane.SessionPersistenceConfig{
922+
Name: "session-persistence",
923+
Expiry: "45m",
924+
SessionType: dataplane.SessionPersistenceCookie,
925+
Path: "/app",
926+
},
927+
},
928+
expectedUpstream: http.Upstream{
929+
Name: "sp-with-endpoints",
930+
ZoneSize: plusZoneSize,
931+
StateFile: stateDir + "/sp-with-endpoints.conf",
932+
Servers: []http.UpstreamServer{
933+
{
934+
Address: "10.0.0.2:80",
935+
},
936+
},
937+
LoadBalancingMethod: defaultLBMethod,
938+
SessionPersistence: http.UpstreamSessionPersistence{
939+
Name: "session-persistence",
940+
Expiry: "45m",
941+
SessionType: string(dataplane.SessionPersistenceCookie),
942+
Path: "/app",
943+
},
944+
},
945+
},
710946
}
711947

712948
for _, test := range tests {

0 commit comments

Comments
 (0)