diff --git a/pkg/ambex/transforms.go b/pkg/ambex/transforms.go index 46731b8b6f..cb1e0bf943 100644 --- a/pkg/ambex/transforms.go +++ b/pkg/ambex/transforms.go @@ -3,6 +3,9 @@ package ambex import ( // standard library "context" + "crypto/md5" + "encoding/hex" + "encoding/json" "fmt" // third-party libraries @@ -89,7 +92,7 @@ import ( // "config_source": { // "ads": {} // }, -// "route_config_name": "ambassador-listener-8443-routeconfig-0" +// "route_config_name": "ambassador-listener-8443-routeconfig-376bf87fb310abb282f452533940481d-0" // } // } // } @@ -100,7 +103,7 @@ import ( // // routes = [ // { -// "name": "ambassador-listener-8443-routeconfig-0", +// "name": "ambassador-listener-8443-routeconfig-376bf87fb310abb282f452533940481d-0", // "virtual_hosts": [ // { // "name": "ambassador-listener-8443-*", @@ -114,6 +117,10 @@ import ( // V3ListenerToRdsListener is the v3 variety of ListnerToRdsListener func V3ListenerToRdsListener(lnr *v3listener.Listener) (*v3listener.Listener, []*v3route.RouteConfiguration, error) { l := proto.Clone(lnr).(*v3listener.Listener) + + // Keep track of number of filter chain matches that hash to the same key for collisions + matchKeyIndex := make(map[string]int) + var routes []*v3route.RouteConfiguration for _, fc := range l.FilterChains { for _, f := range fc.Filters { @@ -135,10 +142,20 @@ func V3ListenerToRdsListener(lnr *v3listener.Listener) (*v3listener.Listener, [] if rc.Name == "" { // Generate a unique name for the RouteConfiguration that we can use to // correlate the listener to the RDS record. We use the listener name plus - // an index because there can be more than one route configuration + // filter_chain_match hash because there can be more than one route configuration // associated with a given listener. - rc.Name = fmt.Sprintf("%s-routeconfig-%d", l.Name, len(routes)) + filterChainMatch, _ := json.Marshal(fc.GetFilterChainMatch()) + + // Use MD5 because it's decently fast and cryptographic security isn't needed. + matchHash := md5.Sum(filterChainMatch) + matchKey := hex.EncodeToString(matchHash[:]) + + rc.Name = fmt.Sprintf("%s-routeconfig-%s-%d", l.Name, matchKey, matchKeyIndex[matchKey]) + + // Add or update map entry for this filter chain key to dedupe those that hash to the same key. + matchKeyIndex[matchKey]++ } + routes = append(routes, rc) // Now that we have extracted and named the RouteConfiguration, we change the // RouteSpecifier from the inline RouteConfig variation to RDS via ADS. This diff --git a/pkg/ambex/transforms_test.go b/pkg/ambex/transforms_test.go new file mode 100644 index 0000000000..2048496a52 --- /dev/null +++ b/pkg/ambex/transforms_test.go @@ -0,0 +1,89 @@ +package ambex + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/wrapperspb" + + v3Listener "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/config/listener/v3" + v3Route "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/config/route/v3" + v3Httpman "github.com/emissary-ingress/emissary/v3/pkg/api/envoy/extensions/filters/network/http_connection_manager/v3" + v3Wellknown "github.com/emissary-ingress/emissary/v3/pkg/envoy-control-plane/wellknown" +) + +func TestV3ListenerToRdsListener(t *testing.T) { + testRoute := &v3Route.Route_Route{ + Route: &v3Route.RouteAction{ + ClusterSpecifier: &v3Route.RouteAction_Cluster{ + Cluster: "cluster_quote_default_default", + }, + PrefixRewrite: "/", + Timeout: durationpb.New(3 * time.Second), + }, + } + + testHcm := &v3Httpman.HttpConnectionManager{ + RouteSpecifier: &v3Httpman.HttpConnectionManager_RouteConfig{ + RouteConfig: &v3Route.RouteConfiguration{ + VirtualHosts: []*v3Route.VirtualHost{{ + Name: "emissary-ingress-listener-8080-*", + Domains: []string{"*"}, + Routes: []*v3Route.Route{{ + Match: &v3Route.RouteMatch{ + PathSpecifier: &v3Route.RouteMatch_Prefix{ + Prefix: "/backend/", + }, + }, + Action: testRoute, + }}, + }}, + }, + }, + } + + anyTestHcm, err := anypb.New(testHcm) + require.NoError(t, err) + + //Create a second identical Hcm + anyTestHcm2, err := anypb.New(testHcm) + require.NoError(t, err) + + testListener := &v3Listener.Listener{ + Name: "emissary-ingress-listener-8080", + FilterChains: []*v3Listener.FilterChain{{ + Filters: []*v3Listener.Filter{{ + Name: v3Wellknown.HTTPConnectionManager, + ConfigType: &v3Listener.Filter_TypedConfig{TypedConfig: anyTestHcm}, + }, { + Name: v3Wellknown.HTTPConnectionManager, + ConfigType: &v3Listener.Filter_TypedConfig{TypedConfig: anyTestHcm2}, + }}, + FilterChainMatch: &v3Listener.FilterChainMatch{ + DestinationPort: &wrapperspb.UInt32Value{Value: uint32(8080)}, + }, + }}, + } + + _, routes, err := V3ListenerToRdsListener(testListener) + require.NoError(t, err) + + //Should have 2 routes + assert.Equal(t, 2, len(routes)) + + for i, rc := range routes { + // Confirm that the route name was transformed to the hashed version + assert.Equal(t, fmt.Sprintf("emissary-ingress-listener-8080-routeconfig-8c82e45fa3f94ab4e879543e0a1a30ac-%d", i), rc.GetName()) + + // Make sure the virtual hosts are unmodified + virtualHosts := rc.GetVirtualHosts() + assert.Equal(t, 1, len(virtualHosts)) + assert.Equal(t, "emissary-ingress-listener-8080-*", virtualHosts[0].GetName()) + assert.Equal(t, []string{"*"}, virtualHosts[0].GetDomains()) + } +}