package healthcheck import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "reflect" "strings" "testing" "time" "github.com/linkerd/linkerd2/pkg/issuercerts" "github.com/linkerd/linkerd2/pkg/tls" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes/duration" "github.com/linkerd/linkerd2/controller/api/public" healthcheckPb "github.com/linkerd/linkerd2/controller/gen/common/healthcheck" configPb "github.com/linkerd/linkerd2/controller/gen/config" pb "github.com/linkerd/linkerd2/controller/gen/public" "github.com/linkerd/linkerd2/pkg/identity" "github.com/linkerd/linkerd2/pkg/k8s" corev1 "k8s.io/api/core/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) type observer struct { results []string } func newObserver() *observer { return &observer{ results: []string{}, } } func (o *observer) resultFn(result *CheckResult) { res := fmt.Sprintf("%s %s", result.Category, result.Description) if result.Err != nil { res += fmt.Sprintf(": %s", result.Err) } o.results = append(o.results, res) } func (hc *HealthChecker) addCheckAsCategory( testCategoryID CategoryID, categoryID CategoryID, desc string, ) { testCategory := category{ id: testCategoryID, checkers: []checker{}, } for _, cat := range hc.categories { if cat.id == categoryID { for _, ch := range cat.checkers { if ch.description == desc { testCategory.checkers = append(testCategory.checkers, ch) break } } break } } hc.addCategory(testCategory) } func TestHealthChecker(t *testing.T) { nullObserver := func(*CheckResult) {} passingCheck1 := category{ id: "cat1", checkers: []checker{ { description: "desc1", check: func(context.Context) error { return nil }, retryDeadline: time.Time{}, }, }, } passingCheck2 := category{ id: "cat2", checkers: []checker{ { description: "desc2", check: func(context.Context) error { return nil }, retryDeadline: time.Time{}, }, }, } failingCheck := category{ id: "cat3", checkers: []checker{ { description: "desc3", check: func(context.Context) error { return fmt.Errorf("error") }, retryDeadline: time.Time{}, }, }, } passingRPCClient := public.MockAPIClient{ SelfCheckResponseToReturn: &healthcheckPb.SelfCheckResponse{ Results: []*healthcheckPb.CheckResult{ { SubsystemName: "rpc1", CheckDescription: "rpc desc1", Status: healthcheckPb.CheckStatus_OK, }, }, }, } passingRPCCheck := category{ id: "cat4", checkers: []checker{ { description: "desc4", checkRPC: func(context.Context) (*healthcheckPb.SelfCheckResponse, error) { return passingRPCClient.SelfCheck(context.Background(), &healthcheckPb.SelfCheckRequest{}) }, retryDeadline: time.Time{}, }, }, } failingRPCClient := public.MockAPIClient{ SelfCheckResponseToReturn: &healthcheckPb.SelfCheckResponse{ Results: []*healthcheckPb.CheckResult{ { SubsystemName: "rpc2", CheckDescription: "rpc desc2", Status: healthcheckPb.CheckStatus_FAIL, FriendlyMessageToUser: "rpc error", }, }, }, } failingRPCCheck := category{ id: "cat5", checkers: []checker{ { description: "desc5", checkRPC: func(context.Context) (*healthcheckPb.SelfCheckResponse, error) { return failingRPCClient.SelfCheck(context.Background(), &healthcheckPb.SelfCheckRequest{}) }, retryDeadline: time.Time{}, }, }, } fatalCheck := category{ id: "cat6", checkers: []checker{ { description: "desc6", fatal: true, check: func(context.Context) error { return fmt.Errorf("fatal") }, retryDeadline: time.Time{}, }, }, } skippingCheck := category{ id: "cat7", checkers: []checker{ { description: "skip", check: func(context.Context) error { return &SkipError{Reason: "needs skipping"} }, retryDeadline: time.Time{}, }, }, } skippingRPCCheck := category{ id: "cat8", checkers: []checker{ { description: "skipRpc", checkRPC: func(context.Context) (*healthcheckPb.SelfCheckResponse, error) { return nil, &SkipError{Reason: "needs skipping"} }, retryDeadline: time.Time{}, }, }, } t.Run("Notifies observer of all results", func(t *testing.T) { hc := NewHealthChecker( []CategoryID{}, &Options{}, ) hc.addCategory(passingCheck1) hc.addCategory(passingCheck2) hc.addCategory(failingCheck) hc.addCategory(passingRPCCheck) hc.addCategory(failingRPCCheck) expectedResults := []string{ "cat1 desc1", "cat2 desc2", "cat3 desc3: error", "cat4 desc4", "cat4 [rpc1] rpc desc1", "cat5 desc5", "cat5 [rpc2] rpc desc2: rpc error", } obs := newObserver() hc.RunChecks(obs.resultFn) if !reflect.DeepEqual(obs.results, expectedResults) { t.Fatalf("Expected results %v, but got %v", expectedResults, obs.results) } }) t.Run("Is successful if all checks were successful", func(t *testing.T) { hc := NewHealthChecker( []CategoryID{}, &Options{}, ) hc.addCategory(passingCheck1) hc.addCategory(passingCheck2) hc.addCategory(passingRPCCheck) success := hc.RunChecks(nullObserver) if !success { t.Fatalf("Expecting checks to be successful, but got [%t]", success) } }) t.Run("Is not successful if one check fails", func(t *testing.T) { hc := NewHealthChecker( []CategoryID{}, &Options{}, ) hc.addCategory(passingCheck1) hc.addCategory(failingCheck) hc.addCategory(passingCheck2) success := hc.RunChecks(nullObserver) if success { t.Fatalf("Expecting checks to not be successful, but got [%t]", success) } }) t.Run("Is not successful if one RPC check fails", func(t *testing.T) { hc := NewHealthChecker( []CategoryID{}, &Options{}, ) hc.addCategory(passingCheck1) hc.addCategory(failingRPCCheck) hc.addCategory(passingCheck2) success := hc.RunChecks(nullObserver) if success { t.Fatalf("Expecting checks to not be successful, but got [%t]", success) } }) t.Run("Does not run remaining check if fatal check fails", func(t *testing.T) { hc := NewHealthChecker( []CategoryID{}, &Options{}, ) hc.addCategory(passingCheck1) hc.addCategory(fatalCheck) hc.addCategory(passingCheck2) expectedResults := []string{ "cat1 desc1", "cat6 desc6: fatal", } obs := newObserver() hc.RunChecks(obs.resultFn) if !reflect.DeepEqual(obs.results, expectedResults) { t.Fatalf("Expected results %v, but got %v", expectedResults, obs.results) } }) t.Run("Retries checks if retry is specified", func(t *testing.T) { retryWindow = 0 returnError := true retryCheck := category{ id: "cat7", checkers: []checker{ { description: "desc7", retryDeadline: time.Now().Add(100 * time.Second), check: func(context.Context) error { if returnError { returnError = false return fmt.Errorf("retry") } return nil }, }, }, } hc := NewHealthChecker( []CategoryID{}, &Options{}, ) hc.addCategory(passingCheck1) hc.addCategory(retryCheck) observedResults := make([]string, 0) observer := func(result *CheckResult) { res := fmt.Sprintf("%s %s retry=%t", result.Category, result.Description, result.Retry) if result.Err != nil { res += fmt.Sprintf(": %s", result.Err) } observedResults = append(observedResults, res) } expectedResults := []string{ "cat1 desc1 retry=false", "cat7 desc7 retry=true: waiting for check to complete", "cat7 desc7 retry=false", } hc.RunChecks(observer) if !reflect.DeepEqual(observedResults, expectedResults) { t.Fatalf("Expected results %v, but got %v", expectedResults, observedResults) } }) t.Run("Does not notify observer of skipped checks", func(t *testing.T) { hc := NewHealthChecker( []CategoryID{}, &Options{}, ) hc.addCategory(passingCheck1) hc.addCategory(skippingCheck) hc.addCategory(skippingRPCCheck) expectedResults := []string{ "cat1 desc1", } obs := newObserver() hc.RunChecks(obs.resultFn) if !reflect.DeepEqual(obs.results, expectedResults) { t.Fatalf("Expected results %v, but got %v", expectedResults, obs.results) } }) } func TestCheckCanCreate(t *testing.T) { exp := fmt.Errorf("not authorized to access deployments.apps") hc := NewHealthChecker( []CategoryID{}, &Options{}, ) var err error hc.kubeAPI, err = k8s.NewFakeAPI() if err != nil { t.Fatalf("Unexpected error: %s", err) } err = hc.checkCanCreate("", "apps", "v1", "deployments") if err == nil || err.Error() != exp.Error() { t.Fatalf("Unexpected error (Expected: %s, Got: %s)", exp, err) } } func TestCheckExtensionAPIServerAuthentication(t *testing.T) { tests := []struct { k8sConfigs []string err error }{ { []string{}, fmt.Errorf("configmaps %q not found", k8s.ExtensionAPIServerAuthenticationConfigMapName), }, { []string{` apiVersion: v1 kind: ConfigMap metadata: name: extension-apiserver-authentication namespace: kube-system data: foo : 'bar' `, }, fmt.Errorf("--%s is not configured", k8s.ExtensionAPIServerAuthenticationRequestHeaderClientCAFileKey), }, { []string{fmt.Sprintf(` apiVersion: v1 kind: ConfigMap metadata: name: extension-apiserver-authentication namespace: kube-system data: %s : 'bar' `, k8s.ExtensionAPIServerAuthenticationRequestHeaderClientCAFileKey)}, nil, }, } for i, test := range tests { test := test t.Run(fmt.Sprintf("%d: returns expected extension apiserver authentication check result", i), func(t *testing.T) { hc := NewHealthChecker([]CategoryID{}, &Options{}) var err error hc.kubeAPI, err = k8s.NewFakeAPI(test.k8sConfigs...) if err != nil { t.Fatal(err) } err = hc.checkExtensionAPIServerAuthentication() if err != nil || test.err != nil { if (err == nil && test.err != nil) || (err != nil && test.err == nil) || (err.Error() != test.err.Error()) { t.Fatalf("Unexpected error (Expected: %s, Got: %s)", test.err, err) } } }) } } func TestCheckClockSkew(t *testing.T) { tests := []struct { k8sConfigs []string err error }{ { []string{}, nil, }, { []string{`apiVersion: v1 kind: Node metadata: name: test-node status: conditions: - lastHeartbeatTime: "2000-01-01T01:00:00Z" status: "True" type: Ready`, }, fmt.Errorf("clock skew detected for node(s): test-node"), }, } for i, test := range tests { test := test // pin t.Run(fmt.Sprintf("%d: returns expected clock skew check result", i), func(t *testing.T) { hc := NewHealthChecker( []CategoryID{}, &Options{}, ) var err error hc.kubeAPI, err = k8s.NewFakeAPI(test.k8sConfigs...) if err != nil { t.Fatalf("Unexpected error: %s", err) } err = hc.checkClockSkew() if err != nil || test.err != nil { if (err == nil && test.err != nil) || (err != nil && test.err == nil) || (err.Error() != test.err.Error()) { t.Fatalf("Unexpected error (Expected: %s, Got: %s)", test.err, err) } } }) } } func TestChecCapability(t *testing.T) { tests := []struct { k8sConfigs []string err error }{ { []string{}, nil, }, { []string{`apiVersion: policy/v1beta1 kind: PodSecurityPolicy metadata: name: restricted spec: requiredDropCapabilities: - ALL`, }, fmt.Errorf("found 1 PodSecurityPolicies, but none provide TEST_CAP, proxy injection will fail if the PSP admission controller is running"), }, } for i, test := range tests { test := test // pin t.Run(fmt.Sprintf("%d: returns expected capability result", i), func(t *testing.T) { hc := NewHealthChecker( []CategoryID{}, &Options{}, ) var err error hc.kubeAPI, err = k8s.NewFakeAPI(test.k8sConfigs...) if err != nil { t.Fatalf("Unexpected error: %s", err) } err = hc.checkCapability("TEST_CAP") if err != nil || test.err != nil { if (err == nil && test.err != nil) || (err != nil && test.err == nil) || (err.Error() != test.err.Error()) { t.Fatalf("Unexpected error (Expected: %s, Got: %s)", test.err, err) } } }) } } func TestConfigExists(t *testing.T) { testCases := []struct { k8sConfigs []string results []string }{ { []string{}, []string{"linkerd-config control plane Namespace exists: The \"test-ns\" namespace does not exist"}, }, { []string{` apiVersion: v1 kind: Namespace metadata: name: test-ns `, }, []string{ "linkerd-config control plane Namespace exists", "linkerd-config control plane ClusterRoles exist: missing ClusterRoles: linkerd-test-ns-controller, linkerd-test-ns-identity, linkerd-test-ns-prometheus, linkerd-test-ns-proxy-injector, linkerd-test-ns-sp-validator, linkerd-test-ns-tap", }, }, { []string{` apiVersion: v1 kind: Namespace metadata: name: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-controller labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-identity labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-prometheus labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-proxy-injector labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-sp-validator labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-tap labels: linkerd.io/control-plane-ns: test-ns `, }, []string{ "linkerd-config control plane Namespace exists", "linkerd-config control plane ClusterRoles exist", "linkerd-config control plane ClusterRoleBindings exist: missing ClusterRoleBindings: linkerd-test-ns-controller, linkerd-test-ns-identity, linkerd-test-ns-prometheus, linkerd-test-ns-proxy-injector, linkerd-test-ns-sp-validator, linkerd-test-ns-tap", }, }, { []string{` apiVersion: v1 kind: Namespace metadata: name: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-controller labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-identity labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-prometheus labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-proxy-injector labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-sp-validator labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-tap labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-controller labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-identity labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-prometheus labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-proxy-injector labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-sp-validator labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-tap labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-controller namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-identity namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-prometheus namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-proxy-injector namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-sp-validator namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-grafana namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-heartbeat namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-web namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-tap namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, }, []string{ "linkerd-config control plane Namespace exists", "linkerd-config control plane ClusterRoles exist", "linkerd-config control plane ClusterRoleBindings exist", "linkerd-config control plane ServiceAccounts exist", "linkerd-config control plane CustomResourceDefinitions exist: missing CustomResourceDefinitions: serviceprofiles.linkerd.io", }, }, { []string{` apiVersion: v1 kind: Namespace metadata: name: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-controller labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-identity labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-prometheus labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-proxy-injector labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-sp-validator labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-tap labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-controller labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-identity labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-prometheus labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-proxy-injector labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-sp-validator labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-tap labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-controller namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-identity namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-prometheus namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-proxy-injector namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-sp-validator namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-grafana namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-heartbeat namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-web namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-tap namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: serviceprofiles.linkerd.io labels: linkerd.io/control-plane-ns: test-ns `, }, []string{ "linkerd-config control plane Namespace exists", "linkerd-config control plane ClusterRoles exist", "linkerd-config control plane ClusterRoleBindings exist", "linkerd-config control plane ServiceAccounts exist", "linkerd-config control plane CustomResourceDefinitions exist", "linkerd-config control plane MutatingWebhookConfigurations exist: missing MutatingWebhookConfigurations: linkerd-proxy-injector-webhook-config", }, }, { []string{` apiVersion: v1 kind: Namespace metadata: name: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-controller labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-identity labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-prometheus labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-proxy-injector labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-sp-validator labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-tap labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-controller labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-identity labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-prometheus labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-proxy-injector labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-sp-validator labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-tap labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-controller namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-identity namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-prometheus namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-proxy-injector namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-sp-validator namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-grafana namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-heartbeat namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-web namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-tap namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: serviceprofiles.linkerd.io labels: linkerd.io/control-plane-ns: test-ns `, ` apiVersion: admissionregistration.k8s.io/v1beta1 kind: MutatingWebhookConfiguration metadata: name: linkerd-proxy-injector-webhook-config labels: linkerd.io/control-plane-ns: test-ns `, }, []string{ "linkerd-config control plane Namespace exists", "linkerd-config control plane ClusterRoles exist", "linkerd-config control plane ClusterRoleBindings exist", "linkerd-config control plane ServiceAccounts exist", "linkerd-config control plane CustomResourceDefinitions exist", "linkerd-config control plane MutatingWebhookConfigurations exist", "linkerd-config control plane ValidatingWebhookConfigurations exist: missing ValidatingWebhookConfigurations: linkerd-sp-validator-webhook-config", }, }, { []string{` apiVersion: v1 kind: Namespace metadata: name: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-controller labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-identity labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-prometheus labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-proxy-injector labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-sp-validator labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-tap labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-controller labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-identity labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-prometheus labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-proxy-injector labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-sp-validator labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-tap labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-controller namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-identity namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-prometheus namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-proxy-injector namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-sp-validator namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-grafana namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-heartbeat namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-web namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-tap namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: serviceprofiles.linkerd.io labels: linkerd.io/control-plane-ns: test-ns `, ` apiVersion: admissionregistration.k8s.io/v1beta1 kind: MutatingWebhookConfiguration metadata: name: linkerd-proxy-injector-webhook-config labels: linkerd.io/control-plane-ns: test-ns `, ` apiVersion: admissionregistration.k8s.io/v1beta1 kind: ValidatingWebhookConfiguration metadata: name: linkerd-sp-validator-webhook-config labels: linkerd.io/control-plane-ns: test-ns `, }, []string{ "linkerd-config control plane Namespace exists", "linkerd-config control plane ClusterRoles exist", "linkerd-config control plane ClusterRoleBindings exist", "linkerd-config control plane ServiceAccounts exist", "linkerd-config control plane CustomResourceDefinitions exist", "linkerd-config control plane MutatingWebhookConfigurations exist", "linkerd-config control plane ValidatingWebhookConfigurations exist", "linkerd-config control plane PodSecurityPolicies exist: missing PodSecurityPolicies: linkerd-test-ns-control-plane", }, }, { []string{` apiVersion: v1 kind: Namespace metadata: name: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-controller labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-identity labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-prometheus labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-proxy-injector labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-sp-validator labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-tap labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-controller labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-identity labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-prometheus labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-proxy-injector labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-sp-validator labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-test-ns-tap labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-controller namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-identity namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-prometheus namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-proxy-injector namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-sp-validator namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-grafana namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-heartbeat namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-web namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` kind: ServiceAccount apiVersion: v1 metadata: name: linkerd-tap namespace: test-ns labels: linkerd.io/control-plane-ns: test-ns `, ` apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: serviceprofiles.linkerd.io labels: linkerd.io/control-plane-ns: test-ns `, ` apiVersion: admissionregistration.k8s.io/v1beta1 kind: MutatingWebhookConfiguration metadata: name: linkerd-proxy-injector-webhook-config labels: linkerd.io/control-plane-ns: test-ns `, ` apiVersion: admissionregistration.k8s.io/v1beta1 kind: ValidatingWebhookConfiguration metadata: name: linkerd-sp-validator-webhook-config labels: linkerd.io/control-plane-ns: test-ns `, ` apiVersion: policy/v1beta1 kind: PodSecurityPolicy metadata: name: linkerd-test-ns-control-plane labels: linkerd.io/control-plane-ns: test-ns `, }, []string{ "linkerd-config control plane Namespace exists", "linkerd-config control plane ClusterRoles exist", "linkerd-config control plane ClusterRoleBindings exist", "linkerd-config control plane ServiceAccounts exist", "linkerd-config control plane CustomResourceDefinitions exist", "linkerd-config control plane MutatingWebhookConfigurations exist", "linkerd-config control plane ValidatingWebhookConfigurations exist", "linkerd-config control plane PodSecurityPolicies exist", }, }, } for i, tc := range testCases { tc := tc // pin t.Run(fmt.Sprintf("%d: returns expected config result", i), func(t *testing.T) { hc := NewHealthChecker( []CategoryID{LinkerdConfigChecks}, &Options{ ControlPlaneNamespace: "test-ns", }, ) var err error hc.kubeAPI, err = k8s.NewFakeAPI(tc.k8sConfigs...) if err != nil { t.Fatalf("Unexpected error: %s", err) } obs := newObserver() hc.RunChecks(obs.resultFn) if !reflect.DeepEqual(obs.results, tc.results) { t.Fatalf("Expected results\n%s,\nbut got:\n%s", strings.Join(tc.results, "\n"), strings.Join(obs.results, "\n")) } }) } } func TestCheckControlPlanePodExistence(t *testing.T) { var testCases = []struct { checkDescription string resources []string expected []string }{ { checkDescription: "controller pod is running", resources: []string{` apiVersion: v1 kind: Pod metadata: name: linkerd-controller-6f78cbd47-bc557 namespace: test-ns status: phase: Running podIP: 1.2.3.4 `, }, expected: []string{ "cat1 controller pod is running", }, }, { checkDescription: "'linkerd-config' config map exists", resources: []string{` apiVersion: v1 kind: ConfigMap metadata: name: linkerd-config namespace: test-ns `, }, expected: []string{ "cat1 'linkerd-config' config map exists", }, }, } for id, testCase := range testCases { testCase := testCase t.Run(fmt.Sprintf("%d", id), func(t *testing.T) { hc := NewHealthChecker( []CategoryID{}, &Options{ ControlPlaneNamespace: "test-ns", }, ) var err error hc.kubeAPI, err = k8s.NewFakeAPI(testCase.resources...) if err != nil { t.Fatalf("Unexpected error: %s", err) } // validate that this check relies on the k8s api, not on hc.controlPlanePods hc.addCheckAsCategory("cat1", LinkerdControlPlaneExistenceChecks, testCase.checkDescription) obs := newObserver() hc.RunChecks(obs.resultFn) if !reflect.DeepEqual(obs.results, testCase.expected) { t.Fatalf("Expected results %v, but got %v", testCase.expected, obs.results) } }) } } func proxiesWithCertificates(certificates ...string) []string { result := []string{} for i, certificate := range certificates { result = append(result, fmt.Sprintf(` apiVersion: v1 kind: Pod metadata: name: pod-%d namespace: namespace-%d labels: %s: linkerd spec: containers: - name: %s env: - name: %s value: %s `, i, i, k8s.ControllerNSLabel, k8s.ProxyContainerName, identity.EnvTrustAnchors, certificate)) } return result } func TestCheckDataPlaneProxiesCertificate(t *testing.T) { const currentCertificate = "current-certificate" const oldCertificate = "old-certificate" linkerdConfigMap := fmt.Sprintf(` kind: ConfigMap apiVersion: v1 metadata: name: %s data: global: | {"identityContext":{"trustAnchorsPem": "%s"}} `, k8s.ConfigConfigMapName, currentCertificate) var testCases = []struct { checkDescription string resources []string namespace string expectedErr error }{ { checkDescription: "all proxies match CA certificate (all namespaces)", resources: proxiesWithCertificates(currentCertificate, currentCertificate), namespace: "", expectedErr: nil, }, { checkDescription: "some proxies match CA certificate (all namespaces)", resources: proxiesWithCertificates(currentCertificate, oldCertificate), namespace: "", expectedErr: errors.New("Some pods do not have the current trust bundle and must be restarted:\n\t* namespace-1/pod-1"), }, { checkDescription: "no proxies match CA certificate (all namespaces)", resources: proxiesWithCertificates(oldCertificate, oldCertificate), namespace: "", expectedErr: errors.New("Some pods do not have the current trust bundle and must be restarted:\n\t* namespace-0/pod-0\n\t* namespace-1/pod-1"), }, { checkDescription: "some proxies match CA certificate (match in target namespace)", resources: proxiesWithCertificates(currentCertificate, oldCertificate), namespace: "namespace-0", expectedErr: nil, }, { checkDescription: "some proxies match CA certificate (unmatch in target namespace)", resources: proxiesWithCertificates(currentCertificate, oldCertificate), namespace: "namespace-1", expectedErr: errors.New("Some pods do not have the current trust bundle and must be restarted:\n\t* pod-1"), }, { checkDescription: "no proxies match CA certificate (specific namespace)", resources: proxiesWithCertificates(oldCertificate, oldCertificate), namespace: "namespace-0", expectedErr: errors.New("Some pods do not have the current trust bundle and must be restarted:\n\t* pod-0"), }, } for id, testCase := range testCases { testCase := testCase t.Run(fmt.Sprintf("%d", id), func(t *testing.T) { hc := NewHealthChecker([]CategoryID{}, &Options{}) hc.DataPlaneNamespace = testCase.namespace var err error hc.kubeAPI, err = k8s.NewFakeAPI(append(testCase.resources, linkerdConfigMap)...) if err != nil { t.Fatalf("Unexpected error: %q", err) } err = hc.checkDataPlaneProxiesCertificate() if !reflect.DeepEqual(err, testCase.expectedErr) { t.Fatalf("Error %q does not match expected error: %q", err, testCase.expectedErr) } }) } } func TestValidateControlPlanePods(t *testing.T) { pod := func(name string, phase corev1.PodPhase, ready bool) corev1.Pod { return corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: name}, Status: corev1.PodStatus{ Phase: phase, ContainerStatuses: []corev1.ContainerStatus{ { Name: strings.Split(name, "-")[1], Ready: ready, }, }, }, } } t.Run("Returns an error if not all pods are running", func(t *testing.T) { pods := []corev1.Pod{ pod("linkerd-controller-6f78cbd47-bc557", corev1.PodRunning, true), pod("linkerd-grafana-5b7d796646-hh46d", corev1.PodRunning, true), pod("linkerd-identity-6849948664-27982", corev1.PodRunning, true), pod("linkerd-prometheus-74d6879cd6-bbdk6", corev1.PodFailed, false), pod("linkerd-tap-6c878df6c8-2hmtd", corev1.PodRunning, true), pod("linkerd-sp-validator-24d2879ce6-cddk9", corev1.PodRunning, true), pod("linkerd-web-98c9ddbcd-7b5lh", corev1.PodRunning, true), } err := validateControlPlanePods(pods) if err == nil { t.Fatal("Expected error, got nothing") } if err.Error() != "No running pods for \"linkerd-prometheus\"" { t.Fatalf("Unexpected error message: %s", err.Error()) } }) t.Run("Returns an error if not all containers are ready", func(t *testing.T) { pods := []corev1.Pod{ pod("linkerd-controller-6f78cbd47-bc557", corev1.PodRunning, true), pod("linkerd-grafana-5b7d796646-hh46d", corev1.PodRunning, false), pod("linkerd-identity-6849948664-27982", corev1.PodRunning, true), pod("linkerd-prometheus-74d6879cd6-bbdk6", corev1.PodRunning, true), pod("linkerd-tap-6c878df6c8-2hmtd", corev1.PodRunning, true), pod("linkerd-sp-validator-24d2879ce6-cddk9", corev1.PodRunning, true), pod("linkerd-web-98c9ddbcd-7b5lh", corev1.PodRunning, true), } err := validateControlPlanePods(pods) if err == nil { t.Fatal("Expected error, got nothing") } if err.Error() != "pod/linkerd-grafana-5b7d796646-hh46d container grafana is not ready" { t.Fatalf("Unexpected error message: %s", err.Error()) } }) t.Run("Returns nil if all pods are running and all containers are ready", func(t *testing.T) { pods := []corev1.Pod{ pod("linkerd-controller-6f78cbd47-bc557", corev1.PodRunning, true), pod("linkerd-grafana-5b7d796646-hh46d", corev1.PodRunning, true), pod("linkerd-identity-6849948664-27982", corev1.PodRunning, true), pod("linkerd-prometheus-74d6879cd6-bbdk6", corev1.PodRunning, true), pod("linkerd-sp-validator-24d2879ce6-cddk9", corev1.PodRunning, true), pod("linkerd-tap-6c878df6c8-2hmtd", corev1.PodRunning, true), pod("linkerd-web-98c9ddbcd-7b5lh", corev1.PodRunning, true), } err := validateControlPlanePods(pods) if err != nil { t.Fatalf("Unexpected error: %s", err) } }) t.Run("Returns nil if, HA mode, at least one pod of each control plane component is ready", func(t *testing.T) { pods := []corev1.Pod{ pod("linkerd-controller-6f78cbd47-bc557", corev1.PodRunning, true), pod("linkerd-controller-6f78cbd47-bc558", corev1.PodRunning, false), pod("linkerd-controller-6f78cbd47-bc559", corev1.PodFailed, false), pod("linkerd-grafana-5b7d796646-hh46d", corev1.PodRunning, true), pod("linkerd-identity-6849948664-27982", corev1.PodRunning, true), pod("linkerd-identity-6849948664-27983", corev1.PodRunning, false), pod("linkerd-identity-6849948664-27984", corev1.PodFailed, false), pod("linkerd-tap-6c878df6c8-2hmtd", corev1.PodRunning, true), pod("linkerd-prometheus-74d6879cd6-bbdk6", corev1.PodRunning, true), pod("linkerd-sp-validator-24d2879ce6-cddk9", corev1.PodRunning, true), pod("linkerd-web-98c9ddbcd-7b5lh", corev1.PodRunning, true), } err := validateControlPlanePods(pods) if err != nil { t.Fatalf("Unexpected error: %s", err) } }) t.Run("Returns nil if all linkerd pods are running and pod list includes non-linkerd pod", func(t *testing.T) { pods := []corev1.Pod{ pod("linkerd-controller-6f78cbd47-bc557", corev1.PodRunning, true), pod("linkerd-grafana-5b7d796646-hh46d", corev1.PodRunning, true), pod("linkerd-identity-6849948664-27982", corev1.PodRunning, true), pod("linkerd-prometheus-74d6879cd6-bbdk6", corev1.PodRunning, true), pod("linkerd-sp-validator-24d2879ce6-cddk9", corev1.PodRunning, true), pod("linkerd-tap-6c878df6c8-2hmtd", corev1.PodRunning, true), pod("linkerd-web-98c9ddbcd-7b5lh", corev1.PodRunning, true), pod("hello-43c25d", corev1.PodRunning, true), } err := validateControlPlanePods(pods) if err != nil { t.Fatalf("Unexpected error message: %s", err.Error()) } }) } func TestValidateDataPlaneNamespace(t *testing.T) { testCases := []struct { ns string result string }{ { "", "data-plane-ns-test-cat data plane namespace exists", }, { "bad-ns", "data-plane-ns-test-cat data plane namespace exists: The \"bad-ns\" namespace does not exist", }, } for i, tc := range testCases { tc := tc // pin t.Run(fmt.Sprintf("%d/%s", i, tc.ns), func(t *testing.T) { hc := NewHealthChecker( []CategoryID{}, &Options{ DataPlaneNamespace: tc.ns, }, ) var err error hc.kubeAPI, err = k8s.NewFakeAPI() if err != nil { t.Fatalf("Unexpected error: %s", err) } // create a synethic category that only includes the "data plane namespace exists" check hc.addCheckAsCategory("data-plane-ns-test-cat", LinkerdDataPlaneChecks, "data plane namespace exists") expectedResults := []string{ tc.result, } obs := newObserver() hc.RunChecks(obs.resultFn) if !reflect.DeepEqual(obs.results, expectedResults) { t.Fatalf("Expected results %v, but got %v", expectedResults, obs.results) } }) } } func TestValidateDataPlanePods(t *testing.T) { t.Run("Returns an error if no inject pods were found", func(t *testing.T) { err := validateDataPlanePods([]*pb.Pod{}, "emojivoto") if err == nil { t.Fatal("Expected error, got nothing") } if err.Error() != "No \"linkerd-proxy\" containers found in the \"emojivoto\" namespace" { t.Fatalf("Unexpected error message: %s", err.Error()) } }) t.Run("Returns an error if not all pods are running", func(t *testing.T) { pods := []*pb.Pod{ {Name: "emoji-d9c7866bb-7v74n", Status: "Running", ProxyReady: true}, {Name: "vote-bot-644b8cb6b4-g8nlr", Status: "Running", ProxyReady: true}, {Name: "voting-65b9fffd77-rlwsd", Status: "Failed", ProxyReady: false}, {Name: "web-6cfbccc48-5g8px", Status: "Running", ProxyReady: true}, } err := validateDataPlanePods(pods, "emojivoto") if err == nil { t.Fatal("Expected error, got nothing") } if err.Error() != "The \"voting-65b9fffd77-rlwsd\" pod is not running" { t.Fatalf("Unexpected error message: %s", err.Error()) } }) t.Run("Returns an error if the proxy container is not ready", func(t *testing.T) { pods := []*pb.Pod{ {Name: "emoji-d9c7866bb-7v74n", Status: "Running", ProxyReady: true}, {Name: "vote-bot-644b8cb6b4-g8nlr", Status: "Running", ProxyReady: false}, {Name: "voting-65b9fffd77-rlwsd", Status: "Running", ProxyReady: true}, {Name: "web-6cfbccc48-5g8px", Status: "Running", ProxyReady: true}, } err := validateDataPlanePods(pods, "emojivoto") if err == nil { t.Fatal("Expected error, got nothing") } if err.Error() != "The \"linkerd-proxy\" container in the \"vote-bot-644b8cb6b4-g8nlr\" pod is not ready" { t.Fatalf("Unexpected error message: %s", err.Error()) } }) t.Run("Returns nil if all pods are running and all proxy containers are ready", func(t *testing.T) { pods := []*pb.Pod{ {Name: "emoji-d9c7866bb-7v74n", Status: "Running", ProxyReady: true}, {Name: "vote-bot-644b8cb6b4-g8nlr", Status: "Running", ProxyReady: true}, {Name: "voting-65b9fffd77-rlwsd", Status: "Running", ProxyReady: true}, {Name: "web-6cfbccc48-5g8px", Status: "Running", ProxyReady: true}, } err := validateDataPlanePods(pods, "emojivoto") if err != nil { t.Fatalf("Unexpected error: %s", err) } }) } func TestValidateDataPlanePodReporting(t *testing.T) { t.Run("Returns success if no pods present", func(t *testing.T) { err := validateDataPlanePodReporting([]*pb.Pod{}) if err != nil { t.Fatalf("Unexpected error message: %s", err.Error()) } }) t.Run("Returns success if all pods are added", func(t *testing.T) { pods := []*pb.Pod{ {Name: "ns1/test1", Added: true}, {Name: "ns2/test2", Added: true}, } err := validateDataPlanePodReporting(pods) if err != nil { t.Fatalf("Unexpected error message: %s", err.Error()) } }) t.Run("Returns an error if any of the pod was not added to Prometheus", func(t *testing.T) { pods := []*pb.Pod{ {Name: "ns1/test1", Added: true}, {Name: "ns2/test2", Added: false}, } err := validateDataPlanePodReporting(pods) if err == nil { t.Fatal("Expected error, got nothing") } if err.Error() != "Data plane metrics not found for ns2/test2." { t.Fatalf("Unexpected error message: %s", err.Error()) } }) } func TestLinkerdPreInstallGlobalResourcesChecks(t *testing.T) { hc := NewHealthChecker( []CategoryID{LinkerdPreInstallGlobalResourcesChecks}, &Options{}) t.Run("global resources don't exist", func(t *testing.T) { var err error hc.kubeAPI, err = k8s.NewFakeAPI() if err != nil { t.Fatalf("Unexpected error: %s", err) } observer := newObserver() if !hc.RunChecks(observer.resultFn) { t.Errorf("Expect RunChecks to return true") } expected := []string{ "pre-linkerd-global-resources no ClusterRoles exist", "pre-linkerd-global-resources no ClusterRoleBindings exist", "pre-linkerd-global-resources no CustomResourceDefinitions exist", "pre-linkerd-global-resources no MutatingWebhookConfigurations exist", "pre-linkerd-global-resources no ValidatingWebhookConfigurations exist", "pre-linkerd-global-resources no PodSecurityPolicies exist", } if !reflect.DeepEqual(observer.results, expected) { t.Errorf("Mismatch result.\nExpected: %v\n Actual: %v\n", expected, observer.results) } }) t.Run("global resources exist", func(t *testing.T) { resources := []string{ `apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cluster-role labels: linkerd.io/control-plane-ns: test-ns`, `apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: cluster-role-binding labels: linkerd.io/control-plane-ns: test-ns`, `apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: custom-resource-definition labels: linkerd.io/control-plane-ns: test-ns`, `apiVersion: admissionregistration.k8s.io/v1beta1 kind: MutatingWebhookConfiguration metadata: name: mutating-webhook-configuration labels: linkerd.io/control-plane-ns: test-ns`, `apiVersion: admissionregistration.k8s.io/v1beta1 kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration labels: linkerd.io/control-plane-ns: test-ns`, `apiVersion: policy/v1beta1 kind: PodSecurityPolicy metadata: name: pod-security-policy labels: linkerd.io/control-plane-ns: test-ns`, } var err error hc.kubeAPI, err = k8s.NewFakeAPI(resources...) hc.ControlPlaneNamespace = "test-ns" if err != nil { t.Fatalf("Unexpected error: %s", err) } observer := newObserver() if hc.RunChecks(observer.resultFn) { t.Errorf("Expect RunChecks to return false") } expected := []string{ "pre-linkerd-global-resources no ClusterRoles exist: ClusterRoles found but should not exist: cluster-role", "pre-linkerd-global-resources no ClusterRoleBindings exist: ClusterRoleBindings found but should not exist: cluster-role-binding", "pre-linkerd-global-resources no CustomResourceDefinitions exist: CustomResourceDefinitions found but should not exist: custom-resource-definition", "pre-linkerd-global-resources no MutatingWebhookConfigurations exist: MutatingWebhookConfigurations found but should not exist: mutating-webhook-configuration", "pre-linkerd-global-resources no ValidatingWebhookConfigurations exist: ValidatingWebhookConfigurations found but should not exist: validating-webhook-configuration", "pre-linkerd-global-resources no PodSecurityPolicies exist: PodSecurityPolicies found but should not exist: pod-security-policy", } if !reflect.DeepEqual(observer.results, expected) { t.Errorf("Mismatch result.\nExpected: %v\n Actual: %v\n", expected, observer.results) } }) } func getConfigAndKubeSystemNamespace(ha bool, nsLabel string) []string { return []string{fmt.Sprintf(` kind: ConfigMap apiVersion: v1 metadata: name: linkerd-config namespace: linkerd data: install: | {"cliVersion":"dev-undefined","flags":[{"name":"ha","value":"%v"}]}`, ha), fmt.Sprintf(` apiVersion: v1 kind: Namespace metadata: creationTimestamp: null labels: %s name: kube-system`, nsLabel), } } func TestKubeSystemNamespaceInHA(t *testing.T) { testCases := []struct { testDescription string k8sConfigs []string expectedOutput string }{ { "passes when HA is not enabled", getConfigAndKubeSystemNamespace(false, ""), "", }, { "passes when HA is enabled and namespace has required metadata", getConfigAndKubeSystemNamespace(true, "config.linkerd.io/admission-webhooks: disabled"), "l5d-injection-disabled pod injection disabled on kube-system", }, { "fails when HA and admission hooks are enabled", getConfigAndKubeSystemNamespace(true, "config.linkerd.io/admission-webhooks: enabled"), "l5d-injection-disabled pod injection disabled on kube-system: kube-system namespace needs to have the label config.linkerd.io/admission-webhooks: disabled if HA mode is enabled", }, { "fails when HA is enabled and metadata is missing", getConfigAndKubeSystemNamespace(true, ""), "l5d-injection-disabled pod injection disabled on kube-system: kube-system namespace needs to have the label config.linkerd.io/admission-webhooks: disabled if HA mode is enabled", }, } for _, tc := range testCases { tc := tc // pin t.Run(tc.testDescription, func(t *testing.T) { hc := NewHealthChecker([]CategoryID{}, &Options{}) hc.ControlPlaneNamespace = "linkerd" var err error hc.kubeAPI, _ = k8s.NewFakeAPI(tc.k8sConfigs...) _, hc.linkerdConfig, err = hc.checkLinkerdConfigConfigMap() if err != nil { t.Fatalf("Unexpected error: %q", err) } hc.addCheckAsCategory("l5d-injection-disabled", LinkerdHAChecks, "pod injection disabled on kube-system") obs := newObserver() hc.RunChecks(obs.resultFn) if tc.expectedOutput == "" { if len(obs.results) != 0 { t.Fatalf("Expected not output, but got %v", obs.results) } } else { expectedResults := []string{ tc.expectedOutput, } if !reflect.DeepEqual(obs.results, expectedResults) { t.Fatalf("Expected results %v, but got %v", expectedResults, obs.results) } } }) } } func TestFetchLinkerdConfigMap(t *testing.T) { testCases := []struct { k8sConfigs []string expected *configPb.All err error }{ { []string{` kind: ConfigMap apiVersion: v1 metadata: name: linkerd-config namespace: linkerd data: global: | {"linkerdNamespace":"linkerd","cniEnabled":false,"version":"install-control-plane-version","identityContext":{"trustDomain":"cluster.local","trustAnchorsPem":"fake-trust-anchors-pem","issuanceLifetime":"86400s","clockSkewAllowance":"20s"}} proxy: | {"proxyImage":{"imageName":"gcr.io/linkerd-io/proxy","pullPolicy":"IfNotPresent"},"proxyInitImage":{"imageName":"gcr.io/linkerd-io/proxy-init","pullPolicy":"IfNotPresent"},"controlPort":{"port":4190},"ignoreInboundPorts":[],"ignoreOutboundPorts":[],"inboundPort":{"port":4143},"adminPort":{"port":4191},"outboundPort":{"port":4140},"resource":{"requestCpu":"","requestMemory":"","limitCpu":"","limitMemory":""},"proxyUid":"2102","logLevel":{"level":"warn,linkerd=info"},"disableExternalProfiles":true,"proxyVersion":"install-proxy-version","proxy_init_image_version":"v1.3.2","debugImage":{"imageName":"gcr.io/linkerd-io/debug","pullPolicy":"IfNotPresent"},"debugImageVersion":"install-debug-version"} install: | {"cliVersion":"dev-undefined","flags":[]}`, }, &configPb.All{ Global: &configPb.Global{ LinkerdNamespace: "linkerd", Version: "install-control-plane-version", IdentityContext: &configPb.IdentityContext{ TrustDomain: "cluster.local", TrustAnchorsPem: "fake-trust-anchors-pem", IssuanceLifetime: &duration.Duration{ Seconds: 86400, }, ClockSkewAllowance: &duration.Duration{ Seconds: 20, }, }, }, Proxy: &configPb.Proxy{ ProxyImage: &configPb.Image{ ImageName: "gcr.io/linkerd-io/proxy", PullPolicy: "IfNotPresent", }, ProxyInitImage: &configPb.Image{ ImageName: "gcr.io/linkerd-io/proxy-init", PullPolicy: "IfNotPresent", }, ControlPort: &configPb.Port{ Port: 4190, }, InboundPort: &configPb.Port{ Port: 4143, }, AdminPort: &configPb.Port{ Port: 4191, }, OutboundPort: &configPb.Port{ Port: 4140, }, Resource: &configPb.ResourceRequirements{}, ProxyUid: 2102, LogLevel: &configPb.LogLevel{ Level: "warn,linkerd=info", }, DisableExternalProfiles: true, ProxyVersion: "install-proxy-version", ProxyInitImageVersion: "v1.3.2", DebugImage: &configPb.Image{ ImageName: "gcr.io/linkerd-io/debug", PullPolicy: "IfNotPresent", }, DebugImageVersion: "install-debug-version", }, Install: &configPb.Install{ CliVersion: "dev-undefined", }}, nil, }, { []string{` kind: ConfigMap apiVersion: v1 metadata: name: linkerd-config namespace: linkerd data: global: | {"linkerdNamespace":"ns","identityContext":null} proxy: "{}" install: "{}"`, }, &configPb.All{Global: &configPb.Global{LinkerdNamespace: "ns", IdentityContext: nil}, Proxy: &configPb.Proxy{}, Install: &configPb.Install{}}, nil, }, { []string{` kind: ConfigMap apiVersion: v1 metadata: name: linkerd-config namespace: linkerd data: global: "{}" proxy: "{}" install: "{}"`, }, &configPb.All{Global: &configPb.Global{}, Proxy: &configPb.Proxy{}, Install: &configPb.Install{}}, nil, }, { nil, nil, k8sErrors.NewNotFound(schema.GroupResource{Resource: "configmaps"}, "linkerd-config"), }, } for i, tc := range testCases { tc := tc // pin t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { clientset, err := k8s.NewFakeAPI(tc.k8sConfigs...) if err != nil { t.Fatalf("Unexpected error: %s", err) } _, configs, err := FetchLinkerdConfigMap(clientset, "linkerd") if !reflect.DeepEqual(err, tc.err) { t.Fatalf("Expected \"%+v\", got \"%+v\"", tc.err, err) } if !proto.Equal(configs, tc.expected) { t.Fatalf("Unexpected config:\nExpected:\n%+v\nGot:\n%+v", tc.expected, configs) } }) } } func getFakeConfigMap(scheme string, issuerCerts *issuercerts.IssuerCertData) string { anchors, _ := json.Marshal(issuerCerts.TrustAnchors) return fmt.Sprintf(` kind: ConfigMap apiVersion: v1 metadata: name: linkerd-config namespace: linkerd data: global: | {"linkerdNamespace": "linkerd", "identityContext":{"trustAnchorsPem": %s, "trustDomain": "cluster.local", "scheme": "%s"}} --- `, anchors, scheme) } func getFakeSecret(scheme string, issuerCerts *issuercerts.IssuerCertData) string { if scheme == k8s.IdentityIssuerSchemeLinkerd { return fmt.Sprintf(` kind: Secret apiVersion: v1 metadata: name: linkerd-identity-issuer namespace: linkerd data: crt.pem: %s key.pem: %s --- `, base64.StdEncoding.EncodeToString([]byte(issuerCerts.IssuerCrt)), base64.StdEncoding.EncodeToString([]byte(issuerCerts.IssuerKey))) } return fmt.Sprintf( ` kind: Secret apiVersion: v1 metadata: name: linkerd-identity-issuer namespace: linkerd data: ca.crt: %s tls.crt: %s tls.key: %s --- `, base64.StdEncoding.EncodeToString([]byte(issuerCerts.TrustAnchors)), base64.StdEncoding.EncodeToString([]byte(issuerCerts.IssuerCrt)), base64.StdEncoding.EncodeToString([]byte(issuerCerts.IssuerKey))) } func createIssuerData(dnsName string, notBefore, notAfter time.Time) *issuercerts.IssuerCertData { // Generate a new root key. key, _ := tls.GenerateKey() rootCa, _ := tls.CreateRootCA(dnsName, key, tls.Validity{ Lifetime: notAfter.Sub(notBefore), ValidFrom: ¬Before, }) return &issuercerts.IssuerCertData{ TrustAnchors: rootCa.Cred.Crt.EncodeCertificatePEM(), IssuerCrt: rootCa.Cred.Crt.EncodeCertificatePEM(), IssuerKey: rootCa.Cred.EncodePrivateKeyPEM(), } } type lifeSpan struct { starts time.Time ends time.Time } func runIdentityCheckTestCase(t *testing.T, testID int, testDescription string, checkerToTest string, fakeConfigMap string, fakeSecret string, expectedOutput []string) { t.Run(fmt.Sprintf("%d/%s", testID, testDescription), func(t *testing.T) { hc := NewHealthChecker( []CategoryID{}, &Options{ DataPlaneNamespace: "linkerd", }, ) hc.addCheckAsCategory("linkerd-identity-test-cat", LinkerdIdentity, checkerToTest) var err error hc.ControlPlaneNamespace = "linkerd" hc.kubeAPI, err = k8s.NewFakeAPI(fakeConfigMap, fakeSecret) _, hc.linkerdConfig, _ = hc.checkLinkerdConfigConfigMap() if testDescription != "certificate config is valid" { hc.issuerCert, hc.trustAnchors, _ = hc.checkCertificatesConfig() } if err != nil { t.Fatalf("Unexpected error: %s", err) } obs := newObserver() hc.RunChecks(obs.resultFn) if !reflect.DeepEqual(obs.results, expectedOutput) { t.Fatalf("Expected results %v, but got %v", expectedOutput, obs.results) } }) } func TestLinkerdIdentityCheckCertConfig(t *testing.T) { var testCases = []struct { checkDescription string tlsSecretScheme string schemeInConfig string expectedOutput []string configMapIssuerDataModifier func(issuercerts.IssuerCertData) issuercerts.IssuerCertData tlsSecretIssuerDataModifier func(issuercerts.IssuerCertData) issuercerts.IssuerCertData }{ { checkDescription: "works with valid cert and linkerd.io/tls secret", tlsSecretScheme: k8s.IdentityIssuerSchemeLinkerd, schemeInConfig: k8s.IdentityIssuerSchemeLinkerd, expectedOutput: []string{"linkerd-identity-test-cat certificate config is valid"}, }, { checkDescription: "works with valid cert and kubernetes.io/tls secret", tlsSecretScheme: string(corev1.SecretTypeTLS), schemeInConfig: string(corev1.SecretTypeTLS), expectedOutput: []string{"linkerd-identity-test-cat certificate config is valid"}, }, { checkDescription: "works if config scheme is empty and secret scheme is linkerd.io/tls (pre 2.7)", tlsSecretScheme: k8s.IdentityIssuerSchemeLinkerd, schemeInConfig: "", expectedOutput: []string{"linkerd-identity-test-cat certificate config is valid"}, }, { checkDescription: "fails if config scheme is empty and secret scheme is kubernetes.io/tls (pre 2.7)", tlsSecretScheme: string(corev1.SecretTypeTLS), schemeInConfig: "", expectedOutput: []string{"linkerd-identity-test-cat certificate config is valid: key crt.pem containing the issuer certificate needs to exist in secret linkerd-identity-issuer if --identity-external-issuer=false"}, }, { checkDescription: "fails when config scheme is linkerd.io/tls but secret scheme is kubernetes.io/tls in config is different than the one in the issuer secret", tlsSecretScheme: string(corev1.SecretTypeTLS), schemeInConfig: k8s.IdentityIssuerSchemeLinkerd, expectedOutput: []string{"linkerd-identity-test-cat certificate config is valid: key crt.pem containing the issuer certificate needs to exist in secret linkerd-identity-issuer if --identity-external-issuer=false"}, }, { checkDescription: "fails when config scheme is kubernetes.io/tls but secret scheme is linkerd.io/tls in config is different than the one in the issuer secret", tlsSecretScheme: k8s.IdentityIssuerSchemeLinkerd, schemeInConfig: string(corev1.SecretTypeTLS), expectedOutput: []string{"linkerd-identity-test-cat certificate config is valid: key ca.crt containing the trust anchors needs to exist in secret linkerd-identity-issuer if --identity-external-issuer=true"}, }, { checkDescription: "does not get influenced by newline differences between trust anchors (missing newline in configMap)", tlsSecretScheme: string(corev1.SecretTypeTLS), schemeInConfig: string(corev1.SecretTypeTLS), expectedOutput: []string{"linkerd-identity-test-cat certificate config is valid"}, configMapIssuerDataModifier: func(issuerData issuercerts.IssuerCertData) issuercerts.IssuerCertData { issuerData.TrustAnchors = strings.TrimSpace(issuerData.TrustAnchors) return issuerData }, }, { checkDescription: "does not get influenced by newline differences between trust anchors (extra newline in configMap)", tlsSecretScheme: string(corev1.SecretTypeTLS), schemeInConfig: string(corev1.SecretTypeTLS), expectedOutput: []string{"linkerd-identity-test-cat certificate config is valid"}, configMapIssuerDataModifier: func(issuerData issuercerts.IssuerCertData) issuercerts.IssuerCertData { issuerData.TrustAnchors = issuerData.TrustAnchors + "\n" return issuerData }, }, { checkDescription: "does not get influenced by newline differences between trust anchors (missing newline in secret)", tlsSecretScheme: string(corev1.SecretTypeTLS), schemeInConfig: string(corev1.SecretTypeTLS), expectedOutput: []string{"linkerd-identity-test-cat certificate config is valid"}, tlsSecretIssuerDataModifier: func(issuerData issuercerts.IssuerCertData) issuercerts.IssuerCertData { issuerData.TrustAnchors = strings.TrimSpace(issuerData.TrustAnchors) return issuerData }, }, { checkDescription: "fails when trying to parse trust anchors from secret (extra newline in secret)", tlsSecretScheme: string(corev1.SecretTypeTLS), schemeInConfig: string(corev1.SecretTypeTLS), expectedOutput: []string{"linkerd-identity-test-cat certificate config is valid: not a PEM certificate"}, tlsSecretIssuerDataModifier: func(issuerData issuercerts.IssuerCertData) issuercerts.IssuerCertData { issuerData.TrustAnchors = issuerData.TrustAnchors + "\n" return issuerData }, }, } for id, testCase := range testCases { testCase := testCase issuerData := createIssuerData("identity.linkerd.cluster.local", time.Now().AddDate(-1, 0, 0), time.Now().AddDate(1, 0, 0)) var fakeConfigMap string if testCase.configMapIssuerDataModifier != nil { modifiedIssuerData := testCase.configMapIssuerDataModifier(*issuerData) fakeConfigMap = getFakeConfigMap(testCase.schemeInConfig, &modifiedIssuerData) } else { fakeConfigMap = getFakeConfigMap(testCase.schemeInConfig, issuerData) } var fakeSecret string if testCase.tlsSecretIssuerDataModifier != nil { modifiedIssuerData := testCase.tlsSecretIssuerDataModifier(*issuerData) fakeSecret = getFakeSecret(testCase.tlsSecretScheme, &modifiedIssuerData) } else { fakeSecret = getFakeSecret(testCase.tlsSecretScheme, issuerData) } runIdentityCheckTestCase(t, id, testCase.checkDescription, "certificate config is valid", fakeConfigMap, fakeSecret, testCase.expectedOutput) } } func TestLinkerdIdentityCheckCertValidity(t *testing.T) { var testCases = []struct { checkDescription string checkerToTest string lifespan *lifeSpan expectedOutput []string }{ { checkerToTest: "trust anchors are within their validity period", checkDescription: "fails when the only anchor is not valid yet", lifespan: &lifeSpan{ starts: time.Date(2100, 1, 1, 1, 1, 1, 1, time.UTC), ends: time.Date(2101, 1, 1, 1, 1, 1, 1, time.UTC), }, expectedOutput: []string{"linkerd-identity-test-cat trust anchors are within their validity period: Invalid anchors:\n\t* 1 identity.linkerd.cluster.local not valid before: 2100-01-01T01:00:51Z"}, }, { checkerToTest: "trust anchors are within their validity period", checkDescription: "fails when the only trust anchor is expired", lifespan: &lifeSpan{ starts: time.Date(1989, 1, 1, 1, 1, 1, 1, time.UTC), ends: time.Date(1990, 1, 1, 1, 1, 1, 1, time.UTC), }, expectedOutput: []string{"linkerd-identity-test-cat trust anchors are within their validity period: Invalid anchors:\n\t* 1 identity.linkerd.cluster.local not valid anymore. Expired on 1990-01-01T01:01:11Z"}, }, { checkerToTest: "issuer cert is within its validity period", checkDescription: "fails when the issuer cert is not valid yet", lifespan: &lifeSpan{ starts: time.Date(2100, 1, 1, 1, 1, 1, 1, time.UTC), ends: time.Date(2101, 1, 1, 1, 1, 1, 1, time.UTC), }, expectedOutput: []string{"linkerd-identity-test-cat issuer cert is within its validity period: issuer certificate is not valid before: 2100-01-01T01:00:51Z"}, }, { checkerToTest: "issuer cert is within its validity period", checkDescription: "fails when the issuer cert is expired", lifespan: &lifeSpan{ starts: time.Date(1989, 1, 1, 1, 1, 1, 1, time.UTC), ends: time.Date(1990, 1, 1, 1, 1, 1, 1, time.UTC), }, expectedOutput: []string{"linkerd-identity-test-cat issuer cert is within its validity period: issuer certificate is not valid anymore. Expired on 1990-01-01T01:01:11Z"}, }, } for id, testCase := range testCases { testCase := testCase issuerData := createIssuerData("identity.linkerd.cluster.local", testCase.lifespan.starts, testCase.lifespan.ends) fakeConfigMap := getFakeConfigMap(k8s.IdentityIssuerSchemeLinkerd, issuerData) fakeSecret := getFakeSecret(k8s.IdentityIssuerSchemeLinkerd, issuerData) runIdentityCheckTestCase(t, id, testCase.checkDescription, testCase.checkerToTest, fakeConfigMap, fakeSecret, testCase.expectedOutput) } } func TestLinkerdIdentityCheckWrongDns(t *testing.T) { expectedOutput := []string{"linkerd-identity-test-cat issuer cert is issued by the trust anchor: x509: certificate is valid for wrong.linkerd.cluster.local, not identity.linkerd.cluster.local"} issuerData := createIssuerData("wrong.linkerd.cluster.local", time.Now().AddDate(-1, 0, 0), time.Now().AddDate(1, 0, 0)) fakeConfigMap := getFakeConfigMap(k8s.IdentityIssuerSchemeLinkerd, issuerData) fakeSecret := getFakeSecret(k8s.IdentityIssuerSchemeLinkerd, issuerData) runIdentityCheckTestCase(t, 0, "fails when cert dns is wrong", "issuer cert is issued by the trust anchor", fakeConfigMap, fakeSecret, expectedOutput) } type fakeCniResourcesOpts struct { hasConfigMap bool hasPodSecurityPolicy bool hasClusterRole bool hasClusterRoleBinding bool hasRole bool hasRoleBinding bool hasServiceAccount bool hasDaemonSet bool scheduled int ready int } func getFakeCniResources(opts fakeCniResourcesOpts) []string { var resources []string if opts.hasConfigMap { resources = append(resources, ` kind: ConfigMap apiVersion: v1 metadata: name: linkerd-cni-config namespace: test-ns labels: linkerd.io/cni-resource: "true" data: dest_cni_net_dir: "/etc/cni/net.d" --- `) } if opts.hasPodSecurityPolicy { resources = append(resources, ` apiVersion: policy/v1beta1 kind: PodSecurityPolicy metadata: name: linkerd-test-ns-cni labels: linkerd.io/cni-resource: "true" spec: allowPrivilegeEscalation: false fsGroup: rule: RunAsAny hostNetwork: true runAsUser: rule: RunAsAny seLinux: rule: RunAsAny supplementalGroups: rule: RunAsAny volumes: - hostPath - secret --- `) } if opts.hasClusterRole { resources = append(resources, ` kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: linkerd-cni labels: linkerd.io/cni-resource: "true" rules: - apiGroups: [""] resources: ["pods", "nodes", "namespaces"] verbs: ["list", "get", "watch"] --- `) } if opts.hasClusterRoleBinding { resources = append(resources, ` apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: linkerd-cni labels: linkerd.io/cni-resource: "true" roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: linkerd-cni subjects: - kind: ServiceAccount name: linkerd-cni namespace: test-ns --- `) } if opts.hasRole { resources = append(resources, ` apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: linkerd-cni namespace: test-ns labels: linkerd.io/cni-resource: "true" rules: - apiGroups: ['extensions', 'policy'] resources: ['podsecuritypolicies'] resourceNames: - linkerd-test-ns-cni verbs: ['use'] --- `) } if opts.hasRoleBinding { resources = append(resources, ` apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: linkerd-cni namespace: test-ns labels: linkerd.io/cni-resource: "true" roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: linkerd-cni subjects: - kind: ServiceAccount name: linkerd-cni namespace: test-ns --- `) } if opts.hasServiceAccount { resources = append(resources, ` apiVersion: v1 kind: ServiceAccount metadata: name: linkerd-cni namespace: test-ns labels: linkerd.io/cni-resource: "true" --- `) } if opts.hasDaemonSet { resources = append(resources, fmt.Sprintf(` kind: DaemonSet apiVersion: apps/v1 metadata: name: linkerd-cni namespace: test-ns labels: k8s-app: linkerd-cni linkerd.io/cni-resource: "true" annotations: linkerd.io/created-by: linkerd/cli git-b4266c93 spec: selector: matchLabels: k8s-app: linkerd-cni updateStrategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 template: metadata: labels: k8s-app: linkerd-cni annotations: linkerd.io/created-by: linkerd/cli git-b4266c93 spec: nodeSelector: beta.kubernetes.io/os: linux hostNetwork: true serviceAccountName: linkerd-cni containers: - name: install-cni image: gcr.io/linkerd-io/cni-plugin:git-b4266c93 env: - name: DEST_CNI_NET_DIR valueFrom: configMapKeyRef: name: linkerd-cni-config key: dest_cni_net_dir - name: DEST_CNI_BIN_DIR valueFrom: configMapKeyRef: name: linkerd-cni-config key: dest_cni_bin_dir - name: CNI_NETWORK_CONFIG valueFrom: configMapKeyRef: name: linkerd-cni-config key: cni_network_config - name: SLEEP value: "true" lifecycle: preStop: exec: command: ["kill","-15","1"] volumeMounts: - mountPath: /host/opt/cni/bin name: cni-bin-dir - mountPath: /host/etc/cni/net.d name: cni-net-dir volumes: - name: cni-bin-dir hostPath: path: /opt/cni/bin - name: cni-net-dir hostPath: path: /etc/cni/net.d status: desiredNumberScheduled: %d numberReady: %d --- `, opts.scheduled, opts.ready)) } return resources } func TestCniChecks(t *testing.T) { testCases := []struct { description string testCaseOpts fakeCniResourcesOpts results []string }{ { "fails when there is no config map", fakeCniResourcesOpts{}, []string{"linkerd-cni-plugin cni plugin ConfigMap exists: configmaps \"linkerd-cni-config\" not found"}, }, { "fails when there is no pod security policy", fakeCniResourcesOpts{hasConfigMap: true}, []string{ "linkerd-cni-plugin cni plugin ConfigMap exists", "linkerd-cni-plugin cni plugin PodSecurityPolicy exists: missing PodSecurityPolicy: linkerd-test-ns-cni"}, }, { "fails then there is no ClusterRole", fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true}, []string{ "linkerd-cni-plugin cni plugin ConfigMap exists", "linkerd-cni-plugin cni plugin PodSecurityPolicy exists", "linkerd-cni-plugin cni plugin ClusterRole exists: missing ClusterRole: linkerd-cni"}, }, { "fails then there is no ClusterRoleBinding", fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true}, []string{ "linkerd-cni-plugin cni plugin ConfigMap exists", "linkerd-cni-plugin cni plugin PodSecurityPolicy exists", "linkerd-cni-plugin cni plugin ClusterRole exists", "linkerd-cni-plugin cni plugin ClusterRoleBinding exists: missing ClusterRoleBinding: linkerd-cni"}, }, { "fails then there is no Role", fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true, hasClusterRoleBinding: true}, []string{ "linkerd-cni-plugin cni plugin ConfigMap exists", "linkerd-cni-plugin cni plugin PodSecurityPolicy exists", "linkerd-cni-plugin cni plugin ClusterRole exists", "linkerd-cni-plugin cni plugin ClusterRoleBinding exists", "linkerd-cni-plugin cni plugin Role exists: missing Role: linkerd-cni"}, }, { "fails then there is no RoleBinding", fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true, hasClusterRoleBinding: true, hasRole: true}, []string{ "linkerd-cni-plugin cni plugin ConfigMap exists", "linkerd-cni-plugin cni plugin PodSecurityPolicy exists", "linkerd-cni-plugin cni plugin ClusterRole exists", "linkerd-cni-plugin cni plugin ClusterRoleBinding exists", "linkerd-cni-plugin cni plugin Role exists", "linkerd-cni-plugin cni plugin RoleBinding exists: missing RoleBinding: linkerd-cni"}, }, { "fails then there is no ServiceAccount", fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true, hasClusterRoleBinding: true, hasRole: true, hasRoleBinding: true}, []string{ "linkerd-cni-plugin cni plugin ConfigMap exists", "linkerd-cni-plugin cni plugin PodSecurityPolicy exists", "linkerd-cni-plugin cni plugin ClusterRole exists", "linkerd-cni-plugin cni plugin ClusterRoleBinding exists", "linkerd-cni-plugin cni plugin Role exists", "linkerd-cni-plugin cni plugin RoleBinding exists", "linkerd-cni-plugin cni plugin ServiceAccount exists: missing ServiceAccount: linkerd-cni", }, }, { "fails then there is no DaemonSet", fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true, hasClusterRoleBinding: true, hasRole: true, hasRoleBinding: true, hasServiceAccount: true}, []string{ "linkerd-cni-plugin cni plugin ConfigMap exists", "linkerd-cni-plugin cni plugin PodSecurityPolicy exists", "linkerd-cni-plugin cni plugin ClusterRole exists", "linkerd-cni-plugin cni plugin ClusterRoleBinding exists", "linkerd-cni-plugin cni plugin Role exists", "linkerd-cni-plugin cni plugin RoleBinding exists", "linkerd-cni-plugin cni plugin ServiceAccount exists", "linkerd-cni-plugin cni plugin DaemonSet exists: missing DaemonSet: linkerd-cni", }, }, { "fails then there is nodes are not ready", fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true, hasClusterRoleBinding: true, hasRole: true, hasRoleBinding: true, hasServiceAccount: true, hasDaemonSet: true, scheduled: 5, ready: 4}, []string{ "linkerd-cni-plugin cni plugin ConfigMap exists", "linkerd-cni-plugin cni plugin PodSecurityPolicy exists", "linkerd-cni-plugin cni plugin ClusterRole exists", "linkerd-cni-plugin cni plugin ClusterRoleBinding exists", "linkerd-cni-plugin cni plugin Role exists", "linkerd-cni-plugin cni plugin RoleBinding exists", "linkerd-cni-plugin cni plugin ServiceAccount exists", "linkerd-cni-plugin cni plugin DaemonSet exists", "linkerd-cni-plugin cni plugin pod is running on all nodes: number ready: 4, number scheduled: 5", }, }, { "fails then there is nodes are not ready", fakeCniResourcesOpts{hasConfigMap: true, hasPodSecurityPolicy: true, hasClusterRole: true, hasClusterRoleBinding: true, hasRole: true, hasRoleBinding: true, hasServiceAccount: true, hasDaemonSet: true, scheduled: 5, ready: 5}, []string{ "linkerd-cni-plugin cni plugin ConfigMap exists", "linkerd-cni-plugin cni plugin PodSecurityPolicy exists", "linkerd-cni-plugin cni plugin ClusterRole exists", "linkerd-cni-plugin cni plugin ClusterRoleBinding exists", "linkerd-cni-plugin cni plugin Role exists", "linkerd-cni-plugin cni plugin RoleBinding exists", "linkerd-cni-plugin cni plugin ServiceAccount exists", "linkerd-cni-plugin cni plugin DaemonSet exists", "linkerd-cni-plugin cni plugin pod is running on all nodes", }, }, } for _, tc := range testCases { tc := tc // pin t.Run(tc.description, func(t *testing.T) { hc := NewHealthChecker( []CategoryID{LinkerdCNIPluginChecks}, &Options{ CNINamespace: "test-ns", }, ) k8sConfigs := getFakeCniResources(tc.testCaseOpts) var err error hc.kubeAPI, err = k8s.NewFakeAPI(k8sConfigs...) hc.CNIEnabled = true if err != nil { t.Fatalf("Unexpected error: %s", err) } obs := newObserver() hc.RunChecks(obs.resultFn) if !reflect.DeepEqual(obs.results, tc.results) { t.Fatalf("Expected results\n%s,\nbut got:\n%s", strings.Join(tc.results, "\n"), strings.Join(obs.results, "\n")) } }) } } func TestMinReplicaCheck(t *testing.T) { hc := NewHealthChecker( []CategoryID{LinkerdHAChecks}, &Options{ ControlPlaneNamespace: "linkerd", }, ) var err error testCases := []struct { controlPlaneResourceDefs []string expected error }{ { controlPlaneResourceDefs: generateAllControlPlaneDef(&controlPlaneReplicaOptions{ controller: 1, destination: 3, identity: 3, proxyInjector: 3, spValidator: 1, tap: 3, }, t), expected: fmt.Errorf("not enough replicas available for [linkerd-controller linkerd-sp-validator]"), }, { controlPlaneResourceDefs: generateAllControlPlaneDef(&controlPlaneReplicaOptions{ controller: 3, destination: 2, identity: 1, proxyInjector: 1, spValidator: 0, tap: 3, }, t), expected: fmt.Errorf("not enough replicas available for [linkerd-identity linkerd-proxy-injector linkerd-sp-validator]"), }, { controlPlaneResourceDefs: generateAllControlPlaneDef(&controlPlaneReplicaOptions{ controller: 3, destination: 2, identity: 2, proxyInjector: 3, spValidator: 2, tap: 3, }, t), expected: nil, }, } for i, tc := range testCases { tc := tc //pin t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { hc.kubeAPI, err = k8s.NewFakeAPI(tc.controlPlaneResourceDefs...) if err != nil { t.Fatal(err) } err = hc.checkMinReplicasAvailable() if err == nil && tc.expected != nil { t.Log("Expected error: nil") t.Logf("Received error: %s\n", err) t.Fatal("test case failed") } if err != nil { if err.Error() != tc.expected.Error() { t.Logf("Expected error: %s\n", tc.expected) t.Logf("Received error: %s\n", err) t.Fatal("test case failed") } } }) } } type controlPlaneReplicaOptions struct { controller int destination int identity int proxyInjector int spValidator int tap int } func getSingleControlPlaneDef(component string, availableReplicas int) string { return fmt.Sprintf(` apiVersion: apps/v1 kind: Deployment metadata: name: %s namespace: linkerd spec: template: spec: containers: - image: "hello-world" name: test status: availableReplicas: %d`, component, availableReplicas) } func generateAllControlPlaneDef(replicaOptions *controlPlaneReplicaOptions, t *testing.T) []string { resourceDefs := []string{} for _, component := range linkerdHAControlPlaneComponents { switch component { case "linkerd-controller": resourceDefs = append(resourceDefs, getSingleControlPlaneDef(component, replicaOptions.controller)) case "linkerd-destination": resourceDefs = append(resourceDefs, getSingleControlPlaneDef(component, replicaOptions.destination)) case "linkerd-identity": resourceDefs = append(resourceDefs, getSingleControlPlaneDef(component, replicaOptions.identity)) case "linkerd-sp-validator": resourceDefs = append(resourceDefs, getSingleControlPlaneDef(component, replicaOptions.spValidator)) case "linkerd-proxy-injector": resourceDefs = append(resourceDefs, getSingleControlPlaneDef(component, replicaOptions.proxyInjector)) case "linkerd-tap": resourceDefs = append(resourceDefs, getSingleControlPlaneDef(component, replicaOptions.tap)) default: t.Fatal("Could not find the resource") } } return resourceDefs }