...

Source file src/github.com/linkerd/linkerd2/controller/api/destination/profile_translator_test.go

Documentation: github.com/linkerd/linkerd2/controller/api/destination

     1  package destination
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/golang/protobuf/ptypes/duration"
     7  	pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
     8  	httpPb "github.com/linkerd/linkerd2-proxy-api/go/http_types"
     9  	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
    10  	logging "github.com/sirupsen/logrus"
    11  	"google.golang.org/protobuf/proto"
    12  )
    13  
    14  var (
    15  	getButNotPrivate = &sp.RequestMatch{
    16  		All: []*sp.RequestMatch{
    17  			{
    18  				Method: "GET",
    19  			},
    20  			{
    21  				Not: &sp.RequestMatch{
    22  					PathRegex: "/private/.*",
    23  				},
    24  			},
    25  		},
    26  	}
    27  
    28  	pbGetButNotPrivate = &pb.RequestMatch{
    29  		Match: &pb.RequestMatch_All{
    30  			All: &pb.RequestMatch_Seq{
    31  				Matches: []*pb.RequestMatch{
    32  					{
    33  						Match: &pb.RequestMatch_Method{
    34  							Method: &httpPb.HttpMethod{
    35  								Type: &httpPb.HttpMethod_Registered_{
    36  									Registered: httpPb.HttpMethod_GET,
    37  								},
    38  							},
    39  						},
    40  					},
    41  					{
    42  						Match: &pb.RequestMatch_Not{
    43  							Not: &pb.RequestMatch{
    44  								Match: &pb.RequestMatch_Path{
    45  									Path: &pb.PathMatch{
    46  										Regex: "/private/.*",
    47  									},
    48  								},
    49  							},
    50  						},
    51  					},
    52  				},
    53  			},
    54  		},
    55  	}
    56  
    57  	login = &sp.RequestMatch{
    58  		PathRegex: "/login",
    59  	}
    60  
    61  	pbLogin = &pb.RequestMatch{
    62  		Match: &pb.RequestMatch_Path{
    63  			Path: &pb.PathMatch{
    64  				Regex: "/login",
    65  			},
    66  		},
    67  	}
    68  
    69  	fiveXX = &sp.ResponseMatch{
    70  		Status: &sp.Range{
    71  			Min: 500,
    72  			Max: 599,
    73  		},
    74  	}
    75  
    76  	pbFiveXX = &pb.ResponseMatch{
    77  		Match: &pb.ResponseMatch_Status{
    78  			Status: &pb.HttpStatusRange{
    79  				Min: 500,
    80  				Max: 599,
    81  			},
    82  		},
    83  	}
    84  
    85  	fiveXXfourTwenty = &sp.ResponseMatch{
    86  		Any: []*sp.ResponseMatch{
    87  			fiveXX,
    88  			{
    89  				Status: &sp.Range{
    90  					Min: 420,
    91  					Max: 420,
    92  				},
    93  			},
    94  		},
    95  	}
    96  
    97  	pbFiveXXfourTwenty = &pb.ResponseMatch{
    98  		Match: &pb.ResponseMatch_Any{
    99  			Any: &pb.ResponseMatch_Seq{
   100  				Matches: []*pb.ResponseMatch{
   101  					pbFiveXX,
   102  					{
   103  						Match: &pb.ResponseMatch_Status{
   104  							Status: &pb.HttpStatusRange{
   105  								Min: 420,
   106  								Max: 420,
   107  							},
   108  						},
   109  					},
   110  				},
   111  			},
   112  		},
   113  	}
   114  
   115  	route1 = &sp.RouteSpec{
   116  		Name:      "route1",
   117  		Condition: getButNotPrivate,
   118  		ResponseClasses: []*sp.ResponseClass{
   119  			{
   120  				Condition: fiveXX,
   121  				IsFailure: true,
   122  			},
   123  		},
   124  	}
   125  
   126  	pbRoute1 = &pb.Route{
   127  		MetricsLabels: map[string]string{
   128  			"route": "route1",
   129  		},
   130  		Condition: pbGetButNotPrivate,
   131  		ResponseClasses: []*pb.ResponseClass{
   132  			{
   133  				Condition: pbFiveXX,
   134  				IsFailure: true,
   135  			},
   136  		},
   137  		Timeout: nil,
   138  	}
   139  
   140  	route2 = &sp.RouteSpec{
   141  		Name:      "route2",
   142  		Condition: login,
   143  		ResponseClasses: []*sp.ResponseClass{
   144  			{
   145  				Condition: fiveXXfourTwenty,
   146  				IsFailure: true,
   147  			},
   148  		},
   149  	}
   150  
   151  	pbRoute2 = &pb.Route{
   152  		MetricsLabels: map[string]string{
   153  			"route": "route2",
   154  		},
   155  		Condition: pbLogin,
   156  		ResponseClasses: []*pb.ResponseClass{
   157  			{
   158  				Condition: pbFiveXXfourTwenty,
   159  				IsFailure: true,
   160  			},
   161  		},
   162  		Timeout: nil,
   163  	}
   164  
   165  	profile = &sp.ServiceProfile{
   166  		Spec: sp.ServiceProfileSpec{
   167  			Routes: []*sp.RouteSpec{
   168  				route1,
   169  				route2,
   170  			},
   171  		},
   172  	}
   173  
   174  	pbProfile = &pb.DestinationProfile{
   175  		Routes: []*pb.Route{
   176  			pbRoute1,
   177  			pbRoute2,
   178  		},
   179  		RetryBudget: defaultRetryBudget(),
   180  	}
   181  
   182  	defaultPbProfile = &pb.DestinationProfile{
   183  		Routes:      []*pb.Route{},
   184  		RetryBudget: defaultRetryBudget(),
   185  	}
   186  
   187  	multipleRequestMatches = &sp.ServiceProfile{
   188  		Spec: sp.ServiceProfileSpec{
   189  			Routes: []*sp.RouteSpec{
   190  				{
   191  					Name: "multipleRequestMatches",
   192  					Condition: &sp.RequestMatch{
   193  						Method:    "GET",
   194  						PathRegex: "/my/path",
   195  					},
   196  				},
   197  			},
   198  		},
   199  	}
   200  
   201  	pbRequestMatchAll = &pb.DestinationProfile{
   202  		Routes: []*pb.Route{
   203  			{
   204  				Condition: &pb.RequestMatch{
   205  					Match: &pb.RequestMatch_All{
   206  						All: &pb.RequestMatch_Seq{
   207  							Matches: []*pb.RequestMatch{
   208  								{
   209  									Match: &pb.RequestMatch_Method{
   210  										Method: &httpPb.HttpMethod{
   211  											Type: &httpPb.HttpMethod_Registered_{
   212  												Registered: httpPb.HttpMethod_GET,
   213  											},
   214  										},
   215  									},
   216  								},
   217  								{
   218  									Match: &pb.RequestMatch_Path{
   219  										Path: &pb.PathMatch{
   220  											Regex: "/my/path",
   221  										},
   222  									},
   223  								},
   224  							},
   225  						},
   226  					},
   227  				},
   228  				MetricsLabels: map[string]string{
   229  					"route": "multipleRequestMatches",
   230  				},
   231  				ResponseClasses: []*pb.ResponseClass{},
   232  				Timeout:         nil,
   233  			},
   234  		},
   235  		RetryBudget: defaultRetryBudget(),
   236  	}
   237  
   238  	notEnoughRequestMatches = &sp.ServiceProfile{
   239  		Spec: sp.ServiceProfileSpec{
   240  			Routes: []*sp.RouteSpec{
   241  				{
   242  					Condition: &sp.RequestMatch{},
   243  				},
   244  			},
   245  		},
   246  	}
   247  
   248  	multipleResponseMatches = &sp.ServiceProfile{
   249  		Spec: sp.ServiceProfileSpec{
   250  			Routes: []*sp.RouteSpec{
   251  				{
   252  					Name: "multipleResponseMatches",
   253  					Condition: &sp.RequestMatch{
   254  						Method: "GET",
   255  					},
   256  					ResponseClasses: []*sp.ResponseClass{
   257  						{
   258  							Condition: &sp.ResponseMatch{
   259  								Status: &sp.Range{
   260  									Min: 400,
   261  									Max: 499,
   262  								},
   263  								Not: &sp.ResponseMatch{
   264  									Status: &sp.Range{
   265  										Min: 404,
   266  									},
   267  								},
   268  							},
   269  						},
   270  					},
   271  				},
   272  			},
   273  		},
   274  	}
   275  
   276  	pbResponseMatchAll = &pb.DestinationProfile{
   277  		Routes: []*pb.Route{
   278  			{
   279  				Condition: &pb.RequestMatch{
   280  					Match: &pb.RequestMatch_Method{
   281  						Method: &httpPb.HttpMethod{
   282  							Type: &httpPb.HttpMethod_Registered_{
   283  								Registered: httpPb.HttpMethod_GET,
   284  							},
   285  						},
   286  					},
   287  				},
   288  				MetricsLabels: map[string]string{
   289  					"route": "multipleResponseMatches",
   290  				},
   291  				ResponseClasses: []*pb.ResponseClass{
   292  					{
   293  						Condition: &pb.ResponseMatch{
   294  							Match: &pb.ResponseMatch_All{
   295  								All: &pb.ResponseMatch_Seq{
   296  									Matches: []*pb.ResponseMatch{
   297  										{
   298  											Match: &pb.ResponseMatch_Status{
   299  												Status: &pb.HttpStatusRange{
   300  													Min: 400,
   301  													Max: 499,
   302  												},
   303  											},
   304  										},
   305  										{
   306  											Match: &pb.ResponseMatch_Not{
   307  												Not: &pb.ResponseMatch{
   308  													Match: &pb.ResponseMatch_Status{
   309  														Status: &pb.HttpStatusRange{
   310  															Min: 404,
   311  														},
   312  													},
   313  												},
   314  											},
   315  										},
   316  									},
   317  								},
   318  							},
   319  						},
   320  					},
   321  				},
   322  				Timeout: nil,
   323  			},
   324  		},
   325  		RetryBudget: defaultRetryBudget(),
   326  	}
   327  
   328  	oneSidedStatusRange = &sp.ServiceProfile{
   329  		Spec: sp.ServiceProfileSpec{
   330  			Routes: []*sp.RouteSpec{
   331  				{
   332  					Condition: &sp.RequestMatch{
   333  						Method: "GET",
   334  					},
   335  					ResponseClasses: []*sp.ResponseClass{
   336  						{
   337  							Condition: &sp.ResponseMatch{
   338  								Status: &sp.Range{
   339  									Min: 200,
   340  								},
   341  							},
   342  						},
   343  					},
   344  				},
   345  			},
   346  		},
   347  	}
   348  
   349  	invalidStatusRange = &sp.ServiceProfile{
   350  		Spec: sp.ServiceProfileSpec{
   351  			Routes: []*sp.RouteSpec{
   352  				{
   353  					Condition: &sp.RequestMatch{
   354  						Method: "GET",
   355  					},
   356  					ResponseClasses: []*sp.ResponseClass{
   357  						{
   358  							Condition: &sp.ResponseMatch{
   359  								Status: &sp.Range{
   360  									Min: 201,
   361  									Max: 200,
   362  								},
   363  							},
   364  						},
   365  					},
   366  				},
   367  			},
   368  		},
   369  	}
   370  
   371  	notEnoughResponseMatches = &sp.ServiceProfile{
   372  		Spec: sp.ServiceProfileSpec{
   373  			Routes: []*sp.RouteSpec{
   374  				{
   375  					Condition: &sp.RequestMatch{
   376  						Method: "GET",
   377  					},
   378  					ResponseClasses: []*sp.ResponseClass{
   379  						{
   380  							Condition: &sp.ResponseMatch{},
   381  						},
   382  					},
   383  				},
   384  			},
   385  		},
   386  	}
   387  
   388  	routeWithTimeout = &sp.RouteSpec{
   389  		Name:            "routeWithTimeout",
   390  		Condition:       login,
   391  		ResponseClasses: []*sp.ResponseClass{},
   392  		Timeout:         "200ms",
   393  	}
   394  
   395  	profileWithTimeout = &sp.ServiceProfile{
   396  		Spec: sp.ServiceProfileSpec{
   397  			Routes: []*sp.RouteSpec{
   398  				routeWithTimeout,
   399  			},
   400  		},
   401  	}
   402  
   403  	pbRouteWithTimeout = &pb.Route{
   404  		MetricsLabels: map[string]string{
   405  			"route": "routeWithTimeout",
   406  		},
   407  		Condition:       pbLogin,
   408  		ResponseClasses: []*pb.ResponseClass{},
   409  		Timeout: &duration.Duration{
   410  			Nanos: 200000000, // 200ms
   411  		},
   412  	}
   413  
   414  	pbProfileWithTimeout = &pb.DestinationProfile{
   415  		Routes: []*pb.Route{
   416  			pbRouteWithTimeout,
   417  		},
   418  		RetryBudget: defaultRetryBudget(),
   419  	}
   420  )
   421  
   422  func TestProfileTranslator(t *testing.T) {
   423  	t.Run("Sends update", func(t *testing.T) {
   424  		mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
   425  
   426  		translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
   427  		translator.Start()
   428  		defer translator.Stop()
   429  
   430  		translator.Update(profile)
   431  
   432  		actualPbProfile := <-mockGetProfileServer.profilesReceived
   433  		if !proto.Equal(actualPbProfile, pbProfile) {
   434  			t.Fatalf("Expected profile sent to be [%v] but was [%v]", pbProfile, actualPbProfile)
   435  		}
   436  		numProfiles := len(mockGetProfileServer.profilesReceived) + 1
   437  		if numProfiles != 1 {
   438  			t.Fatalf("Expecting [1] profile, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
   439  		}
   440  	})
   441  
   442  	t.Run("Request match with more than one field becomes ALL", func(t *testing.T) {
   443  		mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
   444  
   445  		translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
   446  		translator.Start()
   447  		defer translator.Stop()
   448  
   449  		translator.Update(multipleRequestMatches)
   450  
   451  		actualPbProfile := <-mockGetProfileServer.profilesReceived
   452  		if !proto.Equal(actualPbProfile, pbRequestMatchAll) {
   453  			t.Fatalf("Expected profile sent to be [%v] but was [%v]", pbRequestMatchAll, actualPbProfile)
   454  		}
   455  		numProfiles := len(mockGetProfileServer.profilesReceived) + 1
   456  		if numProfiles != 1 {
   457  			t.Fatalf("Expecting [1] profiles, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
   458  		}
   459  	})
   460  
   461  	t.Run("Ignores request match without any fields", func(t *testing.T) {
   462  		mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
   463  
   464  		translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
   465  		translator.Start()
   466  		defer translator.Stop()
   467  
   468  		translator.Update(notEnoughRequestMatches)
   469  
   470  		numProfiles := len(mockGetProfileServer.profilesReceived)
   471  		if numProfiles != 0 {
   472  			t.Fatalf("Expecting [0] profiles, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
   473  		}
   474  	})
   475  
   476  	t.Run("Response match with more than one field becomes ALL", func(t *testing.T) {
   477  		mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
   478  
   479  		translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
   480  		translator.Start()
   481  		defer translator.Stop()
   482  
   483  		translator.Update(multipleResponseMatches)
   484  
   485  		actualPbProfile := <-mockGetProfileServer.profilesReceived
   486  		if !proto.Equal(actualPbProfile, pbResponseMatchAll) {
   487  			t.Fatalf("Expected profile sent to be [%v] but was [%v]", pbResponseMatchAll, actualPbProfile)
   488  		}
   489  		numProfiles := len(mockGetProfileServer.profilesReceived) + 1
   490  		if numProfiles != 1 {
   491  			t.Fatalf("Expecting [1] profiles, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
   492  		}
   493  	})
   494  
   495  	t.Run("Ignores response match without any fields", func(t *testing.T) {
   496  		mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
   497  
   498  		translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
   499  		translator.Start()
   500  		defer translator.Stop()
   501  
   502  		translator.Update(notEnoughResponseMatches)
   503  
   504  		numProfiles := len(mockGetProfileServer.profilesReceived)
   505  		if numProfiles != 0 {
   506  			t.Fatalf("Expecting [0] profiles, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
   507  		}
   508  	})
   509  
   510  	t.Run("Ignores response match with invalid status range", func(t *testing.T) {
   511  		mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
   512  
   513  		translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
   514  		translator.Start()
   515  		defer translator.Stop()
   516  
   517  		translator.Update(invalidStatusRange)
   518  
   519  		numProfiles := len(mockGetProfileServer.profilesReceived)
   520  		if numProfiles != 0 {
   521  			t.Fatalf("Expecting [0] profiles, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
   522  		}
   523  	})
   524  
   525  	t.Run("Sends update for one sided status range", func(t *testing.T) {
   526  		mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
   527  
   528  		translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
   529  		translator.Start()
   530  		defer translator.Stop()
   531  
   532  		translator.Update(oneSidedStatusRange)
   533  
   534  		<-mockGetProfileServer.profilesReceived
   535  
   536  		numProfiles := len(mockGetProfileServer.profilesReceived) + 1
   537  		if numProfiles != 1 {
   538  			t.Fatalf("Expecting [1] profile, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
   539  		}
   540  	})
   541  
   542  	t.Run("Sends empty update", func(t *testing.T) {
   543  		mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
   544  
   545  		translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
   546  		translator.Start()
   547  		defer translator.Stop()
   548  
   549  		translator.Update(nil)
   550  
   551  		actualPbProfile := <-mockGetProfileServer.profilesReceived
   552  		if !proto.Equal(actualPbProfile, defaultPbProfile) {
   553  			t.Fatalf("Expected profile sent to be [%v] but was [%v]", defaultPbProfile, actualPbProfile)
   554  		}
   555  		numProfiles := len(mockGetProfileServer.profilesReceived) + 1
   556  		if numProfiles != 1 {
   557  			t.Fatalf("Expecting [1] profile, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
   558  		}
   559  	})
   560  
   561  	t.Run("Sends update with custom timeout", func(t *testing.T) {
   562  		mockGetProfileServer := &mockDestinationGetProfileServer{profilesReceived: make(chan *pb.DestinationProfile, 50)}
   563  
   564  		translator := newProfileTranslator(mockGetProfileServer, logging.WithField("test", t.Name()), "", 80, nil)
   565  		translator.Start()
   566  		defer translator.Stop()
   567  
   568  		translator.Update(profileWithTimeout)
   569  
   570  		actualPbProfile := <-mockGetProfileServer.profilesReceived
   571  		if !proto.Equal(actualPbProfile, pbProfileWithTimeout) {
   572  			t.Fatalf("Expected profile sent to be [%v] but was [%v]", pbProfileWithTimeout, actualPbProfile)
   573  		}
   574  		numProfiles := len(mockGetProfileServer.profilesReceived) + 1
   575  		if numProfiles != 1 {
   576  			t.Fatalf("Expecting [1] profile, got [%d]. Updates: %v", numProfiles, mockGetProfileServer.profilesReceived)
   577  		}
   578  	})
   579  }
   580  

View as plain text