88
99 clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
1010 routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
11+ "google.golang.org/protobuf/types/known/anypb"
1112
1213 extcache "go.datum.net/network-services-operator/internal/extensionserver/cache"
1314)
@@ -206,9 +207,10 @@ func TestApplyConnectorRoutes_Online_PrependsCONNECTRouteAndAppendsTargetHost(t
206207 },
207208 }
208209
209- n , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
210+ n , converted , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
210211 require .NoError (t , err )
211212 assert .Equal (t , 1 , n , "one VH should be mutated" )
213+ assert .Equal (t , 0 , converted , "online connector must not convert any forwarding routes" )
212214
213215 vh := rc .VirtualHosts [0 ]
214216 require .Len (t , vh .Routes , 2 , "CONNECT route must be prepended; want 2 routes total" )
@@ -252,7 +254,7 @@ func TestApplyConnectorRoutes_Online_TargetHostDeduplicated(t *testing.T) {
252254 },
253255 }
254256
255- _ , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
257+ _ , _ , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
256258 require .NoError (t , err )
257259
258260 // Domain must not be duplicated.
@@ -288,27 +290,139 @@ func TestApplyConnectorRoutes_Offline_Prepends503Route_NoDomain(t *testing.T) {
288290 },
289291 }
290292
291- n , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
293+ n , converted , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
292294 require .NoError (t , err )
293295 assert .Equal (t , 1 , n , "offline VH must be mutated (503 route prepended)" )
296+ assert .Equal (t , 1 , converted , "the user-facing forwarding route must be converted to a direct_response" )
294297
295298 vh := rc .VirtualHosts [0 ]
296299 require .Len (t , vh .Routes , 2 , "503 direct_response route must be prepended" )
297300
301+ // First route: the connect_matcher offline route for CONNECT clients.
298302 offlineRoute := vh .Routes [0 ]
299303 dr := offlineRoute .GetDirectResponse ()
300304 require .NotNil (t , dr , "first route must be a direct_response" )
305+ assert .NotNil (t , offlineRoute .GetMatch ().GetConnectMatcher (),
306+ "prepended offline route must keep its connect_matcher" )
301307 assert .Equal (t , uint32 (503 ), dr .GetStatus (),
302308 "offline route must return 503" )
303309 assert .Equal (t , "Tunnel not online" , dr .GetBody ().GetInlineString (),
304310 "offline route body must be 'Tunnel not online' per STATE.md contract" )
305311
312+ // Second route: the original user-facing forwarding route must now be a
313+ // direct_response 503 too, NOT a cluster route to the empty cluster.
314+ fwd := vh .Routes [1 ]
315+ assert .Equal (t , "fwd" , fwd .GetName (), "forwarding route identity preserved" )
316+ assert .Empty (t , routeCluster (fwd ),
317+ "forwarding route must no longer target the endpoint-less connector cluster" )
318+ fwdDR := fwd .GetDirectResponse ()
319+ require .NotNil (t , fwdDR , "forwarding route must be converted to a direct_response" )
320+ assert .Equal (t , uint32 (503 ), fwdDR .GetStatus (), "converted forwarding route must return 503" )
321+ assert .Equal (t , "Tunnel not online" , fwdDR .GetBody ().GetInlineString (),
322+ "converted forwarding route must reuse the offline body" )
323+
306324 // No domain must be appended for offline connectors.
307325 assert .NotContains (t , vh .Domains , testTargetHost ,
308326 "target host must NOT be appended for offline connector" )
309327 assert .Len (t , vh .Domains , 1 , "domains must remain unchanged for offline connector" )
310328}
311329
330+ // TestApplyConnectorRoutes_Offline_PreservesMatchAndConfig verifies that
331+ // converting a forwarding route to a direct_response only replaces the Action
332+ // oneof — the route's match and typed_per_filter_config survive — and that a
333+ // co-located non-connector route in the same VH is untouched.
334+ func TestApplyConnectorRoutes_Offline_PreservesMatchAndUntouchedRoute (t * testing.T ) {
335+ idx := connectorPolicyIndex (false )
336+ clusterName := testClusterName ()
337+ offlineInfo := & extcache.ConnectorInfo {Online : false , TargetHost : testTargetHost , TargetPort : testTargetPort }
338+ replaced := map [string ]* extcache.ConnectorInfo {}
339+ offline := map [string ]* extcache.ConnectorInfo {clusterName : offlineInfo }
340+
341+ // Connector forwarding route carries a prefix match + typed_per_filter_config.
342+ connRoute := routeTargeting (clusterName )
343+ connRoute .Match = & routev3.RouteMatch {
344+ PathSpecifier : & routev3.RouteMatch_Prefix {Prefix : "/" },
345+ }
346+ connRoute .TypedPerFilterConfig = map [string ]* anypb.Any {
347+ "envoy.filters.http.cors" : {TypeUrl : "type.googleapis.com/example.Cfg" },
348+ }
349+
350+ // A second route in the same VH targets an unrelated cluster.
351+ otherRoute := routeTargeting ("infra-cluster" )
352+ otherRoute .Name = "other"
353+
354+ rc := & routev3.RouteConfiguration {
355+ VirtualHosts : []* routev3.VirtualHost {
356+ {
357+ Name : "vh" ,
358+ Domains : []string {"app.local.test" },
359+ Routes : []* routev3.Route {connRoute , otherRoute },
360+ },
361+ },
362+ }
363+
364+ _ , converted , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
365+ require .NoError (t , err )
366+ assert .Equal (t , 1 , converted , "only the connector forwarding route must be converted" )
367+
368+ vh := rc .VirtualHosts [0 ]
369+ require .Len (t , vh .Routes , 3 , "connect_matcher route prepended to the two originals" )
370+
371+ // Converted forwarding route: match + typed_per_filter_config preserved.
372+ gotConn := vh .Routes [1 ]
373+ require .NotNil (t , gotConn .GetDirectResponse (), "connector forwarding route must be a direct_response" )
374+ assert .Equal (t , "/" , gotConn .GetMatch ().GetPrefix (), "prefix match must be preserved" )
375+ assert .Contains (t , gotConn .GetTypedPerFilterConfig (), "envoy.filters.http.cors" ,
376+ "typed_per_filter_config must be preserved on the converted route" )
377+
378+ // The non-connector route must be completely untouched.
379+ gotOther := vh .Routes [2 ]
380+ assert .Equal (t , "other" , gotOther .GetName ())
381+ assert .Equal (t , "infra-cluster" , routeCluster (gotOther ),
382+ "non-connector route must still target its cluster" )
383+ assert .Nil (t , gotOther .GetDirectResponse (), "non-connector route must not be converted" )
384+ }
385+
386+ // TestApplyConnectorRoutes_Offline_Idempotent verifies a second pass does not
387+ // double-apply: the connect_matcher route is prepended once more (it is keyed by
388+ // VH name and matches no cluster), but no forwarding route is re-converted
389+ // because converted routes are direct_responses and no longer target the cluster.
390+ func TestApplyConnectorRoutes_Offline_Idempotent (t * testing.T ) {
391+ idx := connectorPolicyIndex (false )
392+ clusterName := testClusterName ()
393+ offlineInfo := & extcache.ConnectorInfo {Online : false , TargetHost : testTargetHost , TargetPort : testTargetPort }
394+ replaced := map [string ]* extcache.ConnectorInfo {}
395+ offline := map [string ]* extcache.ConnectorInfo {clusterName : offlineInfo }
396+
397+ rc := & routev3.RouteConfiguration {
398+ VirtualHosts : []* routev3.VirtualHost {
399+ {
400+ Name : "vh" ,
401+ Domains : []string {"app.local.test" },
402+ Routes : []* routev3.Route {routeTargeting (clusterName )},
403+ },
404+ },
405+ }
406+
407+ _ , converted1 , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
408+ require .NoError (t , err )
409+ assert .Equal (t , 1 , converted1 , "first pass converts the forwarding route" )
410+
411+ // Second pass: the cluster is gone from all routes, so nothing converts.
412+ _ , converted2 , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
413+ require .NoError (t , err )
414+ assert .Equal (t , 0 , converted2 , "second pass must not re-convert any route" )
415+
416+ // Every direct_response route still returns the offline 503 body.
417+ for _ , rt := range rc .VirtualHosts [0 ].Routes {
418+ if dr := rt .GetDirectResponse (); dr != nil {
419+ assert .Equal (t , uint32 (503 ), dr .GetStatus ())
420+ assert .Equal (t , "Tunnel not online" , dr .GetBody ().GetInlineString ())
421+ }
422+ assert .Empty (t , routeCluster (rt ), "no route may target the offline connector cluster after conversion" )
423+ }
424+ }
425+
312426func TestApplyConnectorRoutes_NoConnector_VHUntouched (t * testing.T ) {
313427 idx := connectorPolicyIndex (true )
314428
@@ -326,9 +440,10 @@ func TestApplyConnectorRoutes_NoConnector_VHUntouched(t *testing.T) {
326440 },
327441 }
328442
329- n , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
443+ n , converted , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
330444 require .NoError (t , err )
331445 assert .Equal (t , 0 , n , "VH with non-connector cluster must not be mutated" )
446+ assert .Equal (t , 0 , converted , "no forwarding routes converted when no connector present" )
332447 assert .Len (t , rc .VirtualHosts [0 ].Routes , 1 , "route count must not change" )
333448 assert .Len (t , rc .VirtualHosts [0 ].Domains , 1 , "domain list must not change" )
334449}
@@ -340,7 +455,8 @@ func TestApplyConnectorRoutes_EmptyRouteConfiguration_NoOp(t *testing.T) {
340455
341456 rc := & routev3.RouteConfiguration {Name : "empty" }
342457
343- n , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
458+ n , converted , err := ApplyConnectorRoutes (rc , idx , replaced , offline )
344459 require .NoError (t , err )
345460 assert .Equal (t , 0 , n )
461+ assert .Equal (t , 0 , converted )
346462}
0 commit comments