github.com/cilium/cilium@v1.16.2/operator/pkg/model/translation/envoy_virtual_host_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package translation
     5  
     6  import (
     7  	"fmt"
     8  	"regexp"
     9  	"sort"
    10  	"testing"
    11  
    12  	envoy_config_route_v3 "github.com/cilium/proxy/go/envoy/config/route/v3"
    13  	envoy_type_matcher_v3 "github.com/cilium/proxy/go/envoy/type/matcher/v3"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/cilium/cilium/operator/pkg/model"
    18  )
    19  
    20  func TestSortableRoute(t *testing.T) {
    21  	arr := SortableRoute{
    22  		{
    23  			Name: "regex match short",
    24  			Match: &envoy_config_route_v3.RouteMatch{
    25  				PathSpecifier: &envoy_config_route_v3.RouteMatch_SafeRegex{
    26  					SafeRegex: &envoy_type_matcher_v3.RegexMatcher{
    27  						Regex: "/.*",
    28  					},
    29  				},
    30  			},
    31  		},
    32  		{
    33  			Name: "regex match long",
    34  			Match: &envoy_config_route_v3.RouteMatch{
    35  				PathSpecifier: &envoy_config_route_v3.RouteMatch_SafeRegex{
    36  					SafeRegex: &envoy_type_matcher_v3.RegexMatcher{
    37  						Regex: "/regex/.*/long",
    38  					},
    39  				},
    40  			},
    41  		},
    42  		{
    43  			Name: "regex match with one header",
    44  			Match: &envoy_config_route_v3.RouteMatch{
    45  				PathSpecifier: &envoy_config_route_v3.RouteMatch_SafeRegex{
    46  					SafeRegex: &envoy_type_matcher_v3.RegexMatcher{
    47  						Regex: "/regex",
    48  					},
    49  				},
    50  				Headers: []*envoy_config_route_v3.HeaderMatcher{
    51  					{
    52  						Name: "header1",
    53  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
    54  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
    55  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
    56  									Exact: "value1",
    57  								},
    58  							},
    59  						},
    60  					},
    61  				},
    62  			},
    63  		},
    64  		{
    65  			Name: "regex match with one header and one query",
    66  			Match: &envoy_config_route_v3.RouteMatch{
    67  				PathSpecifier: &envoy_config_route_v3.RouteMatch_SafeRegex{
    68  					SafeRegex: &envoy_type_matcher_v3.RegexMatcher{
    69  						Regex: "/regex",
    70  					},
    71  				},
    72  				Headers: []*envoy_config_route_v3.HeaderMatcher{
    73  					{
    74  						Name: "header1",
    75  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
    76  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
    77  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
    78  									Exact: "value1",
    79  								},
    80  							},
    81  						},
    82  					},
    83  				},
    84  				QueryParameters: []*envoy_config_route_v3.QueryParameterMatcher{
    85  					{
    86  						Name: "query1",
    87  						QueryParameterMatchSpecifier: &envoy_config_route_v3.QueryParameterMatcher_PresentMatch{
    88  							PresentMatch: true,
    89  						},
    90  					},
    91  				},
    92  			},
    93  		},
    94  		{
    95  			Name: "regex match with two headers",
    96  			Match: &envoy_config_route_v3.RouteMatch{
    97  				PathSpecifier: &envoy_config_route_v3.RouteMatch_SafeRegex{
    98  					SafeRegex: &envoy_type_matcher_v3.RegexMatcher{
    99  						Regex: "/regex",
   100  					},
   101  				},
   102  				Headers: []*envoy_config_route_v3.HeaderMatcher{
   103  					{
   104  						Name: "header1",
   105  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   106  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   107  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   108  									Exact: "value1",
   109  								},
   110  							},
   111  						},
   112  					},
   113  					{
   114  						Name: "header2",
   115  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   116  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   117  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   118  									Exact: "value2",
   119  								},
   120  							},
   121  						},
   122  					},
   123  				},
   124  			},
   125  		},
   126  		{
   127  			Name: "exact match short",
   128  			Match: &envoy_config_route_v3.RouteMatch{
   129  				PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{
   130  					Path: "/exact/match",
   131  				},
   132  			},
   133  		},
   134  		{
   135  			Name: "exact match long",
   136  			Match: &envoy_config_route_v3.RouteMatch{
   137  				PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{
   138  					Path: "/exact/match/longest",
   139  				},
   140  			},
   141  		},
   142  		{
   143  			Name: "exact match long with POST method",
   144  			Match: &envoy_config_route_v3.RouteMatch{
   145  				PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{
   146  					Path: "/exact/match/longest",
   147  				},
   148  				Headers: []*envoy_config_route_v3.HeaderMatcher{
   149  					{
   150  						Name: ":method",
   151  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   152  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   153  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   154  									Exact: "POST",
   155  								},
   156  							},
   157  						},
   158  					},
   159  				},
   160  			},
   161  		},
   162  		{
   163  			Name: "exact match long with GET method",
   164  			Match: &envoy_config_route_v3.RouteMatch{
   165  				PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{
   166  					Path: "/exact/match/longest",
   167  				},
   168  				Headers: []*envoy_config_route_v3.HeaderMatcher{
   169  					{
   170  						Name: ":method",
   171  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   172  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   173  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   174  									Exact: "GET",
   175  								},
   176  							},
   177  						},
   178  					},
   179  				},
   180  			},
   181  		},
   182  		{
   183  			Name: "exact match with one header",
   184  			Match: &envoy_config_route_v3.RouteMatch{
   185  				PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{
   186  					Path: "/exact/match/header",
   187  				},
   188  				Headers: []*envoy_config_route_v3.HeaderMatcher{
   189  					{
   190  						Name: "header1",
   191  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   192  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   193  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   194  									Exact: "value1",
   195  								},
   196  							},
   197  						},
   198  					},
   199  				},
   200  			},
   201  		},
   202  		{
   203  			Name: "exact match with one header and one query",
   204  			Match: &envoy_config_route_v3.RouteMatch{
   205  				PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{
   206  					Path: "/exact/match/header",
   207  				},
   208  				Headers: []*envoy_config_route_v3.HeaderMatcher{
   209  					{
   210  						Name: "header1",
   211  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   212  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   213  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   214  									Exact: "value1",
   215  								},
   216  							},
   217  						},
   218  					},
   219  				},
   220  				QueryParameters: []*envoy_config_route_v3.QueryParameterMatcher{
   221  					{
   222  						Name: "query1",
   223  						QueryParameterMatchSpecifier: &envoy_config_route_v3.QueryParameterMatcher_PresentMatch{
   224  							PresentMatch: true,
   225  						},
   226  					},
   227  				},
   228  			},
   229  		},
   230  		{
   231  			Name: "exact match with two headers",
   232  			Match: &envoy_config_route_v3.RouteMatch{
   233  				PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{
   234  					Path: "/exact/match/header",
   235  				},
   236  				Headers: []*envoy_config_route_v3.HeaderMatcher{
   237  					{
   238  						Name: "header1",
   239  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   240  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   241  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   242  									Exact: "value1",
   243  								},
   244  							},
   245  						},
   246  					},
   247  					{
   248  						Name: "header2",
   249  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   250  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   251  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   252  									Exact: "value2",
   253  								},
   254  							},
   255  						},
   256  					},
   257  				},
   258  			},
   259  		},
   260  		{
   261  			Name: "prefix match short",
   262  			Match: &envoy_config_route_v3.RouteMatch{
   263  				PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{
   264  					PathSeparatedPrefix: "/prefix/match",
   265  				},
   266  			},
   267  		},
   268  		{
   269  			Name: "prefix match short with HEAD method",
   270  			Match: &envoy_config_route_v3.RouteMatch{
   271  				PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{
   272  					PathSeparatedPrefix: "/prefix/match",
   273  				},
   274  				Headers: []*envoy_config_route_v3.HeaderMatcher{
   275  					{
   276  						Name: ":method",
   277  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   278  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   279  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   280  									Exact: "HEAD",
   281  								},
   282  							},
   283  						},
   284  					},
   285  				},
   286  			},
   287  		},
   288  		{
   289  			Name: "prefix match short with GET method",
   290  			Match: &envoy_config_route_v3.RouteMatch{
   291  				PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{
   292  					PathSeparatedPrefix: "/prefix/match",
   293  				},
   294  				Headers: []*envoy_config_route_v3.HeaderMatcher{
   295  					{
   296  						Name: ":method",
   297  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   298  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   299  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   300  									Exact: "GET",
   301  								},
   302  							},
   303  						},
   304  					},
   305  				},
   306  			},
   307  		},
   308  		{
   309  			Name: "prefix match long",
   310  			Match: &envoy_config_route_v3.RouteMatch{
   311  				PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{
   312  					PathSeparatedPrefix: "/prefix/match/long",
   313  				},
   314  			},
   315  		},
   316  		{
   317  			Name: "prefix match with one header",
   318  			Match: &envoy_config_route_v3.RouteMatch{
   319  				PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{
   320  					PathSeparatedPrefix: "/header",
   321  				},
   322  				Headers: []*envoy_config_route_v3.HeaderMatcher{
   323  					{
   324  						Name: "header1",
   325  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   326  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   327  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   328  									Exact: "value1",
   329  								},
   330  							},
   331  						},
   332  					},
   333  				},
   334  			},
   335  		},
   336  		{
   337  			Name: "prefix match with one header and one query",
   338  			Match: &envoy_config_route_v3.RouteMatch{
   339  				PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{
   340  					PathSeparatedPrefix: "/header",
   341  				},
   342  				Headers: []*envoy_config_route_v3.HeaderMatcher{
   343  					{
   344  						Name: "header1",
   345  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   346  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   347  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   348  									Exact: "value1",
   349  								},
   350  							},
   351  						},
   352  					},
   353  				},
   354  				QueryParameters: []*envoy_config_route_v3.QueryParameterMatcher{
   355  					{
   356  						Name: "query1",
   357  						QueryParameterMatchSpecifier: &envoy_config_route_v3.QueryParameterMatcher_PresentMatch{
   358  							PresentMatch: true,
   359  						},
   360  					},
   361  				},
   362  			},
   363  		},
   364  		{
   365  			Name: "prefix match with two headers",
   366  			Match: &envoy_config_route_v3.RouteMatch{
   367  				PathSpecifier: &envoy_config_route_v3.RouteMatch_PathSeparatedPrefix{
   368  					PathSeparatedPrefix: "/header",
   369  				},
   370  				Headers: []*envoy_config_route_v3.HeaderMatcher{
   371  					{
   372  						Name: "header1",
   373  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   374  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   375  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   376  									Exact: "value1",
   377  								},
   378  							},
   379  						},
   380  					},
   381  					{
   382  						Name: "header2",
   383  						HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{
   384  							StringMatch: &envoy_type_matcher_v3.StringMatcher{
   385  								MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
   386  									Exact: "value2",
   387  								},
   388  							},
   389  						},
   390  					},
   391  				},
   392  			},
   393  		},
   394  	}
   395  
   396  	// This assertion is to it easier to tell how
   397  	// the array is rearranged by the sorting.
   398  	// It also effectively ensures that buildNameSlice is
   399  	// working correctly.
   400  	namesBeforeSort := buildNameSlice(arr)
   401  	assert.Equal(t, []string{
   402  		"regex match short",
   403  		"regex match long",
   404  		"regex match with one header",
   405  		"regex match with one header and one query",
   406  		"regex match with two headers",
   407  		"exact match short",
   408  		"exact match long",
   409  		"exact match long with POST method",
   410  		"exact match long with GET method",
   411  		"exact match with one header",
   412  		"exact match with one header and one query",
   413  		"exact match with two headers",
   414  		"prefix match short",
   415  		"prefix match short with HEAD method",
   416  		"prefix match short with GET method",
   417  		"prefix match long",
   418  		"prefix match with one header",
   419  		"prefix match with one header and one query",
   420  		"prefix match with two headers",
   421  	}, namesBeforeSort)
   422  
   423  	sort.Sort(arr)
   424  
   425  	namesAfterSort := buildNameSlice(arr)
   426  	assert.Equal(t, []string{
   427  		"exact match long with GET method",
   428  		"exact match long with POST method",
   429  		"exact match long",
   430  		"exact match with two headers",
   431  		"exact match with one header and one query",
   432  		"exact match with one header",
   433  		"exact match short",
   434  		"regex match long",
   435  		"regex match with two headers",
   436  		"regex match with one header and one query",
   437  		"regex match with one header",
   438  		"regex match short",
   439  		"prefix match long",
   440  		"prefix match short with GET method",
   441  		"prefix match short with HEAD method",
   442  		"prefix match short",
   443  		"prefix match with two headers",
   444  		"prefix match with one header and one query",
   445  		"prefix match with one header",
   446  	}, namesAfterSort)
   447  
   448  }
   449  
   450  func buildNameSlice(arr []*envoy_config_route_v3.Route) []string {
   451  
   452  	var names []string
   453  
   454  	for _, entry := range arr {
   455  		names = append(names, entry.Name)
   456  	}
   457  
   458  	return names
   459  }
   460  
   461  func Test_hostRewriteMutation(t *testing.T) {
   462  	t.Run("no host rewrite", func(t *testing.T) {
   463  		route := &envoy_config_route_v3.Route_Route{
   464  			Route: &envoy_config_route_v3.RouteAction{},
   465  		}
   466  		res := hostRewriteMutation(nil)(route)
   467  		require.Equal(t, route, res)
   468  	})
   469  
   470  	t.Run("with host rewrite", func(t *testing.T) {
   471  		route := &envoy_config_route_v3.Route_Route{
   472  			Route: &envoy_config_route_v3.RouteAction{},
   473  		}
   474  		rewrite := &model.HTTPURLRewriteFilter{
   475  			HostName: model.AddressOf("example.com"),
   476  		}
   477  
   478  		res := hostRewriteMutation(rewrite)(route)
   479  		require.Equal(t, res.Route.HostRewriteSpecifier, &envoy_config_route_v3.RouteAction_HostRewriteLiteral{
   480  			HostRewriteLiteral: "example.com",
   481  		})
   482  	})
   483  }
   484  
   485  func Test_pathPrefixMutation(t *testing.T) {
   486  	t.Run("no prefix rewrite", func(t *testing.T) {
   487  		route := &envoy_config_route_v3.Route_Route{
   488  			Route: &envoy_config_route_v3.RouteAction{},
   489  		}
   490  		res := pathPrefixMutation(nil, nil)(route)
   491  		require.Equal(t, route, res)
   492  	})
   493  
   494  	t.Run("with prefix rewrite", func(t *testing.T) {
   495  		httpRoute := model.HTTPRoute{}
   496  		httpRoute.PathMatch.Prefix = "/strip-prefix"
   497  		route := &envoy_config_route_v3.Route_Route{
   498  			Route: &envoy_config_route_v3.RouteAction{},
   499  		}
   500  		rewrite := &model.HTTPURLRewriteFilter{
   501  			Path: &model.StringMatch{
   502  				Prefix: "/prefix",
   503  			},
   504  		}
   505  
   506  		res := pathPrefixMutation(rewrite, &httpRoute)(route)
   507  		require.Equal(t, res.Route.PrefixRewrite, "/prefix")
   508  	})
   509  	t.Run("with empty prefix rewrite", func(t *testing.T) {
   510  		httpRoute := model.HTTPRoute{}
   511  		httpRoute.PathMatch.Prefix = "/strip-prefix"
   512  		route := &envoy_config_route_v3.Route_Route{
   513  			Route: &envoy_config_route_v3.RouteAction{},
   514  		}
   515  		rewrite := &model.HTTPURLRewriteFilter{
   516  			Path: &model.StringMatch{
   517  				Prefix: "",
   518  			},
   519  		}
   520  
   521  		res := pathPrefixMutation(rewrite, &httpRoute)(route)
   522  		require.EqualValues(t, &envoy_type_matcher_v3.RegexMatchAndSubstitute{
   523  			Pattern: &envoy_type_matcher_v3.RegexMatcher{
   524  				Regex: fmt.Sprintf(`^%s(/?)(.*)`, regexp.QuoteMeta(httpRoute.PathMatch.Prefix)),
   525  			},
   526  			Substitution: `/\2`,
   527  		}, res.Route.RegexRewrite)
   528  	})
   529  	t.Run("with slash prefix rewrite", func(t *testing.T) {
   530  		httpRoute := model.HTTPRoute{}
   531  		httpRoute.PathMatch.Prefix = "/strip-prefix"
   532  		route := &envoy_config_route_v3.Route_Route{
   533  			Route: &envoy_config_route_v3.RouteAction{},
   534  		}
   535  		rewrite := &model.HTTPURLRewriteFilter{
   536  			Path: &model.StringMatch{
   537  				Prefix: "/",
   538  			},
   539  		}
   540  
   541  		res := pathPrefixMutation(rewrite, &httpRoute)(route)
   542  		require.EqualValues(t, &envoy_type_matcher_v3.RegexMatchAndSubstitute{
   543  			Pattern: &envoy_type_matcher_v3.RegexMatcher{
   544  				Regex: fmt.Sprintf(`^%s(/?)(.*)`, regexp.QuoteMeta(httpRoute.PathMatch.Prefix)),
   545  			},
   546  			Substitution: `/\2`,
   547  		}, res.Route.RegexRewrite)
   548  	})
   549  }
   550  
   551  func Test_requestMirrorMutation(t *testing.T) {
   552  	t.Run("no mirror", func(t *testing.T) {
   553  		route := &envoy_config_route_v3.Route_Route{
   554  			Route: &envoy_config_route_v3.RouteAction{},
   555  		}
   556  		res := requestMirrorMutation(nil)(route)
   557  		require.Equal(t, route, res)
   558  	})
   559  
   560  	t.Run("with mirror", func(t *testing.T) {
   561  		route := &envoy_config_route_v3.Route_Route{
   562  			Route: &envoy_config_route_v3.RouteAction{},
   563  		}
   564  		mirror := []*model.HTTPRequestMirror{
   565  			{
   566  				Backend: &model.Backend{
   567  					Name:      "dummy-service",
   568  					Namespace: "default",
   569  					Port: &model.BackendPort{
   570  						Port: 8080,
   571  						Name: "http",
   572  					},
   573  				},
   574  			},
   575  			{
   576  				Backend: &model.Backend{
   577  					Name:      "another-dummy-service",
   578  					Namespace: "default",
   579  					Port: &model.BackendPort{
   580  						Port: 8080,
   581  						Name: "http",
   582  					},
   583  				},
   584  			},
   585  		}
   586  
   587  		res := requestMirrorMutation(mirror)(route)
   588  		require.Len(t, res.Route.RequestMirrorPolicies, 2)
   589  		require.Equal(t, res.Route.RequestMirrorPolicies[0].Cluster, "default:dummy-service:8080")
   590  		require.Equal(t, res.Route.RequestMirrorPolicies[0].RuntimeFraction.DefaultValue.Numerator, uint32(100))
   591  		require.Equal(t, res.Route.RequestMirrorPolicies[1].Cluster, "default:another-dummy-service:8080")
   592  		require.Equal(t, res.Route.RequestMirrorPolicies[1].RuntimeFraction.DefaultValue.Numerator, uint32(100))
   593  	})
   594  }