1 package cmd
2
3 import (
4 "bytes"
5 "context"
6 "fmt"
7 "os"
8 "path/filepath"
9 "testing"
10
11 "github.com/linkerd/linkerd2/cli/flag"
12 charts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
13 "github.com/linkerd/linkerd2/pkg/tls"
14 "helm.sh/helm/v3/pkg/cli/values"
15 corev1 "k8s.io/api/core/v1"
16 )
17
18 const (
19 installProxyVersion = "install-proxy-version"
20 installControlPlaneVersion = "install-control-plane-version"
21 installDebugVersion = "install-debug-version"
22 )
23
24 func TestRender(t *testing.T) {
25 defaultValues, err := testInstallOptionsFakeCerts()
26 if err != nil {
27 t.Fatal(err)
28 }
29
30 gidValues, err := testInstallOptionsFakeCerts()
31 if err != nil {
32 t.Fatal(err)
33 }
34 gidValues.ControllerGID = 1234
35 gidValues.Proxy.GID = 4321
36
37
38
39 var controllerGID int64 = 2103
40 var proxyGID int64 = 2102
41 metaValues := &charts.Values{
42 ControllerImage: "ControllerImage",
43 LinkerdVersion: "LinkerdVersion",
44 ControllerUID: 2103,
45 ControllerGID: controllerGID,
46 EnableH2Upgrade: true,
47 WebhookFailurePolicy: "WebhookFailurePolicy",
48 HeartbeatSchedule: "1 2 3 4 5",
49 Identity: defaultValues.Identity,
50 NodeSelector: defaultValues.NodeSelector,
51 Tolerations: defaultValues.Tolerations,
52 ClusterDomain: "cluster.local",
53 ClusterNetworks: "ClusterNetworks",
54 ImagePullPolicy: "ImagePullPolicy",
55 CliVersion: "CliVersion",
56 ControllerLogLevel: "ControllerLogLevel",
57 ControllerLogFormat: "ControllerLogFormat",
58 ProxyContainerName: "ProxyContainerName",
59 RevisionHistoryLimit: 10,
60 CNIEnabled: false,
61 IdentityTrustDomain: defaultValues.IdentityTrustDomain,
62 IdentityTrustAnchorsPEM: defaultValues.IdentityTrustAnchorsPEM,
63 PodAnnotations: map[string]string{},
64 PodLabels: map[string]string{},
65 PriorityClassName: "PriorityClassName",
66 PolicyController: &charts.PolicyController{
67 Image: &charts.Image{
68 Name: "PolicyControllerImageName",
69 PullPolicy: "ImagePullPolicy",
70 Version: "PolicyControllerVersion",
71 },
72 LogLevel: "log-level",
73 Resources: &charts.Resources{
74 CPU: charts.Constraints{
75 Limit: "cpu-limit",
76 Request: "cpu-request",
77 },
78 Memory: charts.Constraints{
79 Limit: "memory-limit",
80 Request: "memory-request",
81 },
82 },
83 ProbeNetworks: []string{"1.0.0.0/0", "2.0.0.0/0"},
84 },
85 Proxy: &charts.Proxy{
86 Image: &charts.Image{
87 Name: "ProxyImageName",
88 PullPolicy: "ImagePullPolicy",
89 Version: "ProxyVersion",
90 },
91 LogLevel: "warn,linkerd=info",
92 LogFormat: "plain",
93 Resources: &charts.Resources{
94 CPU: charts.Constraints{
95 Limit: "cpu-limit",
96 Request: "cpu-request",
97 },
98 Memory: charts.Constraints{
99 Limit: "memory-limit",
100 Request: "memory-request",
101 },
102 },
103 Ports: &charts.Ports{
104 Admin: 4191,
105 Control: 4190,
106 Inbound: 4143,
107 Outbound: 4140,
108 },
109 UID: 2102,
110 GID: proxyGID,
111 OpaquePorts: "25,443,587,3306,5432,11211",
112 Await: true,
113 DefaultInboundPolicy: "default-allow-policy",
114 LivenessProbe: &charts.Probe{
115 InitialDelaySeconds: 10,
116 TimeoutSeconds: 1,
117 },
118 ReadinessProbe: &charts.Probe{
119 InitialDelaySeconds: 2,
120 TimeoutSeconds: 1,
121 },
122 },
123 ProxyInit: &charts.ProxyInit{
124 IptablesMode: "legacy",
125 Image: &charts.Image{
126 Name: "ProxyInitImageName",
127 PullPolicy: "ImagePullPolicy",
128 Version: "ProxyInitVersion",
129 },
130 IgnoreOutboundPorts: "443",
131 Resources: &charts.Resources{
132 CPU: charts.Constraints{
133 Limit: "100m",
134 Request: "10m",
135 },
136 Memory: charts.Constraints{
137 Limit: "50Mi",
138 Request: "10Mi",
139 },
140 },
141 XTMountPath: &charts.VolumeMountPath{
142 MountPath: "/run",
143 Name: "linkerd-proxy-init-xtables-lock",
144 },
145 RunAsRoot: false,
146 RunAsUser: 65534,
147 RunAsGroup: 65534,
148 },
149 NetworkValidator: &charts.NetworkValidator{
150 LogLevel: "debug",
151 LogFormat: "plain",
152 ConnectAddr: "1.1.1.1:20001",
153 ListenAddr: "0.0.0.0:4140",
154 Timeout: "10s",
155 },
156 Configs: charts.ConfigJSONs{
157 Global: "GlobalConfig",
158 Proxy: "ProxyConfig",
159 Install: "InstallConfig",
160 },
161 DebugContainer: &charts.DebugContainer{
162 Image: &charts.Image{
163 Name: "DebugImageName",
164 PullPolicy: "DebugImagePullPolicy",
165 Version: "DebugVersion",
166 },
167 },
168 ControllerReplicas: 1,
169 ProxyInjector: defaultValues.ProxyInjector,
170 ProfileValidator: defaultValues.ProfileValidator,
171 PolicyValidator: defaultValues.PolicyValidator,
172 }
173
174 haValues, err := testInstallOptionsHA(true)
175 if err != nil {
176 t.Fatalf("Unexpected error: %v\n", err)
177 }
178 addFakeTLSSecrets(haValues)
179
180 haWithOverridesValues, err := testInstallOptionsHA(true)
181 if err != nil {
182 t.Fatalf("Unexpected error: %v\n", err)
183 }
184
185 haWithOverridesValues.HighAvailability = true
186 haWithOverridesValues.ControllerReplicas = 2
187 haWithOverridesValues.Proxy.Resources.CPU.Request = "400m"
188 haWithOverridesValues.Proxy.Resources.Memory.Request = "300Mi"
189 haWithOverridesValues.EnablePodDisruptionBudget = true
190 addFakeTLSSecrets(haWithOverridesValues)
191
192 cniEnabledValues, err := testInstallOptions()
193 if err != nil {
194 t.Fatalf("Unexpected error: %v\n", err)
195 }
196
197 cniEnabledValues.CNIEnabled = true
198 addFakeTLSSecrets(cniEnabledValues)
199
200 withProxyIgnoresValues, err := testInstallOptions()
201 if err != nil {
202 t.Fatalf("Unexpected error: %v\n", err)
203 }
204 withProxyIgnoresValues.ProxyInit.IgnoreInboundPorts = "22,8100-8102"
205 withProxyIgnoresValues.ProxyInit.IgnoreOutboundPorts = "5432"
206 addFakeTLSSecrets(withProxyIgnoresValues)
207
208 withHeartBeatDisabledValues, err := testInstallOptions()
209 if err != nil {
210 t.Fatalf("Unexpected error: %v\n", err)
211 }
212 withHeartBeatDisabledValues.DisableHeartBeat = true
213 addFakeTLSSecrets(withHeartBeatDisabledValues)
214
215 withControlPlaneTracingValues, err := testInstallOptions()
216 if err != nil {
217 t.Fatalf("Unexpected error: %v\n", err)
218 }
219 withControlPlaneTracingValues.ControlPlaneTracing = true
220 addFakeTLSSecrets(withControlPlaneTracingValues)
221
222 customRegistryOverride := "my.custom.registry/linkerd-io"
223 withCustomRegistryValues, err := testInstallOptions()
224 if err != nil {
225 t.Fatalf("Unexpected error: %v\n", err)
226 }
227 flags, flagSet := makeProxyFlags(withCustomRegistryValues)
228 err = flagSet.Set("registry", customRegistryOverride)
229 if err != nil {
230 t.Fatalf("Unexpected error: %v\n", err)
231 }
232 err = flag.ApplySetFlags(withCustomRegistryValues, flags)
233 if err != nil {
234 t.Fatalf("Unexpected error: %v\n", err)
235 }
236 addFakeTLSSecrets(withCustomRegistryValues)
237
238 withCustomDestinationGetNetsValues, err := testInstallOptions()
239 if err != nil {
240 t.Fatalf("Unexpected error: %v\n", err)
241 }
242 withCustomDestinationGetNetsValues.ClusterNetworks = "10.0.0.0/8,100.64.0.0/10,172.0.0.0/8"
243 addFakeTLSSecrets(withCustomDestinationGetNetsValues)
244
245 testCases := []struct {
246 values *charts.Values
247 goldenFileName string
248 options values.Options
249 }{
250 {defaultValues, "install_default.golden", values.Options{}},
251 {metaValues, "install_output.golden", values.Options{}},
252 {haValues, "install_ha_output.golden", values.Options{}},
253 {haWithOverridesValues, "install_ha_with_overrides_output.golden", values.Options{}},
254 {cniEnabledValues, "install_no_init_container.golden", values.Options{}},
255 {withProxyIgnoresValues, "install_proxy_ignores.golden", values.Options{}},
256 {withHeartBeatDisabledValues, "install_heartbeat_disabled_output.golden", values.Options{}},
257 {withControlPlaneTracingValues, "install_controlplane_tracing_output.golden", values.Options{}},
258 {withCustomRegistryValues, "install_custom_registry.golden", values.Options{}},
259 {withCustomDestinationGetNetsValues, "install_default_override_dst_get_nets.golden", values.Options{}},
260 {defaultValues, "install_custom_domain.golden", values.Options{}},
261 {defaultValues, "install_values_file.golden", values.Options{ValueFiles: []string{filepath.Join("testdata", "install_config.yaml")}}},
262 {defaultValues, "install_default_token.golden", values.Options{Values: []string{"identity.serviceAccountTokenProjection=false"}}},
263 {gidValues, "install_gid_output.golden", values.Options{}},
264 }
265
266 for i, tc := range testCases {
267 tc := tc
268 t.Run(fmt.Sprintf("%d: %s", i, tc.goldenFileName), func(t *testing.T) {
269 valuesOverrides, err := tc.options.MergeValues(nil)
270 if err != nil {
271 t.Fatalf("Failed to get values overrides: %v", err)
272 }
273 var buf bytes.Buffer
274 if err := renderControlPlane(&buf, tc.values, valuesOverrides); err != nil {
275 t.Fatalf("Failed to render templates: %v", err)
276 }
277
278 if err := testDataDiffer.DiffTestYAML(tc.goldenFileName, buf.String()); err != nil {
279 t.Error(err)
280 }
281 })
282 }
283 }
284
285 func TestIgnoreCluster(t *testing.T) {
286 defaultValues, err := testInstallOptions()
287 if err != nil {
288 t.Fatal(err)
289 }
290 addFakeTLSSecrets(defaultValues)
291
292 var buf bytes.Buffer
293 if err := installControlPlane(context.Background(), nil, &buf, defaultValues, nil, values.Options{}); err != nil {
294 t.Fatal(err)
295 }
296 }
297
298 func TestRenderCRDs(t *testing.T) {
299 defaultValues, err := testInstallOptions()
300 if err != nil {
301 t.Fatal(err)
302 }
303 addFakeTLSSecrets(defaultValues)
304
305 var buf bytes.Buffer
306 if err := renderCRDs(&buf, values.Options{}); err != nil {
307 t.Fatalf("Failed to render templates: %v", err)
308 }
309 if err := testDataDiffer.DiffTestYAML("install_crds.golden", buf.String()); err != nil {
310 t.Error(err)
311 }
312 }
313
314 func TestValidateAndBuild_Errors(t *testing.T) {
315 t.Run("Fails validation for invalid ignoreInboundPorts", func(t *testing.T) {
316 values, err := testInstallOptions()
317 if err != nil {
318 t.Fatalf("Unexpected error: %v\n", err)
319 }
320 values.ProxyInit.IgnoreInboundPorts = "-25"
321 err = validateValues(context.Background(), nil, values)
322 if err == nil {
323 t.Fatal("expected error but got nothing")
324 }
325 })
326
327 t.Run("Fails validation for invalid ignoreOutboundPorts", func(t *testing.T) {
328 values, err := testInstallOptions()
329 if err != nil {
330 t.Fatalf("Unexpected error: %v\n", err)
331 }
332 values.ProxyInit.IgnoreOutboundPorts = "-25"
333 err = validateValues(context.Background(), nil, values)
334 if err == nil {
335 t.Fatal("expected error but got nothing")
336 }
337 })
338 }
339
340 func testInstallOptionsFakeCerts() (*charts.Values, error) {
341 values, err := testInstallOptions()
342 if err != nil {
343 return nil, err
344 }
345 addFakeTLSSecrets(values)
346 return values, nil
347 }
348
349 func testInstallOptions() (*charts.Values, error) {
350 return testInstallOptionsHA(false)
351 }
352
353 func testInstallOptionsHA(ha bool) (*charts.Values, error) {
354 values, err := testInstallOptionsNoCerts(ha)
355 if err != nil {
356 return nil, err
357 }
358
359 data, err := os.ReadFile(filepath.Join("testdata", "valid-crt.pem"))
360 if err != nil {
361 return nil, err
362 }
363
364 crt, err := tls.DecodePEMCrt(string(data))
365 if err != nil {
366 return nil, err
367 }
368 values.Identity.Issuer.TLS.CrtPEM = crt.EncodeCertificatePEM()
369
370 key, err := loadKeyPEM(filepath.Join("testdata", "valid-key.pem"))
371 if err != nil {
372 return nil, err
373 }
374 values.Identity.Issuer.TLS.KeyPEM = key
375
376 data, err = os.ReadFile(filepath.Join("testdata", "valid-trust-anchors.pem"))
377 if err != nil {
378 return nil, err
379 }
380 values.IdentityTrustAnchorsPEM = string(data)
381
382 return values, nil
383 }
384
385 func testInstallOptionsNoCerts(ha bool) (*charts.Values, error) {
386 values, err := charts.NewValues()
387 if err != nil {
388 return nil, err
389 }
390 if ha {
391 if err = charts.MergeHAValues(values); err != nil {
392 return nil, err
393 }
394 }
395
396 values.Proxy.Image.Version = installProxyVersion
397 values.DebugContainer.Image.Version = installDebugVersion
398 values.LinkerdVersion = installControlPlaneVersion
399 values.HeartbeatSchedule = fakeHeartbeatSchedule()
400
401 return values, nil
402 }
403
404 func testInstallValues() (*charts.Values, error) {
405 values, err := charts.NewValues()
406 if err != nil {
407 return nil, err
408 }
409
410 values.Proxy.Image.Version = installProxyVersion
411 values.DebugContainer.Image.Version = installDebugVersion
412 values.LinkerdVersion = installControlPlaneVersion
413 values.PolicyController.Image.Version = installControlPlaneVersion
414 values.HeartbeatSchedule = fakeHeartbeatSchedule()
415
416 identityCert, err := os.ReadFile(filepath.Join("testdata", "valid-crt.pem"))
417 if err != nil {
418 return nil, err
419 }
420 identityKey, err := os.ReadFile(filepath.Join("testdata", "valid-key.pem"))
421 if err != nil {
422 return nil, err
423 }
424 trustAnchorsPEM, err := os.ReadFile(filepath.Join("testdata", "valid-trust-anchors.pem"))
425 if err != nil {
426 return nil, err
427 }
428
429 values.Identity.Issuer.TLS.CrtPEM = string(identityCert)
430 values.Identity.Issuer.TLS.KeyPEM = string(identityKey)
431 values.IdentityTrustAnchorsPEM = string(trustAnchorsPEM)
432 return values, nil
433 }
434
435 func TestValidate(t *testing.T) {
436 t.Run("Accepts the default options as valid", func(t *testing.T) {
437 values, err := testInstallOptions()
438 if err != nil {
439 t.Fatalf("Unexpected error: %v\n", err)
440 }
441
442 if err := validateValues(context.Background(), nil, values); err != nil {
443 t.Fatalf("Unexpected error: %s", err)
444 }
445 })
446
447 t.Run("Rejects invalid destination networks", func(t *testing.T) {
448 values, err := testInstallOptions()
449 if err != nil {
450 t.Fatalf("Unexpected error: %v\n", err)
451 }
452
453 values.ClusterNetworks = "wrong"
454 expected := "cannot parse destination get networks: invalid CIDR address: wrong"
455
456 err = validateValues(context.Background(), nil, values)
457 if err == nil {
458 t.Fatal("Expected error, got nothing")
459 }
460 if err.Error() != expected {
461 t.Fatalf("Expected error string\"%s\", got \"%s\"", expected, err)
462 }
463 })
464
465 t.Run("Rejects invalid controller log level", func(t *testing.T) {
466 values, err := testInstallOptions()
467 if err != nil {
468 t.Fatalf("Unexpected error: %v\n", err)
469 }
470
471 values.ControllerLogLevel = "super"
472 expected := "--controller-log-level must be one of: panic, fatal, error, warn, info, debug, trace"
473
474 err = validateValues(context.Background(), nil, values)
475 if err == nil {
476 t.Fatal("Expected error, got nothing")
477 }
478 if err.Error() != expected {
479 t.Fatalf("Expected error string\"%s\", got \"%s\"", expected, err)
480 }
481 })
482
483 t.Run("Properly validates proxy log level", func(t *testing.T) {
484 testCases := []struct {
485 input string
486 valid bool
487 }{
488 {"", false},
489 {"off", true},
490 {"info", true},
491 {"somemodule", true},
492 {"bad%name", false},
493 {"linkerd=debug", true},
494 {"linkerd2%proxy=debug", false},
495 {"linkerd=foobar", false},
496 {"linker2d_proxy,std::option", true},
497 {"warn,linkerd=info", true},
498 {"warn,linkerd=foobar", false},
499 }
500
501 values, err := testInstallOptions()
502 if err != nil {
503 t.Fatalf("Unexpected error: %v\n", err)
504 }
505
506 for _, tc := range testCases {
507 values.Proxy.LogLevel = tc.input
508 err := validateValues(context.Background(), nil, values)
509 if tc.valid && err != nil {
510 t.Fatalf("Error not expected: %s", err)
511 }
512 if !tc.valid && err == nil {
513 t.Fatalf("Expected error string \"%s is not a valid proxy log level\", got nothing", tc.input)
514 }
515 expectedErr := fmt.Sprintf("\"%s\" is not a valid proxy log level - for allowed syntax check https://docs.rs/env_logger/0.6.0/env_logger/#enabling-logging", tc.input)
516 if tc.input == "" {
517 expectedErr = "--proxy-log-level must not be empty"
518 }
519 if !tc.valid && err.Error() != expectedErr {
520 t.Fatalf("Expected error string \"%s\", got \"%s\"; input=\"%s\"", expectedErr, err, tc.input)
521 }
522 }
523 })
524
525 t.Run("Validates the issuer certs upon install", func(t *testing.T) {
526
527 testCases := []struct {
528 crtFilePrefix string
529 expectedError string
530 }{
531 {"valid", ""},
532 {"valid-with-rsa-anchor", ""},
533 {"expired", "failed to validate issuer credentials: not valid anymore. Expired on 1990-01-01T01:01:11Z"},
534 {"not-valid-yet", "failed to validate issuer credentials: not valid before: 2100-01-01T01:00:51Z"},
535 {"wrong-algo", "failed to validate issuer credentials: must use P-256 curve for public key, instead P-521 was used"},
536 }
537 for _, tc := range testCases {
538
539 values, err := testInstallOptions()
540 if err != nil {
541 t.Fatalf("Unexpected error: %v\n", err)
542 }
543
544 crt, err := loadCrtPEM(filepath.Join("testdata", tc.crtFilePrefix+"-crt.pem"))
545 if err != nil {
546 t.Fatal(err)
547 }
548 values.Identity.Issuer.TLS.CrtPEM = crt
549
550 key, err := loadKeyPEM(filepath.Join("testdata", tc.crtFilePrefix+"-key.pem"))
551 if err != nil {
552 t.Fatal(err)
553 }
554 values.Identity.Issuer.TLS.KeyPEM = key
555
556 ca, err := os.ReadFile(filepath.Join("testdata", tc.crtFilePrefix+"-trust-anchors.pem"))
557 if err != nil {
558 t.Fatal(err)
559 }
560 values.IdentityTrustAnchorsPEM = string(ca)
561
562 err = validateValues(context.Background(), nil, values)
563
564 if tc.expectedError != "" {
565 if err == nil {
566 t.Fatal("Expected error, got nothing")
567 }
568 if err.Error() != tc.expectedError {
569 t.Fatalf("Expected error string\"%s\", got \"%s\"", tc.expectedError, err)
570 }
571 } else if err != nil {
572 t.Fatalf("Expected no error but got \"%s\"", err)
573 }
574 }
575 })
576
577 t.Run("Rejects identity cert files data when external issuer is set", func(t *testing.T) {
578
579 values, err := testInstallOptionsNoCerts(false)
580 if err != nil {
581 t.Fatalf("Unexpected error: %v\n", err)
582 }
583
584 values.Identity.Issuer.Scheme = string(corev1.SecretTypeTLS)
585
586 withoutCertDataOptions, _ := values.DeepCopy()
587
588 withCrtFile, _ := values.DeepCopy()
589 withCrtFile.Identity.Issuer.TLS.CrtPEM = "certificate"
590
591 withKeyFile, _ := values.DeepCopy()
592 withKeyFile.Identity.Issuer.TLS.KeyPEM = "key"
593
594 testCases := []struct {
595 input *charts.Values
596 expectedError string
597 }{
598 {withoutCertDataOptions, ""},
599 {withCrtFile, "--identity-issuer-certificate-file must not be specified if --identity-external-issuer=true"},
600 {withKeyFile, "--identity-issuer-key-file must not be specified if --identity-external-issuer=true"},
601 }
602
603 for _, tc := range testCases {
604 err = validateValues(context.Background(), nil, tc.input)
605
606 if tc.expectedError != "" {
607 if err == nil {
608 t.Fatalf("Expected error '%s', got nothing", tc.expectedError)
609 }
610 if err.Error() != tc.expectedError {
611 t.Fatalf("Expected error string\"%s\", got \"%s\"", tc.expectedError, err)
612 }
613 } else if err != nil {
614 t.Fatalf("Expected no error but got \"%s\"", err)
615 }
616 }
617 })
618
619 t.Run("Rejects invalid default-inbound-policy", func(t *testing.T) {
620 values, err := testInstallOptions()
621 if err != nil {
622 t.Fatalf("Unexpected error: %v\n", err)
623 }
624 values.Proxy.DefaultInboundPolicy = "everybody"
625 expected := "--default-inbound-policy must be one of: all-authenticated, all-unauthenticated, cluster-authenticated, cluster-unauthenticated, deny (got everybody)"
626
627 err = validateValues(context.Background(), nil, values)
628 if err == nil {
629 t.Fatal("Expected error, got nothing")
630 }
631 if err.Error() != expected {
632 t.Fatalf("Expected error string \"%s\", got \"%s\"", expected, err)
633 }
634 })
635 }
636
637 func fakeHeartbeatSchedule() string {
638 return "1 2 3 4 5"
639 }
640
641 func addFakeTLSSecrets(values *charts.Values) {
642 values.ProxyInjector.ExternalSecret = true
643 values.ProxyInjector.CaBundle = "proxy injector CA bundle"
644 values.ProxyInjector.InjectCaFrom = ""
645 values.ProxyInjector.InjectCaFromSecret = ""
646 values.ProfileValidator.ExternalSecret = true
647 values.ProfileValidator.CaBundle = "profile validator CA bundle"
648 values.ProfileValidator.InjectCaFrom = ""
649 values.ProfileValidator.InjectCaFromSecret = ""
650 values.PolicyValidator.ExternalSecret = true
651 values.PolicyValidator.CaBundle = "policy validator CA bundle"
652 values.PolicyValidator.InjectCaFrom = ""
653 values.PolicyValidator.InjectCaFromSecret = ""
654 }
655
View as plain text