...

Source file src/github.com/linkerd/linkerd2/test/integration/viz/serviceprofiles/serviceprofiles_test.go

Documentation: github.com/linkerd/linkerd2/test/integration/viz/serviceprofiles

     1  package serviceprofiles
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"strings"
     9  	"testing"
    10  	"time"
    11  
    12  	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
    13  	"github.com/linkerd/linkerd2/testutil"
    14  	cmd2 "github.com/linkerd/linkerd2/viz/cmd"
    15  	"sigs.k8s.io/yaml"
    16  )
    17  
    18  var TestHelper *testutil.TestHelper
    19  
    20  type testCase struct {
    21  	args           []string
    22  	deployName     string
    23  	expectedRoutes []string
    24  	namespace      string
    25  	sourceName     string
    26  	spName         string
    27  }
    28  
    29  func TestMain(m *testing.M) {
    30  	TestHelper = testutil.NewTestHelper()
    31  	// Block test execution until viz extension is running
    32  	TestHelper.WaitUntilDeployReady(testutil.LinkerdVizDeployReplicas)
    33  	os.Exit(m.Run())
    34  }
    35  
    36  func TestServiceProfiles(t *testing.T) {
    37  	ctx := context.Background()
    38  	TestHelper.WithDataPlaneNamespace(ctx, "serviceprofile-test", map[string]string{}, t, func(t *testing.T, ns string) {
    39  		t.Run("service profiles", testProfiles)
    40  		t.Run("service profiles metrics", testMetrics)
    41  	})
    42  }
    43  
    44  func testProfiles(t *testing.T) {
    45  	ctx := context.Background()
    46  	testNamespace := TestHelper.GetTestNamespace("serviceprofile-test")
    47  	out, err := TestHelper.LinkerdRun("inject", "--manual", "testdata/tap_application.yaml")
    48  	if err != nil {
    49  		testutil.AnnotatedFatal(t, "'linkerd inject' command failed", err)
    50  	}
    51  
    52  	out, err = TestHelper.KubectlApply(out, testNamespace)
    53  	if err != nil {
    54  		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
    55  			"'kubectl apply' command failed\n%s", out)
    56  	}
    57  
    58  	// wait for deployments to start
    59  	for _, deploy := range []string{"t1", "t2", "t3", "gateway"} {
    60  		if err := TestHelper.CheckPods(ctx, testNamespace, deploy, 1); err != nil {
    61  			//nolint:errorlint
    62  			if rce, ok := err.(*testutil.RestartCountError); ok {
    63  				testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
    64  			} else {
    65  				testutil.AnnotatedError(t, "CheckPods timed-out", err)
    66  			}
    67  		}
    68  	}
    69  
    70  	testCases := []testCase{
    71  		{
    72  			sourceName:     "tap",
    73  			namespace:      testNamespace,
    74  			deployName:     "deployment/t1",
    75  			spName:         "t1-svc",
    76  			expectedRoutes: []string{"POST /buoyantio.bb.TheService/theFunction", "[DEFAULT]"},
    77  		},
    78  		{
    79  			sourceName:     "open-api",
    80  			namespace:      testNamespace,
    81  			spName:         "t3-svc",
    82  			deployName:     "deployment/t3",
    83  			expectedRoutes: []string{"DELETE /testpath", "GET /testpath", "PATCH /testpath", "POST /testpath", "[DEFAULT]"},
    84  		},
    85  	}
    86  
    87  	for _, tc := range testCases {
    88  		tc := tc // pin
    89  		t.Run(tc.sourceName, func(t *testing.T) {
    90  			routes, err := getRoutes(tc.deployName, tc.namespace, []string{})
    91  			if err != nil {
    92  				testutil.AnnotatedFatalf(t, "'linkerd routes' command failed",
    93  					"'linkerd routes' command failed: %s\n", err)
    94  			}
    95  
    96  			initialExpectedRoutes := []string{"[DEFAULT]"}
    97  
    98  			assertExpectedRoutes(initialExpectedRoutes, routes, t)
    99  
   100  			sourceFlag := fmt.Sprintf("--%s", tc.sourceName)
   101  			cmd := []string{"profile", "--namespace", tc.namespace, tc.spName, sourceFlag}
   102  			if tc.sourceName == "tap" {
   103  				cmd = append([]string{"viz"}, cmd...)
   104  				tc.args = []string{
   105  					tc.deployName,
   106  					"--tap-route-limit",
   107  					"1",
   108  					"--tap-duration",
   109  					"25s",
   110  				}
   111  			}
   112  
   113  			if tc.sourceName == "open-api" {
   114  				tc.args = []string{
   115  					"testdata/t3.swagger",
   116  				}
   117  			}
   118  
   119  			cmd = append(cmd, tc.args...)
   120  			out, err := TestHelper.LinkerdRun(cmd...)
   121  			if err != nil {
   122  				testutil.AnnotatedFatal(t, fmt.Sprintf("'linkerd %s' command failed", cmd), err)
   123  			}
   124  
   125  			_, err = TestHelper.KubectlApply(out, tc.namespace)
   126  			if err != nil {
   127  				testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
   128  					"'kubectl apply' command failed:\n%s", err)
   129  			}
   130  
   131  			routes, err = getRoutes(tc.deployName, tc.namespace, []string{})
   132  			if err != nil {
   133  				testutil.AnnotatedFatalf(t, "'linkerd routes' command failed",
   134  					"'linkerd routes' command failed: %s\n", err)
   135  			}
   136  
   137  			assertExpectedRoutes(tc.expectedRoutes, routes, t)
   138  		})
   139  	}
   140  }
   141  
   142  func testMetrics(t *testing.T) {
   143  	var (
   144  		testNamespace        = TestHelper.GetTestNamespace("serviceprofile-test")
   145  		testSP               = "world-svc"
   146  		testDownstreamDeploy = "deployment/world"
   147  		testUpstreamDeploy   = "deployment/hello"
   148  		testYAML             = "testdata/hello_world.yaml"
   149  	)
   150  
   151  	out, err := TestHelper.LinkerdRun("inject", "--manual", testYAML)
   152  	if err != nil {
   153  		testutil.AnnotatedError(t, "'linkerd inject' command failed", err)
   154  	}
   155  
   156  	out, err = TestHelper.KubectlApply(out, testNamespace)
   157  	if err != nil {
   158  		testutil.AnnotatedErrorf(t, "'kubectl apply' command failed",
   159  			"'kubectl apply' command failed\n%s", out)
   160  	}
   161  
   162  	cmd := []string{
   163  		"profile",
   164  		"--namespace",
   165  		testNamespace,
   166  		"--open-api",
   167  		"testdata/world.swagger",
   168  		testSP,
   169  	}
   170  
   171  	out, err = TestHelper.LinkerdRun(cmd...)
   172  	if err != nil {
   173  		testutil.AnnotatedError(t, fmt.Sprintf("'linkerd %s' command failed", cmd), err)
   174  	}
   175  
   176  	_, err = TestHelper.KubectlApply(out, testNamespace)
   177  	if err != nil {
   178  		testutil.AnnotatedErrorf(t, "'kubectl apply' command failed",
   179  			"'kubectl apply' command failed\n%s", err)
   180  	}
   181  
   182  	assertRouteStat(testUpstreamDeploy, testNamespace, testDownstreamDeploy, t, func(stat *cmd2.JSONRouteStats) error {
   183  		if !(*stat.ActualSuccess > 0.00 && *stat.ActualSuccess < 100.00) {
   184  			return fmt.Errorf("expected Actual Success to be greater than 0%% and less than 100%% due to pre-seeded failure rate. But got %0.2f", *stat.ActualSuccess)
   185  		}
   186  		return nil
   187  	})
   188  
   189  	profile := &sp.ServiceProfile{}
   190  
   191  	// Grab the output and convert it to a service profile object for modification
   192  	err = yaml.Unmarshal([]byte(out), profile)
   193  	if err != nil {
   194  		testutil.AnnotatedErrorf(t, "unable to unmarshal YAML",
   195  			"unable to unmarshal YAML: %s", err)
   196  	}
   197  
   198  	// introduce retry in the service profile
   199  	for _, route := range profile.Spec.Routes {
   200  		if route.Name == "GET /testpath" {
   201  			route.IsRetryable = true
   202  			break
   203  		}
   204  	}
   205  
   206  	bytes, err := yaml.Marshal(profile)
   207  	if err != nil {
   208  		testutil.AnnotatedErrorf(t, "error marshalling service profile",
   209  			"error marshalling service profile: %s", bytes)
   210  	}
   211  
   212  	out, err = TestHelper.KubectlApply(string(bytes), testNamespace)
   213  	if err != nil {
   214  		testutil.AnnotatedErrorf(t, "'kubectl apply' command failed",
   215  			"'kubectl apply' command failed:\n%s :%s", err, out)
   216  	}
   217  
   218  	assertRouteStat(testUpstreamDeploy, testNamespace, testDownstreamDeploy, t, func(stat *cmd2.JSONRouteStats) error {
   219  		if *stat.EffectiveSuccess < 0.95 {
   220  			return fmt.Errorf("expected Effective Success to be at least 95%% with retries enabled. But got %.2f", *stat.EffectiveSuccess)
   221  		}
   222  		return nil
   223  	})
   224  }
   225  
   226  func assertRouteStat(upstream, namespace, downstream string, t *testing.T, assertFn func(stat *cmd2.JSONRouteStats) error) {
   227  	const routePath = "GET /testpath"
   228  	timeout := 2 * time.Minute
   229  	err := testutil.RetryFor(timeout, func() error {
   230  		routes, err := getRoutes(upstream, namespace, []string{"--to", downstream})
   231  		if err != nil {
   232  			return fmt.Errorf("'linkerd routes' command failed: %w", err)
   233  		}
   234  
   235  		var testRoute *cmd2.JSONRouteStats
   236  		assertExpectedRoutes([]string{routePath, "[DEFAULT]"}, routes, t)
   237  
   238  		for _, route := range routes {
   239  			if route.Route == routePath {
   240  				testRoute = route
   241  			}
   242  		}
   243  
   244  		if testRoute == nil {
   245  			return errors.New("expected test route not to be nil")
   246  		}
   247  
   248  		return assertFn(testRoute)
   249  	})
   250  
   251  	if err != nil {
   252  		testutil.AnnotatedFatal(t, fmt.Sprintf("timed-out asserting route stat (%s)", timeout), err)
   253  	}
   254  }
   255  
   256  func assertExpectedRoutes(expected []string, actual []*cmd2.JSONRouteStats, t *testing.T) {
   257  
   258  	if len(expected) != len(actual) {
   259  		testutil.Errorf(t, "mismatch routes count. Expected %d, Actual %d", len(expected), len(actual))
   260  	}
   261  
   262  	for _, expectedRoute := range expected {
   263  		containsRoute := false
   264  		for _, actualRoute := range actual {
   265  			if actualRoute.Route == expectedRoute {
   266  				containsRoute = true
   267  				break
   268  			}
   269  		}
   270  		if !containsRoute {
   271  			sb := strings.Builder{}
   272  			for _, route := range actual {
   273  				sb.WriteString(fmt.Sprintf("%s ", route.Route))
   274  			}
   275  			testutil.Errorf(t, "expected route %s not found in %+v", expectedRoute, sb.String())
   276  		}
   277  	}
   278  }
   279  
   280  func getRoutes(deployName, namespace string, additionalArgs []string) ([]*cmd2.JSONRouteStats, error) {
   281  	cmd := []string{"viz", "routes", "--namespace", namespace, deployName}
   282  
   283  	if len(additionalArgs) > 0 {
   284  		cmd = append(cmd, additionalArgs...)
   285  	}
   286  
   287  	cmd = append(cmd, "--output", "json")
   288  	var results map[string][]*cmd2.JSONRouteStats
   289  	err := testutil.RetryFor(2*time.Minute, func() error {
   290  		out, err := TestHelper.LinkerdRun(cmd...)
   291  		if err != nil {
   292  			return err
   293  		}
   294  
   295  		if err := yaml.Unmarshal([]byte(out), &results); err != nil {
   296  			return err
   297  		}
   298  
   299  		if _, ok := results[deployName]; ok {
   300  			return nil
   301  		}
   302  
   303  		keys := []string{}
   304  		for k := range results {
   305  			keys = append(keys, k)
   306  		}
   307  		return fmt.Errorf("could not retrieve route info for %s; found [%s]", deployName, strings.Join(keys, ", "))
   308  	})
   309  	if err != nil {
   310  		return nil, err
   311  	}
   312  	return results[deployName], nil
   313  
   314  }
   315  

View as plain text