Repository: linkerd/linkerd2
Pull request #7452 (2021-12-09 21:44:59)

Use CheckPodsRunning in Linkerd healthcheck
`validateDataPlanePods` and `CheckPodsRunning` both check that for a given list of Pods, each Pod is running and has a `linkerd-proxy` container that is ready.

Aside from the error message returned when there are no Pods given, these functions do the same thing. This change merges the two in favor for `CheckPodsRunning` which is used by other packages.

Signed-off-by: Kevin Leimkuhler <kleimkuhler@icloud.com>


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index d398fa3d7..c2d86111a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -40,9 +40,9 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.51"
+version = "0.1.52"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
+checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -428,9 +428,9 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.3.7"
+version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55"
+checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd"
 dependencies = [
  "bytes",
  "fnv",
@@ -530,9 +530,9 @@ checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
 
 [[package]]
 name = "hyper"
-version = "0.14.15"
+version = "0.14.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c"
+checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -1861,9 +1861,9 @@ dependencies = [
 
 [[package]]
 name = "tonic"
-version = "0.6.1"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24203b79cf2d68909da91178db3026e77054effba0c5d93deb870d3ca7b35afa"
+checksum = "ff08f4649d10a70ffa3522ca559031285d8e421d727ac85c60825761818f5d0a"
 dependencies = [
  "async-stream",
  "async-trait",
diff --git a/jaeger/cmd/check.go b/jaeger/cmd/check.go
index 282c0572d..0a663cbfb 100644
--- a/jaeger/cmd/check.go
+++ b/jaeger/cmd/check.go
@@ -110,7 +110,7 @@ func jaegerCategory(hc *healthcheck.HealthChecker) *healthcheck.Category {
 					return err
 				}
 
-				return healthcheck.CheckPodsRunning(pods, "")
+				return healthcheck.CheckPodsRunning(pods, jaegerNamespace)
 
 			}))
 
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index f1a4dbd62..871e6b2ae 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -1312,8 +1312,7 @@ func (hc *HealthChecker) allCategories() []*Category {
 						if err != nil {
 							return err
 						}
-
-						return validateDataPlanePods(pods, hc.DataPlaneNamespace)
+						return CheckPodsRunning(pods, hc.DataPlaneNamespace)
 					},
 				},
 				{
@@ -1504,7 +1503,7 @@ func (hc *HealthChecker) CheckProxyHealth(ctx context.Context, controlPlaneNames
 	}
 
 	// Validate the status of the pods
-	err = validateDataPlanePods(podList.Items, controlPlaneNamespace)
+	err = CheckPodsRunning(podList.Items, controlPlaneNamespace)
 	if err != nil {
 		return err
 	}
@@ -2706,36 +2705,6 @@ func validateControlPlanePods(pods []corev1.Pod) error {
 	return nil
 }
 
-func validateDataPlanePods(pods []corev1.Pod, targetNamespace string) error {
-	if len(pods) == 0 {
-		msg := fmt.Sprintf("No \"%s\" containers found", k8s.ProxyContainerName)
-		if targetNamespace != "" {
-			msg += fmt.Sprintf(" in the \"%s\" namespace", targetNamespace)
-		}
-		return fmt.Errorf(msg)
-	}
-
-	for _, pod := range pods {
-		status := k8s.GetPodStatus(pod)
-		// Skip validating meshed pods that are in the `Completed` or `Shutdown` state
-		// as they do not have a running proxy
-		if status == "Completed" || status == "Shutdown" {
-			continue
-		}
-
-		if status != string(corev1.PodRunning) && status != "Evicted" {
-			return fmt.Errorf("The \"%s\" pod is not running", pod.Name)
-		}
-
-		if !k8s.GetProxyReady(pod) {
-			return fmt.Errorf("The \"%s\" container in the \"%s\" pod is not ready",
-				k8s.ProxyContainerName, pod.Name)
-		}
-	}
-
-	return nil
-}
-
 func checkUnschedulablePods(pods []corev1.Pod) error {
 	for _, pod := range pods {
 		for _, condition := range pod.Status.Conditions {
@@ -2787,17 +2756,25 @@ func CheckForPods(pods []corev1.Pod, deployNames []string) error {
 
 // CheckPodsRunning checks if the given pods are in running state
 // along with containers to be in ready state
-func CheckPodsRunning(pods []corev1.Pod, podsNotFoundMsg string) error {
-	if len(pods) == 0 && podsNotFoundMsg != "" {
-		return fmt.Errorf(podsNotFoundMsg)
+func CheckPodsRunning(pods []corev1.Pod, namespace string) error {
+	if len(pods) == 0 {
+		msg := fmt.Sprintf("no \"%s\" containers found", k8s.ProxyContainerName)
+		if namespace != "" {
+			msg += fmt.Sprintf(" in the \"%s\" namespace", namespace)
+		}
+		return fmt.Errorf(msg)
 	}
 	for _, pod := range pods {
-		if pod.Status.Phase != corev1.PodRunning && pod.Status.Phase != "Evicted" {
-			return fmt.Errorf("%s status is %s", pod.Name, pod.Status.Phase)
-		}
+		status := k8s.GetPodStatus(pod)
 
-		if !k8s.GetProxyReady(pod) {
-			return fmt.Errorf("container %s in pod %s is not ready", k8s.ProxyContainerName, pod.Name)
+		// Skip validating meshed pods that are in the `Completed` or
+		// `Shutdown` state as they do not have a running proxy
+		if status == "Completed" || status == "Shutdown" {
+			continue
+		} else if status != string(corev1.PodRunning) && status != "Evicted" {
+			return fmt.Errorf("pod \"%s\" status is %s", pod.Name, pod.Status.Phase)
+		} else if !k8s.GetProxyReady(pod) {
+			return fmt.Errorf("container \"%s\" in pod \"%s\" is not ready", k8s.ProxyContainerName, pod.Name)
 		}
 	}
 	return nil
diff --git a/viz/pkg/healthcheck/healthcheck.go b/viz/pkg/healthcheck/healthcheck.go
index 2b316970a..20e107fd9 100644
--- a/viz/pkg/healthcheck/healthcheck.go
+++ b/viz/pkg/healthcheck/healthcheck.go
@@ -164,7 +164,7 @@ func (hc *HealthChecker) VizCategory() *healthcheck.Category {
 					return err
 				}
 
-				return healthcheck.CheckPodsRunning(pods, "")
+				return healthcheck.CheckPodsRunning(pods, hc.vizNamespace)
 			}),
 		*healthcheck.NewChecker("viz extension proxies are healthy").
 			WithHintAnchor("l5d-viz-proxy-healthy").


Test patch:
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index 879be377c..7aa5a95d3 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -1491,14 +1491,14 @@ func TestValidateDataPlaneNamespace(t *testing.T) {
 	}
 }
 
-func TestValidateDataPlanePods(t *testing.T) {
+func TestCheckDataPlanePods(t *testing.T) {
 
 	t.Run("Returns an error if no inject pods were found", func(t *testing.T) {
-		err := validateDataPlanePods([]corev1.Pod{}, "emojivoto")
+		err := CheckPodsRunning([]corev1.Pod{}, "emojivoto")
 		if err == nil {
 			t.Fatal("Expected error, got nothing")
 		}
-		if err.Error() != "No \"linkerd-proxy\" containers found in the \"emojivoto\" namespace" {
+		if err.Error() != "no \"linkerd-proxy\" containers found in the \"emojivoto\" namespace" {
 			t.Fatalf("Unexpected error message: %s", err.Error())
 		}
 	})
@@ -1555,11 +1555,11 @@ func TestValidateDataPlanePods(t *testing.T) {
 			},
 		}
 
-		err := validateDataPlanePods(pods, "emojivoto")
+		err := CheckPodsRunning(pods, "emojivoto")
 		if err == nil {
 			t.Fatal("Expected error, got nothing")
 		}
-		if err.Error() != "The \"voting-65b9fffd77-rlwsd\" pod is not running" {
+		if err.Error() != "pod \"voting-65b9fffd77-rlwsd\" status is Failed" {
 			t.Fatalf("Unexpected error message: %s", err.Error())
 		}
 	})
@@ -1580,7 +1580,7 @@ func TestValidateDataPlanePods(t *testing.T) {
 			},
 		}
 
-		err := validateDataPlanePods(pods, "emojivoto")
+		err := CheckPodsRunning(pods, "emojivoto")
 		if err != nil {
 			t.Fatalf("Expected no error, got %s", err)
 		}
@@ -1597,7 +1597,7 @@ func TestValidateDataPlanePods(t *testing.T) {
 			},
 		}
 
-		err := validateDataPlanePods(pods, "emojivoto")
+		err := CheckPodsRunning(pods, "emojivoto")
 		if err != nil {
 			t.Fatalf("Expected no error, got %s", err)
 		}
@@ -1655,11 +1655,11 @@ func TestValidateDataPlanePods(t *testing.T) {
 			},
 		}
 
-		err := validateDataPlanePods(pods, "emojivoto")
+		err := CheckPodsRunning(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" {
+		if err.Error() != "container \"linkerd-proxy\" in pod \"vote-bot-644b8cb6b4-g8nlr\" is not ready" {
 			t.Fatalf("Unexpected error message: %s", err.Error())
 		}
 	})
@@ -1716,7 +1716,7 @@ func TestValidateDataPlanePods(t *testing.T) {
 			},
 		}
 
-		err := validateDataPlanePods(pods, "emojivoto")
+		err := CheckPodsRunning(pods, "emojivoto")
 		if err != nil {
 			t.Fatalf("Unexpected error: %s", err)
 		}
@@ -1777,7 +1777,7 @@ func TestValidateDataPlanePods(t *testing.T) {
 			},
 		}
 
-		err := validateDataPlanePods(pods, "emojivoto")
+		err := CheckPodsRunning(pods, "emojivoto")
 		if err != nil {
 			t.Fatalf("Unexpected error: %s", err)
 		}
diff --git a/test/integration/endpoints/endpoints_test.go b/test/integration/endpoints/endpoints_test.go
index cc1d56ca2..e97670fa5 100644
--- a/test/integration/endpoints/endpoints_test.go
+++ b/test/integration/endpoints/endpoints_test.go
@@ -2,6 +2,7 @@ package endpoints
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"os"
 	"regexp"
@@ -20,50 +21,57 @@ func TestMain(m *testing.M) {
 }
 
 func TestGoodEndpoints(t *testing.T) {
-	ns := TestHelper.GetLinkerdNamespace()
-	vizNs := TestHelper.GetVizNamespace()
+	ctx := context.Background()
+	controlNs := TestHelper.GetLinkerdNamespace()
 	testDataPath := "testdata"
 	cmd := []string{
 		"diagnostics",
 		"endpoints",
-		fmt.Sprintf("linkerd-dst.%s.svc.cluster.local:8086", ns),
-		fmt.Sprintf("grafana.%s.svc.cluster.local:3000", vizNs),
-		fmt.Sprintf("linkerd-identity.%s.svc.cluster.local:8080", ns),
-		fmt.Sprintf("linkerd-proxy-injector.%s.svc.cluster.local:443", ns),
-		fmt.Sprintf("tap.%s.svc.cluster.local:8088", vizNs),
-		fmt.Sprintf("web.%s.svc.cluster.local:8084", vizNs),
+		fmt.Sprintf("linkerd-dst.%s.svc.cluster.local:8086", controlNs),
+		fmt.Sprintf("linkerd-identity.%s.svc.cluster.local:8080", controlNs),
+		fmt.Sprintf("linkerd-proxy-injector.%s.svc.cluster.local:443", controlNs),
+		fmt.Sprintf("nginx.%s.svc.cluster.local:8080", "linkerd-endpoints-test"),
+		"-ojson",
 	}
 
-	if !TestHelper.ExternalPrometheus() {
-		cmd = append(cmd, fmt.Sprintf("prometheus.%s.svc.cluster.local:9090", vizNs))
-	} else {
-		cmd = append(cmd, "prometheus.external-prometheus.svc.cluster.local:9090")
-		testDataPath += "/external_prometheus"
-	}
+	TestHelper.WithDataPlaneNamespace(ctx, "endpoints-test", map[string]string{}, t, func(t *testing.T, ns string) {
+		out, err := TestHelper.Kubectl("", "apply", "-f", fmt.Sprintf("%s/%s", testDataPath, "nginx.yaml"), "-n", ns)
+		if err != nil {
+			testutil.AnnotatedFatalf(t, "unexpected error", "unexpected error: %v output:\n%s", err, out)
+		}
 
-	cmd = append(cmd, "-ojson")
-	out, err := TestHelper.LinkerdRun(cmd...)
-	if err != nil {
-		testutil.AnnotatedFatal(t, "unexpected error", err)
-	}
+		err = TestHelper.CheckPods(ctx, ns, "nginx", 1)
+		if err != nil {
+			if rce, ok := err.(*testutil.RestartCountError); ok {
+				testutil.AnnotatedWarn(t, "CheckPods timed-out", rce)
+			} else {
+				testutil.AnnotatedError(t, "CheckPods timed-out", err)
+			}
+		}
 
-	tpl := template.Must(template.ParseFiles(testDataPath + "/linkerd_endpoints.golden"))
-	vars := struct {
-		Ns    string
-		VizNs string
-	}{
-		ns,
-		vizNs,
-	}
-	var b bytes.Buffer
-	if err := tpl.Execute(&b, vars); err != nil {
-		testutil.AnnotatedFatalf(t, "failed to parse linkerd_endpoints.golden template", "failed to parse linkerd_endpoints.golden template: %s", err)
-	}
+		out, err = TestHelper.LinkerdRun(cmd...)
+		if err != nil {
+			testutil.AnnotatedFatal(t, "unexpected error", err)
+		}
 
-	r := regexp.MustCompile(b.String())
-	if !r.MatchString(out) {
-		testutil.AnnotatedErrorf(t, "unexpected output", "expected output:\n%s\nactual:\n%s", b.String(), out)
-	}
+		tpl := template.Must(template.ParseFiles(testDataPath + "/linkerd_endpoints.golden"))
+		vars := struct {
+			Ns         string
+			EndpointNs string
+		}{
+			controlNs,
+			ns,
+		}
+		var b bytes.Buffer
+		if err := tpl.Execute(&b, vars); err != nil {
+			testutil.AnnotatedFatalf(t, "failed to parse linkerd_endpoints.golden template", "failed to parse linkerd_endpoints.golden template: %s", err)
+		}
+
+		r := regexp.MustCompile(b.String())
+		if !r.MatchString(out) {
+			testutil.AnnotatedErrorf(t, "unexpected output", "expected output:\n%s\nactual:\n%s", b.String(), out)
+		}
+	})
 }
 
 // TODO: when #3004 gets fixed, add a negative test for mismatched ports
diff --git a/test/integration/endpoints/testdata/external_prometheus/linkerd_endpoints.golden b/test/integration/endpoints/testdata/external_prometheus/linkerd_endpoints.golden
index 1722b076a..85678ba98 100644
--- a/test/integration/endpoints/testdata/external_prometheus/linkerd_endpoints.golden
+++ b/test/integration/endpoints/testdata/external_prometheus/linkerd_endpoints.golden
@@ -26,26 +26,5 @@
     "port": 8443,
     "pod": "linkerd\-proxy\-injector-[a-f0-9]+\-[a-z0-9]+",
     "service": "linkerd\-proxy\-injector\.{{.Ns}}"
-  \},
-  \{
-    "namespace": "{{.VizNs}}",
-    "ip": "\d+\.\d+\.\d+\.\d+",
-    "port": 3000,
-    "pod": "grafana\-[a-f0-9]+\-[a-z0-9]+",
-    "service": "grafana\.{{.VizNs}}"
-  \},
-  \{
-    "namespace": "{{.VizNs}}",
-    "ip": "\d+\.\d+\.\d+\.\d+",
-    "port": 8088,
-    "pod": "tap\-[a-f0-9]+\-[a-z0-9]+",
-    "service": "tap\.{{.VizNs}}"
-  \},
-  \{
-    "namespace": "{{.VizNs}}",
-    "ip": "\d+\.\d+\.\d+\.\d+",
-    "port": 8084,
-    "pod": "web\-[a-f0-9]+\-[a-z0-9]+",
-    "service": "web\.{{.VizNs}}"
   \}
 \]
diff --git a/test/integration/endpoints/testdata/linkerd_endpoints.golden b/test/integration/endpoints/testdata/linkerd_endpoints.golden
index 640ba9a01..2d7bf22d5 100644
--- a/test/integration/endpoints/testdata/linkerd_endpoints.golden
+++ b/test/integration/endpoints/testdata/linkerd_endpoints.golden
@@ -21,31 +21,10 @@
     "service": "linkerd\-proxy\-injector\.{{.Ns}}"
   \},
   \{
-    "namespace": "{{.VizNs}}",
+    "namespace": "{{.EndpointNs}}",
     "ip": "\d+\.\d+\.\d+\.\d+",
-    "port": 3000,
-    "pod": "grafana\-[a-f0-9]+\-[a-z0-9]+",
-    "service": "grafana\.{{.VizNs}}"
-  \},
-  \{
-    "namespace": "{{.VizNs}}",
-    "ip": "\d+\.\d+\.\d+\.\d+",
-    "port": 9090,
-    "pod": "prometheus-[a-f0-9]+\-[a-z0-9]+",
-    "service": "prometheus\.{{.VizNs}}"
-  \},
-  \{
-    "namespace": "{{.VizNs}}",
-    "ip": "\d+\.\d+\.\d+\.\d+",
-    "port": 8088,
-    "pod": "tap\-[a-f0-9]+\-[a-z0-9]+",
-    "service": "tap\.{{.VizNs}}"
-  \},
-  \{
-    "namespace": "{{.VizNs}}",
-    "ip": "\d+\.\d+\.\d+\.\d+",
-    "port": 8084,
-    "pod": "web\-[a-f0-9]+\-[a-z0-9]+",
-    "service": "web\.{{.VizNs}}"
+    "port": 8080,
+    "pod": "nginx\-[a-f0-9]+\-[a-z0-9]+",
+    "service": "nginx\.{{.EndpointNs}}"
   \}
 \]
diff --git a/test/integration/endpoints/testdata/nginx.yaml b/test/integration/endpoints/testdata/nginx.yaml
new file mode 100644
index 000000000..3850c1ba6
--- /dev/null
+++ b/test/integration/endpoints/testdata/nginx.yaml
@@ -0,0 +1,32 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx
+  labels:
+    app: nginx
+spec:
+  selector:
+    matchLabels:
+      app: nginx
+  replicas: 1
+  template:
+    metadata:
+      annotations:
+        linkerd.io/inject: "enabled"
+      labels:
+        app: nginx
+    spec:
+      containers:
+        - name: nginx
+          image: nginx:alpine
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: nginx
+spec:
+  ports:
+  - name: service
+    port: 8080
+  selector:
+    app: nginx 
diff --git a/test/integration/localhost/localhost_test.go b/test/integration/localhost/localhost_test.go
index e870805e6..4a133dc40 100644
--- a/test/integration/localhost/localhost_test.go
+++ b/test/integration/localhost/localhost_test.go
@@ -2,23 +2,15 @@ package localhost
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"os"
+	"regexp"
 	"testing"
 	"time"
 
 	"github.com/linkerd/linkerd2/testutil"
 )
 
-type jsonStats struct {
-	Namespace          string   `json:"namespace"`
-	Name               string   `json:"name"`
-	Success            *float64 `json:"success"`
-	Rps                *float64 `json:"rps"`
-	TCPOpenConnections *int     `json:"tcp_open_connections"`
-}
-
 var TestHelper *testutil.TestHelper
 
 func TestMain(m *testing.M) {
@@ -66,33 +58,33 @@ func TestLocalhostServer(t *testing.T) {
 		err = TestHelper.RetryFor(50*time.Second, func() error {
 			// Use a short time window so that transient errors at startup
 			// fall out of the window.
-			out, err := TestHelper.LinkerdRun("viz", "stat", "-n", ns, "deploy/slow-cooker", "--to", "deploy/nginx", "-t", "30s", "-o", "json")
-			if err != nil {
-				testutil.AnnotatedFatalf(t, "unexpected stat error",
-					"unexpected stat error: %s\n%s", err, out)
-			}
-
-			var stats []jsonStats
-			err = json.Unmarshal([]byte(out), &stats)
+			metrics, err := TestHelper.LinkerdRun("diagnostics", "proxy-metrics", "-n", ns, "deploy/slow-cooker")
 			if err != nil {
-				return fmt.Errorf("failed to parse json stat output: %s\n%s", err, out)
-			}
-
-			if len(stats) != 1 {
-				return fmt.Errorf("expected 1 row of stat output, got: %s", out)
+				testutil.AnnotatedFatalf(t, "unexpected diagnostics error",
+					"unexpected diagnostics error: %s\n%s", err, out)
 			}
 
-			if stats[0].Rps == nil || *stats[0].Rps == 0.0 {
-				return fmt.Errorf("expected non-zero RPS from slowcooker to nginx: %s", out)
+			rpsRE := regexp.MustCompile(
+				`request_total\{direction="outbound",authority="nginx\.linkerd-localhost-test\.svc\.cluster\.local:8080",.*,dst_deployment="nginx",dst_namespace="linkerd-localhost-test",dst_pod="nginx.*",dst_pod_template_hash=".*",dst_service="nginx",dst_serviceaccount="default"\} [1-9]\d*`,
+			)
+			if !rpsRE.MatchString(metrics) {
+				return fmt.Errorf("expected non-zero RPS from slowcooker to nginx\nexpected: %s, got: %s", rpsRE, metrics)
 			}
 
 			// Requests sent to a port which is only bound to localhost should
 			// fail.
-			if *stats[0].Success >= 1.0 {
-				return fmt.Errorf("expected zero success-rate from slowcooker to nginx: %s", out)
+			successRE := regexp.MustCompile(
+				`response_total\{direction="outbound",authority="nginx\.linkerd-localhost-test\.svc\.cluster\.local:8080",.*,classification="success"\} [1-9]\d*`,
+			)
+			if successRE.MatchString(metrics) {
+				return fmt.Errorf("expected zero success-rate from slowcooker to nginx\nexpected: %s, got: %s", successRE, metrics)
 			}
-			if *stats[0].TCPOpenConnections > 0 {
-				return fmt.Errorf("expected no tcp connection from slowcooker to nginx: %s", out)
+
+			tcpConnRE := regexp.MustCompile(
+				`tcp_open_connections\{direction="outbound",peer="dst",authority="nginx\.linkerd-localhost-test\.svc\.cluster\.local:8080",.*\} 0`,
+			)
+			if !tcpConnRE.MatchString(metrics) {
+				return fmt.Errorf("expected no tcp connections from slowcooker to nginx\nexpected: %s, got: %s", tcpConnRE, metrics)
 			}
 
 			return nil