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
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
59 for _, deploy := range []string{"t1", "t2", "t3", "gateway"} {
60 if err := TestHelper.CheckPods(ctx, testNamespace, deploy, 1); err != nil {
61
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
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
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
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