Skip to content

Commit 00d54fe

Browse files
committed
add body mutator to filter config
Signed-off-by: Xiaolin Lin <xlin158@bloomberg.net>
1 parent 4758340 commit 00d54fe

File tree

2 files changed

+212
-2
lines changed

2 files changed

+212
-2
lines changed

internal/controller/gateway.go

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,20 @@ func headerMutationToFilterAPI(m *aigv1a1.HTTPHeaderMutation) *filterapi.HTTPHea
178178
return ret
179179
}
180180

181-
// TODO
181+
// bodyMutationToFilterAPI converts an aigv1a1.HTTPBodyMutation to filterapi.HTTPBodyMutation.
182182
func bodyMutationToFilterAPI(m *aigv1a1.HTTPBodyMutation) *filterapi.HTTPBodyMutation {
183-
return nil
183+
if m == nil {
184+
return nil
185+
}
186+
ret := &filterapi.HTTPBodyMutation{}
187+
ret.Remove = make([]string, 0, len(m.Remove))
188+
for _, f := range m.Remove {
189+
ret.Remove = append(ret.Remove, f)
190+
}
191+
for _, f := range m.Set {
192+
ret.Set = append(ret.Set, filterapi.HTTPBodyField{Path: f.Path, Value: f.Value})
193+
}
194+
return ret
184195
}
185196

186197
// mergeHeaderMutations merges route-level and backend-level HeaderMutation with route-level taking precedence.
@@ -230,6 +241,53 @@ func mergeHeaderMutations(routeLevel, backendLevel *aigv1a1.HTTPHeaderMutation)
230241
return result
231242
}
232243

244+
// mergeBodyMutations merges route-level and backend-level BodyMutation with route-level taking precedence.
245+
// Returns the merged BodyMutation where route-level operations override backend-level operations for conflicting fields.
246+
func mergeBodyMutations(routeLevel, backendLevel *aigv1a1.HTTPBodyMutation) *aigv1a1.HTTPBodyMutation {
247+
if routeLevel == nil {
248+
return backendLevel
249+
}
250+
if backendLevel == nil {
251+
return routeLevel
252+
}
253+
254+
result := &aigv1a1.HTTPBodyMutation{}
255+
256+
// Merge Set operations (route-level wins conflicts)
257+
fieldMap := make(map[string]aigv1a1.HTTPBodyField)
258+
259+
// Add backend-level fields first
260+
for _, f := range backendLevel.Set {
261+
fieldMap[f.Path] = f
262+
}
263+
264+
// Override with route-level fields (route-level wins)
265+
for _, f := range routeLevel.Set {
266+
fieldMap[f.Path] = f
267+
}
268+
269+
// Convert back to slice
270+
for _, f := range fieldMap {
271+
result.Set = append(result.Set, f)
272+
}
273+
274+
// Merge Remove operations (combine and deduplicate)
275+
removeMap := make(map[string]struct{})
276+
277+
for _, f := range backendLevel.Remove {
278+
removeMap[f] = struct{}{}
279+
}
280+
for _, f := range routeLevel.Remove {
281+
removeMap[f] = struct{}{}
282+
}
283+
284+
for f := range removeMap {
285+
result.Remove = append(result.Remove, f)
286+
}
287+
288+
return result
289+
}
290+
233291
// reconcileFilterConfigSecret updates the filter config secret for the external processor.
234292
func (c *GatewayController) reconcileFilterConfigSecret(
235293
ctx context.Context,
@@ -298,8 +356,17 @@ func (c *GatewayController) reconcileFilterConfigSecret(
298356
// Merge with route-level taking precedence over backend-level
299357
mergedHeaderMutation := mergeHeaderMutations(routeHeaderMutation, backendHeaderMutation)
300358

359+
// Extract BodyMutation from both route and backend levels
360+
routeBodyMutation := backendRef.BodyMutation
361+
backendBodyMutation := backendObj.Spec.BodyMutation
362+
363+
// Merge with route-level taking precedence over backend-level
364+
mergedBodyMutation := mergeBodyMutations(routeBodyMutation, backendBodyMutation)
365+
301366
// Convert to FilterAPI format
302367
b.HeaderMutation = headerMutationToFilterAPI(mergedHeaderMutation)
368+
b.BodyMutation = bodyMutationToFilterAPI(mergedBodyMutation)
369+
303370
b.Schema = schemaToFilterAPI(backendObj.Spec.APISchema)
304371
if bsp != nil {
305372
b.Auth, err = c.bspToFilterAPIBackendAuth(ctx, bsp)

internal/controller/gateway_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,3 +1170,146 @@ func Test_mergeHeaderMutations(t *testing.T) {
11701170
})
11711171
}
11721172
}
1173+
1174+
func Test_mergeBodyMutations(t *testing.T) {
1175+
tests := []struct {
1176+
name string
1177+
routeLevel *aigv1a1.HTTPBodyMutation
1178+
backendLevel *aigv1a1.HTTPBodyMutation
1179+
expected *aigv1a1.HTTPBodyMutation
1180+
}{
1181+
{
1182+
name: "both nil",
1183+
routeLevel: nil,
1184+
backendLevel: nil,
1185+
expected: nil,
1186+
},
1187+
{
1188+
name: "route nil, backend has values",
1189+
routeLevel: nil,
1190+
backendLevel: &aigv1a1.HTTPBodyMutation{
1191+
Set: []aigv1a1.HTTPBodyField{{Path: "backend_field", Value: "backend-value"}},
1192+
Remove: []string{"backend_remove"},
1193+
},
1194+
expected: &aigv1a1.HTTPBodyMutation{
1195+
Set: []aigv1a1.HTTPBodyField{{Path: "backend_field", Value: "backend-value"}},
1196+
Remove: []string{"backend_remove"},
1197+
},
1198+
},
1199+
{
1200+
name: "route has values, backend nil",
1201+
routeLevel: &aigv1a1.HTTPBodyMutation{
1202+
Set: []aigv1a1.HTTPBodyField{{Path: "route_field", Value: "route-value"}},
1203+
Remove: []string{"route_remove"},
1204+
},
1205+
backendLevel: nil,
1206+
expected: &aigv1a1.HTTPBodyMutation{
1207+
Set: []aigv1a1.HTTPBodyField{{Path: "route_field", Value: "route-value"}},
1208+
Remove: []string{"route_remove"},
1209+
},
1210+
},
1211+
{
1212+
name: "no conflicts - different fields",
1213+
routeLevel: &aigv1a1.HTTPBodyMutation{
1214+
Set: []aigv1a1.HTTPBodyField{{Path: "route_field", Value: "route-value"}},
1215+
Remove: []string{"route_remove"},
1216+
},
1217+
backendLevel: &aigv1a1.HTTPBodyMutation{
1218+
Set: []aigv1a1.HTTPBodyField{{Path: "backend_field", Value: "backend-value"}},
1219+
Remove: []string{"backend_remove"},
1220+
},
1221+
expected: &aigv1a1.HTTPBodyMutation{
1222+
Set: []aigv1a1.HTTPBodyField{
1223+
{Path: "backend_field", Value: "backend-value"},
1224+
{Path: "route_field", Value: "route-value"},
1225+
},
1226+
Remove: []string{"backend_remove", "route_remove"},
1227+
},
1228+
},
1229+
{
1230+
name: "route overrides backend for same field path",
1231+
routeLevel: &aigv1a1.HTTPBodyMutation{
1232+
Set: []aigv1a1.HTTPBodyField{{Path: "service_tier", Value: "route-value"}},
1233+
},
1234+
backendLevel: &aigv1a1.HTTPBodyMutation{
1235+
Set: []aigv1a1.HTTPBodyField{{Path: "service_tier", Value: "backend-value"}},
1236+
},
1237+
expected: &aigv1a1.HTTPBodyMutation{
1238+
Set: []aigv1a1.HTTPBodyField{{Path: "service_tier", Value: "route-value"}},
1239+
},
1240+
},
1241+
{
1242+
name: "remove operations are combined and deduplicated",
1243+
routeLevel: &aigv1a1.HTTPBodyMutation{
1244+
Remove: []string{"field1", "shared_field"},
1245+
},
1246+
backendLevel: &aigv1a1.HTTPBodyMutation{
1247+
Remove: []string{"field2", "shared_field"},
1248+
},
1249+
expected: &aigv1a1.HTTPBodyMutation{
1250+
Remove: []string{"field1", "field2", "shared_field"},
1251+
},
1252+
},
1253+
{
1254+
name: "complex merge scenario",
1255+
routeLevel: &aigv1a1.HTTPBodyMutation{
1256+
Set: []aigv1a1.HTTPBodyField{
1257+
{Path: "route_only", Value: "route-only"},
1258+
{Path: "override_field", Value: "route-wins"},
1259+
},
1260+
Remove: []string{"route_remove", "shared_remove"},
1261+
},
1262+
backendLevel: &aigv1a1.HTTPBodyMutation{
1263+
Set: []aigv1a1.HTTPBodyField{
1264+
{Path: "backend_only", Value: "backend-only"},
1265+
{Path: "override_field", Value: "backend-loses"},
1266+
},
1267+
Remove: []string{"backend_remove", "shared_remove"},
1268+
},
1269+
expected: &aigv1a1.HTTPBodyMutation{
1270+
Set: []aigv1a1.HTTPBodyField{
1271+
{Path: "backend_only", Value: "backend-only"},
1272+
{Path: "override_field", Value: "route-wins"},
1273+
{Path: "route_only", Value: "route-only"},
1274+
},
1275+
Remove: []string{"backend_remove", "route_remove", "shared_remove"},
1276+
},
1277+
},
1278+
{
1279+
name: "empty mutations",
1280+
routeLevel: &aigv1a1.HTTPBodyMutation{
1281+
Set: []aigv1a1.HTTPBodyField{},
1282+
Remove: []string{},
1283+
},
1284+
backendLevel: &aigv1a1.HTTPBodyMutation{
1285+
Set: []aigv1a1.HTTPBodyField{},
1286+
Remove: []string{},
1287+
},
1288+
expected: &aigv1a1.HTTPBodyMutation{
1289+
Set: nil,
1290+
Remove: nil,
1291+
},
1292+
},
1293+
}
1294+
1295+
for _, tt := range tests {
1296+
t.Run(tt.name, func(t *testing.T) {
1297+
result := mergeBodyMutations(tt.routeLevel, tt.backendLevel)
1298+
1299+
if tt.expected == nil {
1300+
require.Nil(t, result)
1301+
return
1302+
}
1303+
1304+
require.NotNil(t, result)
1305+
1306+
if d := cmp.Diff(tt.expected, result, cmpopts.SortSlices(func(a, b aigv1a1.HTTPBodyField) bool {
1307+
return a.Path < b.Path
1308+
}), cmpopts.SortSlices(func(a, b string) bool {
1309+
return a < b
1310+
})); d != "" {
1311+
t.Errorf("mergeBodyMutations() mismatch (-expected +got):\n%s", d)
1312+
}
1313+
})
1314+
}
1315+
}

0 commit comments

Comments
 (0)